Reviewed User package and invite user optical fix

This commit is contained in:
Inga Kirschnick 2023-03-13 13:29:01 +01:00
parent 7e54d56ed5
commit 646f735ab2
16 changed files with 190 additions and 364 deletions

View File

@ -13,6 +13,7 @@ from app.models import (
Corpus, Corpus,
CorpusFollowerAssociation, CorpusFollowerAssociation,
CorpusFollowerRole, CorpusFollowerRole,
User
) )
from . import bp from . import bp
from .forms import CreateCorpusForm from .forms import CreateCorpusForm
@ -46,12 +47,14 @@ def create_corpus():
def corpus(corpus_id): def corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id) corpus = Corpus.query.get_or_404(corpus_id)
corpus_follower_roles = CorpusFollowerRole.query.all() 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 # TODO: Add URL query option to toggle view
if corpus.user == current_user or current_user.is_administrator(): if corpus.user == current_user or current_user.is_administrator():
return render_template( return render_template(
'corpora/corpus.html.j2', 'corpora/corpus.html.j2',
corpus=corpus, corpus=corpus,
corpus_follower_roles=corpus_follower_roles, corpus_follower_roles=corpus_follower_roles,
users = users,
title='Corpus' title='Corpus'
) )
if current_user.is_following_corpus(corpus) or corpus.is_public: if current_user.is_following_corpus(corpus) or corpus.is_public:

View File

@ -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 flask_login import current_user, login_required
from threading import Thread from threading import Thread
import os import os
@ -7,6 +7,7 @@ from app.decorators import admin_required, content_negotiation
from app.models import Job, JobStatus from app.models import Job, JobStatus
from . import bp from . import bp
@bp.route('/<hashid:job_id>', methods=['DELETE']) @bp.route('/<hashid:job_id>', methods=['DELETE'])
@login_required @login_required
@content_negotiation(produces='application/json') @content_negotiation(produces='application/json')
@ -26,12 +27,10 @@ def delete_job(job_id):
) )
thread.start() thread.start()
response_data = { response_data = {
'message': \ 'message': f'Job "{job.title}" marked for deletion'
f'Job "{job.title}" marked for deletion'
} }
response = jsonify(response_data) return response_data, 202
response.status_code = 202
return response
@bp.route('/<hashid:job_id>/log') @bp.route('/<hashid:job_id>/log')
@login_required @login_required
@ -48,9 +47,7 @@ def job_log(job_id):
'message': '', 'message': '',
'jobLog': log 'jobLog': log
} }
response = jsonify(response_data) return response_data, 200
response.status_code = 200
return response
@bp.route('/<hashid:job_id>/restart', methods=['POST']) @bp.route('/<hashid:job_id>/restart', methods=['POST'])
@ -75,9 +72,6 @@ def restart_job(job_id):
) )
thread.start() thread.start()
response_data = { response_data = {
'message': \ 'message': f'Job "{job.title}" marked for restarting'
f'Job "{job.title}" marked for restarting'
} }
response = jsonify(response_data) return response_data, 202
response.status_code = 202
return response

View File

@ -1,3 +1,8 @@
.parallax-container .parallax { .parallax-container .parallax {
z-index: 0; z-index: 0;
} }
.autocomplete-content {
width: 100% !important;
left: 0 !important;
}

View File

@ -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);
}

View File

