From f4d3415c11032afde1588839654a661cefc52118 Mon Sep 17 00:00:00 2001
From: Patrick Jentsch <p.jentsch@uni-bielefeld.de>
Date: Tue, 24 Oct 2023 16:11:08 +0200
Subject: [PATCH] First work to bring back Community Update functionality

---
 app/corpora/followers/json_routes.py          | 116 +++++++++---------
 app/main/routes.py                            |  24 ++--
 app/templates/_sidenav.html.j2                |  11 +-
 app/templates/corpora/corpus.html.j2          |  32 ++---
 app/templates/users/settings/settings.html.j2 |  24 ++--
 app/users/json_routes.py                      |  44 +++----
 app/users/routes.py                           |  58 ++++-----
 7 files changed, 154 insertions(+), 155 deletions(-)

diff --git a/app/corpora/followers/json_routes.py b/app/corpora/followers/json_routes.py
index db6bb635..87299862 100644
--- a/app/corpora/followers/json_routes.py
+++ b/app/corpora/followers/json_routes.py
@@ -12,65 +12,65 @@ from ..decorators import corpus_follower_permission_required
 from . import bp
 
 
-# @bp.route('/<hashid:corpus_id>/followers', methods=['POST'])
-# @corpus_follower_permission_required('MANAGE_FOLLOWERS')
-# @content_negotiation(consumes='application/json', produces='application/json')
-# def create_corpus_followers(corpus_id):
-#     usernames = request.json
-#     if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)):
-#         abort(400)
-#     corpus = Corpus.query.get_or_404(corpus_id)
-#     for username in usernames:
-#         user = User.query.filter_by(username=username, is_public=True).first_or_404()
-#         user.follow_corpus(corpus)
-#     db.session.commit()
-#     response_data = {
-#         'message': f'Users are now following "{corpus.title}"',
-#         'category': 'corpus'
-#     }
-#     return response_data, 200
+@bp.route('/<hashid:corpus_id>/followers', methods=['POST'])
+@corpus_follower_permission_required('MANAGE_FOLLOWERS')
+@content_negotiation(consumes='application/json', produces='application/json')
+def create_corpus_followers(corpus_id):
+    usernames = request.json
+    if not (isinstance(usernames, list) or all(isinstance(u, str) for u in usernames)):
+        abort(400)
+    corpus = Corpus.query.get_or_404(corpus_id)
+    for username in usernames:
+        user = User.query.filter_by(username=username, is_public=True).first_or_404()
+        user.follow_corpus(corpus)
+    db.session.commit()
+    response_data = {
+        'message': f'Users are now following "{corpus.title}"',
+        'category': 'corpus'
+    }
+    return response_data, 200
 
 
-# @bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/role', methods=['PUT'])
-# @corpus_follower_permission_required('MANAGE_FOLLOWERS')
-# @content_negotiation(consumes='application/json', produces='application/json')
-# def update_corpus_follower_role(corpus_id, follower_id):
-#     role_name = request.json
-#     if not isinstance(role_name, str):
-#         abort(400)
-#     cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
-#     if cfr is None:
-#         abort(400)
-#     cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
-#     cfa.role = cfr
-#     db.session.commit()
-#     response_data = {
-#         'message': f'User "{cfa.follower.username}" is now {cfa.role.name}',
-#         'category': 'corpus'
-#     }
-#     return response_data, 200
+@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>/role', methods=['PUT'])
+@corpus_follower_permission_required('MANAGE_FOLLOWERS')
+@content_negotiation(consumes='application/json', produces='application/json')
+def update_corpus_follower_role(corpus_id, follower_id):
+    role_name = request.json
+    if not isinstance(role_name, str):
+        abort(400)
+    cfr = CorpusFollowerRole.query.filter_by(name=role_name).first()
+    if cfr is None:
+        abort(400)
+    cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
+    cfa.role = cfr
+    db.session.commit()
+    response_data = {
+        'message': f'User "{cfa.follower.username}" is now {cfa.role.name}',
+        'category': 'corpus'
+    }
+    return response_data, 200
 
 
-# @bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>', methods=['DELETE'])
-# def delete_corpus_follower(corpus_id, follower_id):
-#     cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
-#     if not (
-#         current_user.id == follower_id
-#         or current_user == cfa.corpus.user 
-#         or CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first().role.has_permission('MANAGE_FOLLOWERS')
-#         or current_user.is_administrator()):
-#         abort(403)
-#     if current_user.id == follower_id:
-#         flash(f'You are no longer following "{cfa.corpus.title}"', 'corpus')
-#         response = make_response()
-#         response.status_code = 204
-#     else:
-#         response_data = {
-#             'message': f'"{cfa.follower.username}" is not following "{cfa.corpus.title}" anymore',
-#             'category': 'corpus'
-#         }
-#         response = jsonify(response_data)
-#         response.status_code = 200
-#     cfa.follower.unfollow_corpus(cfa.corpus)
-#     db.session.commit()
-#     return response
+@bp.route('/<hashid:corpus_id>/followers/<hashid:follower_id>', methods=['DELETE'])
+def delete_corpus_follower(corpus_id, follower_id):
+    cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=follower_id).first_or_404()
+    if not (
+        current_user.id == follower_id
+        or current_user == cfa.corpus.user 
+        or CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first().role.has_permission('MANAGE_FOLLOWERS')
+        or current_user.is_administrator()):
+        abort(403)
+    if current_user.id == follower_id:
+        flash(f'You are no longer following "{cfa.corpus.title}"', 'corpus')
+        response = make_response()
+        response.status_code = 204
+    else:
+        response_data = {
+            'message': f'"{cfa.follower.username}" is not following "{cfa.corpus.title}" anymore',
+            'category': 'corpus'
+        }
+        response = jsonify(response_data)
+        response.status_code = 200
+    cfa.follower.unfollow_corpus(cfa.corpus)
+    db.session.commit()
+    return response
diff --git a/app/main/routes.py b/app/main/routes.py
index 3be92196..080616ec 100644
--- a/app/main/routes.py
+++ b/app/main/routes.py
@@ -78,15 +78,15 @@ def terms_of_use():
     )
 
 
