diff --git a/app/corpora/routes.py b/app/corpora/routes.py index 5484fb50..322c3657 100644 --- a/app/corpora/routes.py +++ b/app/corpora/routes.py @@ -13,6 +13,7 @@ from app.models import ( Corpus, CorpusFollowerAssociation, CorpusFollowerRole, + User ) from . import bp from .forms import CreateCorpusForm @@ -46,12 +47,14 @@ def create_corpus(): def corpus(corpus_id): corpus = Corpus.query.get_or_404(corpus_id) corpus_follower_roles = CorpusFollowerRole.query.all() + users = [u.to_json_serializeable() for u in User.query.filter(User.is_public == True, User.id != current_user.id).all()] # TODO: Add URL query option to toggle view if corpus.user == current_user or current_user.is_administrator(): return render_template( 'corpora/corpus.html.j2', corpus=corpus, corpus_follower_roles=corpus_follower_roles, + users = users, title='Corpus' ) if current_user.is_following_corpus(corpus) or corpus.is_public: diff --git a/app/jobs/json_routes.py b/app/jobs/json_routes.py index 50846bae..3562470f 100644 --- a/app/jobs/json_routes.py +++ b/app/jobs/json_routes.py @@ -1,4 +1,4 @@ -from flask import abort, current_app, jsonify +from flask import abort, current_app from flask_login import current_user, login_required from threading import Thread import os @@ -7,6 +7,7 @@ from app.decorators import admin_required, content_negotiation from app.models import Job, JobStatus from . import bp + @bp.route('/', methods=['DELETE']) @login_required @content_negotiation(produces='application/json') @@ -26,12 +27,10 @@ def delete_job(job_id): ) thread.start() response_data = { - 'message': \ - f'Job "{job.title}" marked for deletion' + 'message': f'Job "{job.title}" marked for deletion' } - response = jsonify(response_data) - response.status_code = 202 - return response + return response_data, 202 + @bp.route('//log') @login_required @@ -48,9 +47,7 @@ def job_log(job_id): 'message': '', 'jobLog': log } - response = jsonify(response_data) - response.status_code = 200 - return response + return response_data, 200 @bp.route('//restart', methods=['POST']) @@ -75,9 +72,6 @@ def restart_job(job_id): ) thread.start() response_data = { - 'message': \ - f'Job "{job.title}" marked for restarting' + 'message': f'Job "{job.title}" marked for restarting' } - response = jsonify(response_data) - response.status_code = 202 - return response + return response_data, 202 diff --git a/app/static/css/materialize/fixes.css b/app/static/css/materialize/fixes.css index b44af75f..75003a1f 100644 --- a/app/static/css/materialize/fixes.css +++ b/app/static/css/materialize/fixes.css @@ -1,3 +1,8 @@ .parallax-container .parallax { z-index: 0; } + +.autocomplete-content { + width: 100% !important; + left: 0 !important; +} diff --git a/app/static/js/Requests/users/users.js b/app/static/js/Requests/users/users.js new file mode 100644 index 00000000..0ae1434e --- /dev/null +++ b/app/static/js/Requests/users/users.js @@ -0,0 +1,23 @@ +/***************************************************************************** +* Users * +* Fetch requests for /users routes * +*****************************************************************************/ +Requests.users = {}; + +Requests.users.entity = {}; + +Requests.users.entity.delete = (userId) => { + let input = `/users/${userId}`; + let init = { + method: 'DELETE' + }; + return Requests.JSONfetch(input, init); +} + +Requests.users.entity.deleteAvatar = (userId) => { + let input = `/users/${userId}/avatar`; + let init = { + method: 'DELETE' + }; + return Requests.JSONfetch(input, init); +} diff --git a/app/static/js/Utils.js b/app/static/js/Utils.js index 822ab775..79879c75 100644 --- a/app/static/js/Utils.js +++ b/app/static/js/Utils.js @@ -69,240 +69,4 @@ class Utils { return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2)); } - static deleteProfileAvatarRequest(userId) { - return new Promise((resolve, reject) => { - let modalElement = Utils.HTMLToElement( - ` - - ` - ); - document.querySelector('#modals').appendChild(modalElement); - let modal = M.Modal.init( - modalElement, - { - dismissible: false, - onCloseEnd: () => { - modal.destroy(); - modalElement.remove(); - } - } - ); - - let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]'); - confirmElement.addEventListener('click', (event) => { - fetch(`/users/${userId}/avatar`, {method: 'DELETE', headers: {Accept: 'application/json'}}) - .then( - (response) => { - if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);} - if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);} - app.flash(`Avatar marked for deletion`); - resolve(response); - }, - (response) => { - app.flash('Something went wrong', 'error'); - reject(response); - } - ); - }); - modal.open(); - }); - } - - // static deleteJobRequest(userId, jobId) { - // return new Promise((resolve, reject) => { - // let job; - // try { - // job = app.data.users[userId].jobs[jobId]; - // } catch (error) { - // job = {}; - // } - - // let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]'); - // confirmElement.addEventListener('click', (event) => { - // let jobTitle = job?.title; - // fetch(`/jobs/${jobId}`, {method: 'DELETE', headers: {Accept: 'application/json'}}) - // .then( - // (response) => { - // if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);} - // if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);} - // app.flash(`Job "${jobTitle}" marked for deletion`, 'job'); - // resolve(response); - // }, - // (response) => { - // app.flash('Something went wrong', 'error'); - // reject(response); - // } - // ); - // }); - // modal.open(); - // }); - // } - - // static getJobLogRequest(userId, jobId) { - // return new Promise((resolve, reject) => { - // fetch(`/jobs/${jobId}/log`, {method: 'GET', headers: {Accept: 'application/json, text/plain'}}) - // .then( - // (response) => { - // if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);} - // if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);} - // return response.text(); - // }, - // (response) => { - // app.flash('Something went wrong', 'error'); - // reject(response); - // } - // ) - // .then( - // (text) => { - // let modalElement = Utils.HTMLToElement( - // ` - // - // ` - // ); - // document.querySelector('#modals').appendChild(modalElement); - // let modal = M.Modal.init( - // modalElement, - // { - // onCloseEnd: () => { - // modal.destroy(); - // modalElement.remove(); - // } - // } - // ); - // modal.open(); - // resolve(text); - // } - // ); - // }); - // } - - // static restartJobRequest(userId, jobId) { - // return new Promise((resolve, reject) => { - // let job; - // try { - // job = app.data.users[userId].jobs[jobId]; - // } catch (error) { - // job = {}; - // } - - // let modalElement = Utils.HTMLToElement( - // ` - // - // ` - // ); - // document.querySelector('#modals').appendChild(modalElement); - // let modal = M.Modal.init( - // modalElement, - // { - // dismissible: false, - // onCloseEnd: () => { - // modal.destroy(); - // modalElement.remove(); - // } - // } - // ); - - // let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]'); - // confirmElement.addEventListener('click', (event) => { - // let jobTitle = job?.title; - // fetch(`/jobs/${jobId}/restart`, {method: 'POST', headers: {Accept: 'application/json'}}) - // .then( - // (response) => { - // if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);} - // if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);} - // if (response.status === 409) {app.flash('Conflict', 'error'); reject(response);} - // app.flash(`Job "${jobTitle}" restarted.`, 'job'); - // resolve(response); - // }, - // (response) => { - // app.flash('Something went wrong', 'error'); - // reject(response); - // } - // ); - // }); - // modal.open(); - // }); - // } - - static deleteUserRequest(userId) { - return new Promise((resolve, reject) => { - let user; - try { - user = app.data.users[userId]; - } catch (error) { - user = {}; - } - - let modalElement = Utils.HTMLToElement( - ` - - ` - ); - document.querySelector('#modals').appendChild(modalElement); - let modal = M.Modal.init( - modalElement, - { - dismissible: false, - onCloseEnd: () => { - modal.destroy(); - modalElement.remove(); - } - } - ); - - let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]'); - confirmElement.addEventListener('click', (event) => { - let userName = user?.username; - fetch(`/users/${userId}`, {method: 'DELETE', headers: {Accept: 'application/json'}}) - .then( - (response) => { - if (response.status === 403) {app.flash('Forbidden', 'error'); reject(response);} - if (response.status === 404) {app.flash('Not Found', 'error'); reject(response);} - app.flash(`User "${userName}" marked for deletion`); - resolve(response); - }, - (response) => { - app.flash('Something went wrong', 'error'); - reject(response); - } - ); - }); - modal.open(); - }); - } } diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index 6dc253b4..11398cb9 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -64,7 +64,8 @@ 'js/Requests/corpora/corpora.js', 'js/Requests/corpora/files.js', 'js/Requests/corpora/followers.js', - 'js/Requests/jobs/jobs.js' + 'js/Requests/jobs/jobs.js', + 'js/Requests/users/users.js' %} {%- endassets %} diff --git a/app/templates/_sidenav.html.j2 b/app/templates/_sidenav.html.j2 index 0027772e..2d55999a 100644 --- a/app/templates/_sidenav.html.j2 +++ b/app/templates/_sidenav.html.j2 @@ -39,7 +39,7 @@
  • Account
  • settingsGeneral Settings
  • -
  • contact_pageProfile settings
  • +
  • contact_pageProfile Settings
  • Log out
  • {% if current_user.can('ADMINISTRATE') or current_user.can('USE_API') %}
  • diff --git a/app/templates/corpora/corpus.js.j2 b/app/templates/corpora/corpus.js.j2 index 7e815cf3..016b0b6a 100644 --- a/app/templates/corpora/corpus.js.j2 +++ b/app/templates/corpora/corpus.js.j2 @@ -26,16 +26,17 @@ deleteModalDeleteButtonElement.addEventListener('click', (event) => { let inviteUserModalElement = document.querySelector('#invite-user-modal'); let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search'); let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button'); +const users = {}; + +for (let user of {{ users|tojson }}) { + users[user.username] = user.avatar ? `/users/${user.id}/avatar` : '/static/images/user_avatar.png'; +} let inviteUserModalSearch = M.Chips.init( inviteUserModalSearchElement, { autocompleteOptions: { - data: { - 'nopaque': '/users/3V8Aqpg74JvxOd9o/avatar', - 'pjentsch': '/users/3V8Aqpg74JvxOd9o/avatar', - 'pjentsch2': '/users/3V8Aqpg74JvxOd9o/avatar' - } + data: users }, limit: 3, onChipAdd: (a, chipElement) => { @@ -43,7 +44,7 @@ let inviteUserModalSearch = M.Chips.init( chipElement.firstElementChild.click(); } }, - placeholder: 'Enter a username', + placeholder: 'Enter username', secondaryPlaceholder: 'Add more users' } ); diff --git a/app/templates/settings/settings.html.j2 b/app/templates/settings/settings.html.j2 index e9cfb97b..9210d36f 100644 --- a/app/templates/settings/settings.html.j2 +++ b/app/templates/settings/settings.html.j2 @@ -51,7 +51,7 @@ @@ -59,12 +59,26 @@ {% endblock page_content %} +{% block modals%} + +{% endblock modals %} + {% block scripts %} {{ super() }} {% endblock scripts %} diff --git a/app/templates/users/edit_profile.html.j2 b/app/templates/users/edit_profile.html.j2 index 11b4b4d2..1b2ef3f9 100644 --- a/app/templates/users/edit_profile.html.j2 +++ b/app/templates/users/edit_profile.html.j2 @@ -79,7 +79,7 @@
    @@ -111,44 +111,15 @@
    {% endblock page_content %} -{% block scripts %} -{{ super() }} - -{% endblock scripts %} +{% block modals %} + +{% endblock modals %} diff --git a/app/templates/users/edit_profile.js.j2 b/app/templates/users/edit_profile.js.j2 new file mode 100644 index 00000000..a5058d85 --- /dev/null +++ b/app/templates/users/edit_profile.js.j2 @@ -0,0 +1,36 @@ +let publicProfile = document.querySelector('#public-profile'); +let disableButtons = document.querySelectorAll('[data-action="disable"]'); +let deleteButton = document.querySelector('#delete-avatar'); +let avatar = document.querySelector('#avatar'); +let avatarUpload = document.querySelector('#avatar-upload'); + +for (let disableButton of disableButtons) { + disableButton.disabled = !publicProfile.checked; +} + +publicProfile.addEventListener('change', () => { + if (publicProfile.checked) { + for (let disableButton of disableButtons) { + disableButton.disabled = false; + } + } else { + for (let disableButton of disableButtons) { + disableButton.checked = false; + disableButton.disabled = true; + } + } +}); + +avatarUpload.addEventListener('change', function() { + let file = this.files[0]; + avatar.src = URL.createObjectURL(file); +}); + +deleteButton.addEventListener('click', () => { + Requests.users.entity.deleteAvatar({{ user.hashid|tojson }}) + .then( + (response) => { + avatar.src = "{{ url_for('static', filename='images/user_avatar.png') }}"; + } + ); +}); diff --git a/app/templates/users/profile.html.j2 b/app/templates/users/profile.html.j2 index 4a9a4c13..e73a3b7a 100644 --- a/app/templates/users/profile.html.j2 +++ b/app/templates/users/profile.html.j2 @@ -111,28 +111,3 @@
    {% endblock page_content %} -{% block scripts %} -{{ super() }} - -{% endblock scripts %} - diff --git a/app/templates/users/profile.js.j2 b/app/templates/users/profile.js.j2 new file mode 100644 index 00000000..5f729f18 --- /dev/null +++ b/app/templates/users/profile.js.j2 @@ -0,0 +1,19 @@ +let publicInformationBadge = document.querySelector('#public-information-badge'); +if ("{{ user.id }}" == "{{ current_user.hashid }}") { + if ("{{ user.is_public }}" == "True") { + publicInformationBadge.dataset.badgeCaption = 'Your profile is public'; + publicInformationBadge.classList.add('green'); + publicInformationBadge.classList.remove('red'); + } else { + publicInformationBadge.dataset.badgeCaption = 'Your profile is private'; + publicInformationBadge.classList.add('red'); + publicInformationBadge.classList.remove('green'); + } +} else { + publicInformationBadge.remove(); +} + +let followedCorpusList = new FollowedCorpusList(document.querySelector('.followed-corpus-list')); +followedCorpusList.add({{ followed_corpora|tojson }}); +let publicCorpusList = new PublicCorpusList(document.querySelector('.public-corpus-list')); +publicCorpusList.add({{ own_public_corpora|tojson }}); diff --git a/app/users/__init__.py b/app/users/__init__.py index 885cdbe2..705b3afa 100644 --- a/app/users/__init__.py +++ b/app/users/__init__.py @@ -2,4 +2,5 @@ from flask import Blueprint bp = Blueprint('users', __name__) -from . import events, routes +from . import events, routes, json_routes + diff --git a/app/users/json_routes.py b/app/users/json_routes.py new file mode 100644 index 00000000..0e421078 --- /dev/null +++ b/app/users/json_routes.py @@ -0,0 +1,54 @@ +from flask import abort, current_app +from flask_login import current_user, login_required, logout_user +from threading import Thread +import os +from app import db +from app.decorators import content_negotiation +from app.models import Avatar, User +from . import bp + +@bp.route('/', methods=['DELETE']) +@login_required +@content_negotiation(produces='application/json') +def delete_user(user_id): + def _delete_user(app, user_id): + with app.app_context(): + user = User.query.get(user_id) + user.delete() + db.session.commit() + + user = User.query.get_or_404(user_id) + if not (user == current_user or current_user.is_administrator()): + abort(403) + thread = Thread( + target=_delete_user, + args=(current_app._get_current_object(), user_id) + ) + if user == current_user: + logout_user() + thread.start() + response_data = { + 'message': f'User "{user.username}" marked for deletion' + } + return response_data, 202 + +@bp.route('//avatar', methods=['DELETE']) +@content_negotiation(produces='application/json') +def delete_profile_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) + 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 diff --git a/app/users/routes.py b/app/users/routes.py index d573da34..5d8ff266 100644 --- a/app/users/routes.py +++ b/app/users/routes.py @@ -51,24 +51,7 @@ def user(user_id): user_id=user_id ) -@bp.route('/', methods=['DELETE']) -@login_required -def delete_user(user_id): - def _delete_user(app, user_id): - with app.app_context(): - user = User.query.get(user_id) - user.delete() - db.session.commit() - user = User.query.get_or_404(user_id) - if not (user == current_user or current_user.is_administrator()): - abort(403) - thread = Thread( - target=_delete_user, - args=(current_app._get_current_object(), user_id) - ) - thread.start() - return {}, 202 @bp.route('//avatar') def profile_avatar(user_id): @@ -86,24 +69,6 @@ def profile_avatar(user_id): ) -@bp.route('//avatar', methods=['DELETE']) -def delete_profile_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) - thread = Thread( - target=_delete_avatar, - args=(current_app._get_current_object(), user.avatar.id) - ) - thread.start() - return {}, 202 - - @bp.route('//edit', methods=['GET', 'POST']) def edit_profile(user_id): user = User.query.get_or_404(user_id)