@ -69,240 +69,4 @@ class Utils {
return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2)); return Utils.mergeObjectsDeep(mergedObject, ...objects.slice(2));
} }
static deleteProfileAvatarRequest(userId) {
return new Promise((resolve, reject) => {
let modalElement = Utils.HTMLToElement(
`
<div class="modal">
<div class="modal-content">
<h4>Confirm Avatar deletion</h4>
<p>Do you really want to delete your Avatar?</p>
</div>
<div class="modal-footer">
<a class="action-button btn modal-close waves-effect waves-light" data-action="cancel">Cancel</a>
<a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
</div>
</div>
`
);
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(
// `
// <div class="modal">
// <div class="modal-content">
// <h4>Job logs</h4>
// <pre><code>${text}</code></pre>
// </div>
// <div class="modal-footer">
// <a class="btn modal-close waves-effect waves-light">Close</a>
// </div>
// </div>
// `
// );
// 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(
// `
// <div class="modal">
// <div class="modal-content">
// <h4>Confirm Job restart</h4>
// <p>Do you really want to restart the Job <b>${job?.title}</b>? All Job Results will be permanently deleted.</p>
// </div>
// <div class="modal-footer">
// <a class="action-button btn modal-close waves-effect waves-light" data-action="cancel">Cancel</a>
// <a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Restart</a>
// </div>
// </div>
// `
// );
// 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(
`
<div class="modal">
<div class="modal-content">
<h4>Confirm User deletion</h4>
<p>Do you really want to delete the User <b>${user?.username}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a class="action-button btn modal-close waves-effect waves-light" data-action="cancel">Cancel</a>
<a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
</div>
</div>
`
);
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();
});
}
} }

View File

@ -64,7 +64,8 @@
'js/Requests/corpora/corpora.js', 'js/Requests/corpora/corpora.js',
'js/Requests/corpora/files.js', 'js/Requests/corpora/files.js',
'js/Requests/corpora/followers.js', 'js/Requests/corpora/followers.js',
'js/Requests/jobs/jobs.js' 'js/Requests/jobs/jobs.js',
'js/Requests/users/users.js'
%} %}
<script src="{{ ASSET_URL }}"></script> <script src="{{ ASSET_URL }}"></script>
{%- endassets %} {%- endassets %}

View File

@ -39,7 +39,7 @@
<li><div class="divider"></div></li> <li><div class="divider"></div></li>
<li><a class="subheader">Account</a></li> <li><a class="subheader">Account</a></li>
<li><a href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>General Settings</a></li> <li><a href="{{ url_for('settings.settings') }}"><i class="material-icons">settings</i>General Settings</a></li>
<li><a href="{{ url_for('users.edit_profile', user_id=current_user.id) }}"><i class="material-icons left">contact_page</i>Profile settings</a></li> <li><a href="{{ url_for('users.edit_profile', user_id=current_user.id) }}"><i class="material-icons left">contact_page</i>Profile Settings</a></li>
<li><a href="{{ url_for('auth.logout') }}">Log out</a></li> <li><a href="{{ url_for('auth.logout') }}">Log out</a></li>
{% if current_user.can('ADMINISTRATE') or current_user.can('USE_API') %} {% if current_user.can('ADMINISTRATE') or current_user.can('USE_API') %}
<li><div class="divider"></div></li> <li><div class="divider"></div></li>

View File

@ -26,16 +26,17 @@ deleteModalDeleteButtonElement.addEventListener('click', (event) => {
let inviteUserModalElement = document.querySelector('#invite-user-modal'); let inviteUserModalElement = document.querySelector('#invite-user-modal');
let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search'); let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search');
let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button'); 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( let inviteUserModalSearch = M.Chips.init(
inviteUserModalSearchElement, inviteUserModalSearchElement,
{ {
autocompleteOptions: { autocompleteOptions: {
data: { data: users
'nopaque': '/users/3V8Aqpg74JvxOd9o/avatar',
'pjentsch': '/users/3V8Aqpg74JvxOd9o/avatar',
'pjentsch2': '/users/3V8Aqpg74JvxOd9o/avatar'
}
}, },
limit: 3, limit: 3,
onChipAdd: (a, chipElement) => { onChipAdd: (a, chipElement) => {
@ -43,7 +44,7 @@ let inviteUserModalSearch = M.Chips.init(
chipElement.firstElementChild.click(); chipElement.firstElementChild.click();
} }
}, },
placeholder: 'Enter a username', placeholder: 'Enter username',
secondaryPlaceholder: 'Add more users' secondaryPlaceholder: 'Add more users'
} }
); );

View File

