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