-# @bp.route('/social-area')
-# @register_breadcrumb(bp, '.social_area', '<i class="material-icons left">group</i>Social Area')
-# @login_required
-# def social_area():
-#     corpora = Corpus.query.filter(Corpus.is_public == True, Corpus.user != current_user).all()
-#     users = User.query.filter(User.is_public == True, User.id != current_user.id).all()
-#     return render_template(
-#         'main/social_area.html.j2',
-#         title='Social Area',
-#         corpora=corpora,
-#         users=users
-#     )
+@bp.route('/social-area')
+@register_breadcrumb(bp, '.social_area', '<i class="material-icons left">group</i>Social Area')
+@login_required
+def social_area():
+    corpora = Corpus.query.filter(Corpus.is_public == True, Corpus.user != current_user).all()
+    users = User.query.filter(User.is_public == True, User.id != current_user.id).all()
+    return render_template(
+        'main/social_area.html.j2',
+        title='Social Area',
+        corpora=corpora,
+        users=users
+    )
diff --git a/app/templates/_sidenav.html.j2 b/app/templates/_sidenav.html.j2
index 06fc0b97..765e566e 100644
--- a/app/templates/_sidenav.html.j2
+++ b/app/templates/_sidenav.html.j2
@@ -3,13 +3,12 @@
     <div class="user-view" style="padding-top: 1px; padding-left: 20px !important; padding-right: 20px !important; height: 112px;">
       <div class="background primary-color"></div>
       <div class="row">