@ -51,7 +51,7 @@
</ul> </ul>
</div> </div>
<div class="card-action right-align"> <div class="card-action right-align">
<a class="btn red waves-effect waves-light" id="delete-user"><i class="material-icons left">delete</i>Delete</a> <a class="btn red waves-effect waves-light modal-trigger" href="#delete-user"><i class="material-icons left">delete</i>Delete</a>
</div> </div>
</div> </div>
</div> </div>
@ -59,12 +59,26 @@
</div> </div>
{% endblock page_content %} {% endblock page_content %}
{% block modals%}
<div class="modal" id="delete-user">
<div class="modal-content">
<h4>Confirm User deletion</h4>
<p>Do you really want to delete the User <b>{{ current_user.username }}</b>? All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="btn modal-close red waves-effect waves-light" id="delete-user-button">Delete</a>
</div>
</div>
{% endblock modals %}
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script> <script>
document.querySelector('#delete-user').addEventListener('click', (event) => { document.querySelector('#delete-user-button').addEventListener('click', (event) => {
Utils.deleteUserRequest(currentUserId) Requests.users.entity.delete(currentUserId)
.then((response) => {window.location.href = '/';}); .then((response) => {window.location.href = '/';});
}); });
</script> </script>
{% endblock scripts %} {% endblock scripts %}

View File

@ -79,7 +79,7 @@
<div class="row"> <div class="row">
<div class="col s2"> <div class="col s2">
<div class="center-align"> <div class="center-align">
<a class="action-button btn-floating red waves-effect waves-light" style="margin-top:15px;" id="delete-avatar"><i class="material-icons">delete</i></a> <a class="btn-floating red waves-effect waves-light modal-trigger" style="margin-top:15px;" href="#delete-avatar-modal"><i class="material-icons">delete</i></a>
</div> </div>
</div> </div>
<div class="col s10"> <div class="col s10">
@ -111,44 +111,15 @@
</div> </div>
{% endblock page_content %} {% endblock page_content %}
{% block scripts %} {% block modals %}
{{ super() }} <div class="modal" id="delete-avatar-modal">
<script> <div class="modal-content">
let publicProfile = document.querySelector('#public-profile'); <h4>Confirm Avatar deletion</h4>
let disableButtons = document.querySelectorAll('[data-action="disable"]'); <p>Do you really want to delete your Avatar?</p>
let deleteButton = document.querySelector('#delete-avatar'); </div>
let avatar = document.querySelector('#avatar'); <div class="modal-footer">
let avatarUpload = document.querySelector('#avatar-upload'); <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>
for (let disableButton of disableButtons) { </div>
disableButton.disabled = !publicProfile.checked; </div>
} {% endblock modals %}
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', () => {
Utils.deleteProfileAvatarRequest({{ user.hashid|tojson }})
.then(
(response) => {
avatar.src = "{{ url_for('static', filename='images/user_avatar.png') }}";
}
);
});
</script>
{% endblock scripts %}

View File

@ -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') }}";
}
);
});

View File

@ -111,28 +111,3 @@
</div> </div>
{% endblock page_content %} {% endblock page_content %}
{% block scripts %}
{{ super() }}
<script>
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 }});
</script>
{% endblock scripts %}

View File

@ -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 }});

View File

@ -2,4 +2,5 @@ from flask import Blueprint
bp = Blueprint('users', __name__) bp = Blueprint('users', __name__)
from . import events, routes from . import events, routes, json_routes

54
app/users/json_routes.py Normal file
View File

@ -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('/<hashid:user_id>', 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('/<hashid:user_id>/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

View File

@ -51,24 +51,7 @@ def user(user_id):
user_id=user_id user_id=user_id
) )
@bp.route('/<hashid:user_id>', 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('/<hashid:user_id>/avatar') @bp.route('/<hashid:user_id>/avatar')
def profile_avatar(user_id): def profile_avatar(user_id):
@ -86,24 +69,6 @@ def profile_avatar(user_id):
) )
@bp.route('/<hashid:user_id>/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('/<hashid:user_id>/edit', methods=['GET', 'POST']) @bp.route('/<hashid:user_id>/edit', methods=['GET', 'POST'])
def edit_profile(user_id): def edit_profile(user_id):
user = User.query.get_or_404(user_id) user = User.query.get_or_404(user_id)