-        {# <div class="col s5">
+        <div class="col s5">
           <a href="{{ url_for('users.user', user_id=current_user.id) }}">
             <img src="{{ url_for('users.user_avatar', user_id=current_user.id) }}" alt="user-image" class="circle responsive-img"  style="height:80%; margin-top: 22px;">
           </a>
-        </div> #}
-        {# Change col s12 to col s5 to show user image #}
-        <div class="col s12" style="word-wrap: break-word; margin-left:-10px;">
+        </div>
+        <div class="col s5" style="word-wrap: break-word; margin-left:-10px;">
           <span class="white-text name">
           {% if current_user.username|length > 18 %}
             {{ current_user.username[:15] + '...' }}
@@ -71,7 +70,7 @@
   <li class="service-color service-color-border border-darken" data-service="corpus-analysis" style="border-left: 10px solid; margin-top: 5px;">
     <a class="waves-effect" href="{{ url_for('services.corpus_analysis') }}"><i class="nopaque-icons service-icons" data-service="corpus-analysis"></i>Corpus Analysis</a>
   </li>
-  {# <li><div class="divider"></div></li>
+  <li><div class="divider"></div></li>
   <li>
     <a class="waves-effect" class="waves-effect" href="{{ url_for('main.social_area') }}"><i class="material-icons">rocket_launch</i>Social Area</a>
     <ul>
@@ -82,7 +81,7 @@
     <a class="waves-effect" href="{{ url_for('main.social_area', _anchor='public-corpora') }}" style="padding-left: 47px;"><i class="nopaque-icons">I</i>Public Corpora</a>
   </li>
     </ul>
-  </li> #}
+  </li>
   <li class="hide-on-large-only"><div class="divider"></div></li>
   <li class="hide-on-large-only"><a class="subheader">Account</a></li>
   <li class="hide-on-large-only">
diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2
index b6e600b9..e31ed50c 100644
--- a/app/templates/corpora/corpus.html.j2
+++ b/app/templates/corpora/corpus.html.j2
@@ -65,14 +65,14 @@
             <div class="col s12 l6" style="padding: 0 2.5px;">
               <a class="action-button btn disabled waves-effect waves-light" data-action="analyze" href="{{ url_for('corpora.analysis', corpus_id=corpus.id) }}" style="width: 100%;"><i class="material-icons left">search</i>Analyze</a>
             </div>
-            {# <div class="col s12 l6" style="padding: 5px 2.5px 0 2.5px;">
+            <div class="col s12 l6" style="padding: 5px 2.5px 0 2.5px;">
               <a class="btn waves-effect waves-light modal-trigger" href="#publishing-modal" style="width: 100%;"><i class="material-icons left">publish</i>Publishing</a>
-            </div> #}
+            </div>
             <div class="col s12 l6" style="padding: 5px 2.5px 0 2.5px;">
               <a class="btn red waves-effect waves-light modal-trigger" href="#delete-modal" style="width: 100%;"><i class="material-icons left">delete</i>Delete</a>
             </div>
           </div>
-          {# {% if cfr.has_permission('MANAGE_FOLLOWERS') %}
+          {% if cfr.has_permission('MANAGE_FOLLOWERS') %}
           <span class="card-title">Social</span>
           <div class="row">
             <div class="col s12 l6" style="padding: 0 2.5px;">
@@ -82,7 +82,7 @@
               <a class="btn waves-effect waves-light modal-trigger" href="#share-link-modal" style="width: 100%;"><i class="material-icons left">link</i>Share link</a>
             </div>
           </div>
-          {% endif %} #}
+          {% endif %}
         </div>
       </div>
     </div>
@@ -102,7 +102,7 @@
       </div>
     </div>
 
-    {# {% if cfr.has_permission('MANAGE_FOLLOWERS') %}
+    {% if cfr.has_permission('MANAGE_FOLLOWERS') %}
     <div class="col s12">
       <div class="card">
         <div class="card-content">
@@ -111,7 +111,7 @@
         </div>
       </div>
     </div>
-    {% endif %} #}
+    {% endif %}
 
   </div>
 </div>
@@ -121,7 +121,7 @@
 {{ super() }}
 
 {% if current_user == corpus.user or current_user.is_administrator() %}
-{# <div class="modal" id="publishing-modal">
+<div class="modal" id="publishing-modal">
   <div class="modal-content">
     <h4>Change your Corpus publishing status</h4>
     <p><i>Other users can only see the meta data of your corpus. The files of the corpus remain private and can only be viewed via a share link.</i></p>
@@ -138,7 +138,7 @@
   <div class="modal-footer">
     <a class="modal-close waves-effect waves-green btn-flat">Close</a>
   </div>
-</div> #}
+</div>
 
 <div class="modal" id="delete-modal">
   <div class="modal-content">
@@ -152,7 +152,7 @@
 </div>
 {% endif %}
 
-{# {% if cfr.has_permission('MANAGE_FOLLOWERS') %}
+{% if cfr.has_permission('MANAGE_FOLLOWERS') %}
 <div class="modal no-autoinit" id="invite-user-modal">
   <div class="modal-content">
     <h4>Invite a nopaque user by username</h4>
@@ -230,14 +230,14 @@
     <a class="modal-close waves-effect waves-green btn-flat">Close</a>
   </div>
 </div>
-{% endif %} #}
+{% endif %}
 
 {% endblock modals %}
 
 {% block scripts %}
 {{ super() }}
 <script>
-{# {% if current_user.is_following_corpus(corpus) %}
+{% if current_user.is_following_corpus(corpus) %}
   let unfollowRequestElement = document.querySelector('.action-button[data-action="unfollow-request"]');
   unfollowRequestElement.addEventListener('click', () => {
     requests.corpora.entity.followers.entity.delete({{ corpus.hashid|tojson }}, {{ current_user.hashid|tojson }})
@@ -245,18 +245,18 @@
         window.location.reload();
       });
   });
-{% endif %} #}
+{% endif %}
 
 {% if current_user == corpus.user or current_user.is_administrator() %}
 // #region Publishing
-{# let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
+let publishingModalIsPublicSwitchElement = document.querySelector('#publishing-modal-is-public-switch');
 publishingModalIsPublicSwitchElement.addEventListener('change', (event) => {
   let newIsPublic = publishingModalIsPublicSwitchElement.checked;
   requests.corpora.entity.isPublic.update({{ corpus.hashid|tojson }}, newIsPublic)
     .catch((response) => {
       publishingModalIsPublicSwitchElement.checked = !newIsPublic;
     });
-}); #}
+});
 // #endregion Publishing
 
 // #region Delete
@@ -272,7 +272,7 @@ deleteModalDeleteButtonElement.addEventListener('click', (event) => {
 
 {% if cfr.has_permission('MANAGE_FOLLOWERS') %}
 // #region Invite user
-{# let inviteUserModalElement = document.querySelector('#invite-user-modal');
+let inviteUserModalElement = document.querySelector('#invite-user-modal');
 let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search');
 let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button');
 let users = {
@@ -374,7 +374,7 @@ shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
       () => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');}
     );
   
-}); #}
+});
 // #endregion Share link
 {% endif %}
 
diff --git a/app/templates/users/settings/settings.html.j2 b/app/templates/users/settings/settings.html.j2
index 82b8a928..4e52344a 100644
--- a/app/templates/users/settings/settings.html.j2
+++ b/app/templates/users/settings/settings.html.j2
@@ -8,7 +8,7 @@
       <h1 id="title">{{ title }}</h1>
     </div>
 
-    {# <div class="col s12 l4">
+    <div class="col s12 l4">
       <h4>Profile Settings</h4>
       <p>You can edit your public profile here and share it with other nopaque users. 
       Tell others about your (scientific) background so they can relate and network with you. 
@@ -112,7 +112,7 @@
       </ul>
     </div>
 
-    <div class="col s12"></div> #}
+    <div class="col s12"></div>
 
     <div class="col s12 l4">
       <h4>General Settings</h4>
@@ -181,7 +181,7 @@
 
 {% block modals %}
 {{ super() }}
-{# <div class="modal" id="delete-avatar-modal">
+<div class="modal" id="delete-avatar-modal">
   <div class="modal-content">
     <h4>Confirm Avatar deletion</h4>
     <p>Do you really want to delete <b>{{ user.username }}</b>’s avatar?</p>
@@ -190,7 +190,7 @@
     <a class="btn modal-close waves-effect waves-light">Cancel</a>
     <a class="btn modal-close red waves-effect waves-light" id="delete-avatar">Delete</a>
   </div>
-</div> #}
+</div>
 
 <div class="modal" id="delete-user-modal">
   <div class="modal-content">
@@ -212,7 +212,7 @@
 {% block scripts %}
 {{ super() }}
 <script>
-{# let avatarPreviewElement = document.querySelector('#update-avatar-form-avatar-preview');
+let avatarPreviewElement = document.querySelector('#update-avatar-form-avatar-preview');
 let avatarUploadElement = document.querySelector('#update-avatar-form-avatar');
 
 avatarUploadElement.addEventListener('change', () => {
@@ -221,16 +221,16 @@ avatarUploadElement.addEventListener('change', () => {
 });
 
 document.querySelector('#delete-avatar').addEventListener('click', () => {
-  requests.users.entity.avatar.delete({{ user.hashid|tojson }})
+  Requests.users.entity.avatar.delete({{ user.hashid|tojson }})
     .then(
       (response) => {
         avatarPreviewElement.src = {{ url_for('static', filename='images/user_avatar.png')|tojson }};
       }
     );
-}); #}
+});
 
 document.querySelector('#delete-user').addEventListener('click', (event) => {
-  requests.users.entity.delete({{ user.hashid|tojson }})
+  Requests.users.entity.delete({{ user.hashid|tojson }})
     .then((response) => {window.location.href = '/';});
 });
 
@@ -251,11 +251,11 @@ for (let collapsibleElement of document.querySelectorAll('.collapsible.no-autoin
 }
 
 // #region Profile Privacy settings
-{# let profileIsPublicSwitchElement = document.querySelector('#profile-is-public-switch');
+let profileIsPublicSwitchElement = document.querySelector('#profile-is-public-switch');
 let profilePrivacySettingCheckboxElements = document.querySelectorAll('.profile-privacy-setting-checkbox');
 profileIsPublicSwitchElement.addEventListener('change', (event) => {
   let newEnabled = profileIsPublicSwitchElement.checked;
-  requests.users.entity.settings.profilePrivacy.update({{ user.hashid|tojson }}, 'is-public', newEnabled)
+  Requests.users.entity.settings.profilePrivacy.update({{ user.hashid|tojson }}, 'is-public', newEnabled)
     .then(
       (response) => {
         for (let profilePrivacySettingCheckboxElement of document.querySelectorAll('.profile-privacy-setting-checkbox')) {
@@ -271,12 +271,12 @@ for (let profilePrivacySettingCheckboxElement of profilePrivacySettingCheckboxEl
   profilePrivacySettingCheckboxElement.addEventListener('change', (event) => {
     let newEnabled = profilePrivacySettingCheckboxElement.checked;
     let valueName = profilePrivacySettingCheckboxElement.dataset.profilePrivacySettingName;
-    requests.users.entity.settings.profilePrivacy.update({{ user.hashid|tojson }}, valueName, newEnabled)
+    Requests.users.entity.settings.profilePrivacy.update({{ user.hashid|tojson }}, valueName, newEnabled)
       .catch((response) => {
         profilePrivacySettingCheckboxElement.checked = !newEnabled;
       });
   });
-} #}
+}
 // #endregion Profile Privacy settings
 </script>
 {% endblock scripts %}
diff --git a/app/users/json_routes.py b/app/users/json_routes.py
index 0e51631c..0e1503d6 100644
--- a/app/users/json_routes.py
+++ b/app/users/json_routes.py
@@ -32,29 +32,29 @@ def delete_user(user_id):
     return response_data, 202
 
 
-# @bp.route('/<hashid:user_id>/avatar', methods=['DELETE'])
-# @content_negotiation(produces='application/json')
-# def delete_user_avatar(user_id):
-#     def _delete_avatar(app, avatar_id):
-#         with app.app_context():
-#             avatar = Avatar.query.get(avatar_id)
-#             avatar.delete()
-#             db.session.commit()
+@bp.route('/<hashid:user_id>/avatar', methods=['DELETE'])
+@content_negotiation(produces='application/json')
+def delete_user_avatar(user_id):
+    def _delete_avatar(app, avatar_id):
+        with app.app_context():
+            avatar = Avatar.query.get(avatar_id)
+            avatar.delete()
+            db.session.commit()
 
-#     user = User.query.get_or_404(user_id)
-#     if user.avatar is None:
-#         abort(404)
-#     if not (user == current_user or current_user.is_administrator()):
-#         abort(403)
-#     thread = Thread(
-#         target=_delete_avatar,
-#         args=(current_app._get_current_object(), user.avatar.id)
-#     )
-#     thread.start()
-#     response_data = {
-#         'message': f'Avatar marked for deletion'
-#     }
-#     return response_data, 202
+    user = User.query.get_or_404(user_id)
+    if user.avatar is None:
+        abort(404)
+    if not (user == current_user or current_user.is_administrator()):
+        abort(403)
+    thread = Thread(
+        target=_delete_avatar,
+        args=(current_app._get_current_object(), user.avatar.id)
+    )
+    thread.start()
+    response_data = {
+        'message': f'Avatar marked for deletion'
+    }
+    return response_data, 202
 
 @bp.route('/accept-terms-of-use', methods=['POST'])
 @content_negotiation(produces='application/json')
diff --git a/app/users/routes.py b/app/users/routes.py
index 8c817617..fbb5a609 100644
--- a/app/users/routes.py
+++ b/app/users/routes.py
@@ -13,36 +13,36 @@ from . import bp
 from .utils import user_dynamic_list_constructor as user_dlc
 
 
-# @bp.route('')
-# @register_breadcrumb(bp, '.', '<i class="material-icons left">group</i>Users')
-# def users():
-#     return redirect(url_for('main.social_area', _anchor='users'))
+@bp.route('')
+@register_breadcrumb(bp, '.', '<i class="material-icons left">group</i>Users')
+def users():
+    return redirect(url_for('main.social_area', _anchor='users'))
 
 
-# @bp.route('/<hashid:user_id>')
-# @register_breadcrumb(bp, '.entity', '', dynamic_list_constructor=user_dlc)
-# def user(user_id):
-#     user = User.query.get_or_404(user_id)
-#     if not (user.is_public or user == current_user or current_user.is_administrator()):
-#         abort(403)
-#     return render_template(
-#         'users/user.html.j2',
-#         title=user.username,
-#         user=user
-#     )
+@bp.route('/<hashid:user_id>')
+@register_breadcrumb(bp, '.entity', '', dynamic_list_constructor=user_dlc)
+def user(user_id):
+    user = User.query.get_or_404(user_id)
+    if not (user.is_public or user == current_user or current_user.is_administrator()):
+        abort(403)
+    return render_template(
+        'users/user.html.j2',
+        title=user.username,
+        user=user
+    )
 
 
-# @bp.route('/<hashid:user_id>/avatar')
-# def user_avatar(user_id):
-#     user = User.query.get_or_404(user_id)
-#     if not (user.is_public or user == current_user or current_user.is_administrator()):
-#         abort(403)
-#     if user.avatar is None:
-#         return redirect(url_for('static', filename='images/user_avatar.png'))
-#     return send_from_directory(
-#         os.path.dirname(user.avatar.path),
-#         os.path.basename(user.avatar.path),
-#         as_attachment=True,
-#         attachment_filename=user.avatar.filename,
-#         mimetype=user.avatar.mimetype
-#     )
+@bp.route('/<hashid:user_id>/avatar')
+def user_avatar(user_id):
+    user = User.query.get_or_404(user_id)
+    if not (user.is_public or user == current_user or current_user.is_administrator()):
+        abort(403)
+    if user.avatar is None:
+        return redirect(url_for('static', filename='images/user_avatar.png'))
+    return send_from_directory(
+        os.path.dirname(user.avatar.path),
+        os.path.basename(user.avatar.path),
+        as_attachment=True,
+        attachment_filename=user.avatar.filename,
+        mimetype=user.avatar.mimetype
+    )
-- 
GitLab