diff --git a/app/admin/forms.py b/app/admin/forms.py index b063b938a45ce939764d96ecb4733775f22621ea..f24ce8f36ae2776c1382cd38c464842a2f98b70b 100644 --- a/app/admin/forms.py +++ b/app/admin/forms.py @@ -3,11 +3,12 @@ from app.forms import NopaqueForm from app.models import Role -class AdminEditUserForm(NopaqueForm): - confirmed = BooleanField('Confirmed') +class UpdateUserForm(NopaqueForm): role = SelectField('Role') - submit = SubmitField('Submit') + submit = SubmitField() - def __init__(self, *args, **kwargs): + def __init__(self, user, *args, **kwargs): + if 'data' not in kwargs: + kwargs['data'] = {'role': user.role.hashid} super().__init__(*args, **kwargs) self.role.choices = [(x.hashid, x.name) for x in Role.query.all()] diff --git a/app/admin/json_routes.py b/app/admin/json_routes.py index d8b454255092384f9b7eac898e7792e2330f5504..d4c21307940395a72fa5253025670c342625ee37 100644 --- a/app/admin/json_routes.py +++ b/app/admin/json_routes.py @@ -1,22 +1,23 @@ -from flask import current_app -from threading import Thread +from flask import abort, request from app import db +from app.decorators import content_negotiation from app.models import User from . import bp -@bp.route('/users/<hashid:user_id>/delete', methods=['DELETE']) -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.query.get_or_404(user_id) - thread = Thread( - target=_delete_user, - args=(current_app._get_current_object(), user_id) - ) - thread.start() - return {}, 202 +@bp.route('/users/<hashid:user_id>/confirmed', methods=['PUT']) +@content_negotiation(consumes='application/json', produces='application/json') +def update_user_role(user_id): + confirmed = request.json + if not isinstance(confirmed, bool): + abort(400) + user = User.query.get_or_404(user_id) + user.confirmed = confirmed + db.session.commit() + resonse_data = { + 'message': ( + f'User "{user.username}" is now ' + f'{"confirmed" if confirmed else "unconfirmed"}' + ) + } + return resonse_data, 200 diff --git a/app/admin/routes.py b/app/admin/routes.py index 899b4dfd5b7d9ce00252b71ba2ae1e77abceb2ec..9a5401b8ed6debe0c051a42dd0f7db8a7ad942d6 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -1,15 +1,16 @@ from flask import abort, flash, redirect, render_template, url_for from flask_breadcrumbs import register_breadcrumb -from app import db -from app.models import Avatar, Corpus, User +from app import db, hashids +from app.models import Avatar, Corpus, Role, User from app.settings.forms import ( - ChangePasswordForm, - EditNotificationsForm, - EditAccountForm, - EditProfileForm + UpdateAvatarForm, + UpdatePasswordForm, + UpdateNotificationsForm, + UpdateAccountInformationForm, + UpdateProfileInformationForm ) from . import bp -from .forms import AdminEditUserForm +from .forms import UpdateUserForm from app.users.utils import ( user_endpoint_arguments_constructor as user_eac, user_dynamic_list_constructor as user_dlc @@ -62,112 +63,82 @@ def user(user_id): @register_breadcrumb(bp, '.users.entity.settings', '<i class="material-icons left">settings</i>Settings') def user_settings(user_id): user = User.query.get_or_404(user_id) - # region forms - edit_account_form = EditAccountForm(user=user) - edit_profile_form = EditProfileForm(user=user) - change_password_form = ChangePasswordForm(user=user) - edit_notifications_form = EditNotificationsForm(user=user) - # endregion forms - # region handle edit profile settings form - if edit_account_form.validate_on_submit(): - user.email = edit_account_form.email.data - user.username = edit_account_form.username.data + update_account_information_form = UpdateAccountInformationForm(user=user) + update_profile_information_form = UpdateProfileInformationForm(user=user) + update_avatar_form = UpdateAvatarForm(user=user) + update_password_form = UpdatePasswordForm(user=user) + update_notifications_form = UpdateNotificationsForm(user=user) + update_user_form = UpdateUserForm(user) + + # region handle update profile information form + if update_profile_information_form.submit.data and update_profile_information_form.validate(): + user.about_me = update_profile_information_form.about_me.data + user.location = update_profile_information_form.location.data + user.organization = update_profile_information_form.organization.data + user.website = update_profile_information_form.website.data + user.full_name = update_profile_information_form.full_name.data db.session.commit() - flash('Profile settings updated') - return redirect(url_for('.user_settings')) - # endregion handle edit profile settings forms - # region handle edit public profile information form - if edit_profile_form.validate_on_submit(): - if edit_profile_form.avatar.data: - try: - Avatar.create( - edit_profile_form.avatar.data, - user=user - ) - except (AttributeError, OSError): - abort(500) - user.about_me = edit_profile_form.about_me.data - user.location = edit_profile_form.location.data - user.organization = edit_profile_form.organization.data - user.website = edit_profile_form.website.data - user.full_name = edit_profile_form.full_name.data + flash('Your changes have been saved') + return redirect(url_for('.user_settings', user_id=user.id)) + # endregion handle update profile information form + + # region handle update avatar form + if update_avatar_form.submit.data and update_avatar_form.validate(): + try: + Avatar.create( + update_avatar_form.avatar.data, + user=user + ) + except (AttributeError, OSError): + abort(500) db.session.commit() flash('Your changes have been saved') - return redirect(url_for('.user_settings')) - # endregion handle edit public profile information form - # region handle change_password_form POST - if change_password_form.validate_on_submit(): - user.password = change_password_form.new_password.data + return redirect(url_for('.user_settings', user_id=user.id)) + # endregion handle update avatar form + + # region handle update account information form + if update_account_information_form.submit.data and update_account_information_form.validate(): + user.email = update_account_information_form.email.data + user.username = update_account_information_form.username.data + db.session.commit() + flash('Profile settings updated') + return redirect(url_for('.user_settings', user_id=user.id)) + # endregion handle update account information form + + # region handle update password form + if update_password_form.submit.data and update_password_form.validate(): + user.password = update_password_form.new_password.data db.session.commit() flash('Your changes have been saved') - return redirect(url_for('.user_settings')) - # endregion handle change_password_form POST - # region handle edit_notification_settings_form POST - if edit_notifications_form.validate_on_submit(): + return redirect(url_for('.user_settings', user_id=user.id)) + # endregion handle update password form + + # region handle update notifications form + if update_notifications_form.submit.data and update_notifications_form.validate(): user.setting_job_status_mail_notification_level = \ - edit_notifications_form.job_status_mail_notification_level.data + update_notifications_form.job_status_mail_notification_level.data + db.session.commit() + flash('Your changes have been saved') + return redirect(url_for('.user_settings', user_id=user.id)) + # endregion handle update notifications form + + # region handle update user form + if update_user_form.submit.data and update_user_form.validate(): + role_id = hashids.decode(update_user_form.role.data) + user.role = Role.query.get(role_id) db.session.commit() flash('Your changes have been saved') - return redirect(url_for('.user_settings')) - # endregion handle edit_notification_settings_form POST + return redirect(url_for('.user_settings', user_id=user.id)) + # endregion handle update user form + return render_template( 'admin/user_settings.html.j2', title='Settings', - change_password_form=change_password_form, - edit_account_form=edit_account_form, - edit_notifications_form=edit_notifications_form, - edit_profile_form=edit_profile_form, + update_account_information_form=update_account_information_form, + update_avatar_form=update_avatar_form, + update_notifications_form=update_notifications_form, + update_password_form=update_password_form, + update_profile_information_form=update_profile_information_form, + update_user_form=update_user_form, user=user ) - - - -# @bp.route('/users/<hashid:user_id>/edit', methods=['GET', 'POST']) -# @register_breadcrumb(bp, '.users.entity.edit', 'Edit', endpoint_arguments_constructor=user_eac) -# def edit_user(user_id): -# user = User.query.get_or_404(user_id) -# admin_edit_user_form = AdminEditUserForm( -# data={'confirmed': user.confirmed, 'role': user.role.hashid}, -# prefix='admin-edit-user-form' -# ) -# edit_profile_settings_form = EditAccountForm( -# user, -# data=user.to_json_serializeable(), -# prefix='edit-profile-settings-form' -# ) -# edit_notification_settings_form = EditNotificationsForm( -# data=user.to_json_serializeable(), -# prefix='edit-notification-settings-form' -# ) -# if (admin_edit_user_form.submit.data -# and admin_edit_user_form.validate()): -# user.confirmed = admin_edit_user_form.confirmed.data -# role_id = hashids.decode(admin_edit_user_form.role.data) -# user.role = Role.query.get(role_id) -# db.session.commit() -# flash('Your changes have been saved') -# return redirect(url_for('.edit_user', user_id=user.id)) -# if (edit_profile_settings_form.submit.data -# and edit_profile_settings_form.validate()): -# user.email = edit_profile_settings_form.email.data -# user.username = edit_profile_settings_form.username.data -# db.session.commit() -# flash('Your changes have been saved') -# return redirect(url_for('.edit_user', user_id=user.id)) -# if (edit_notification_settings_form.submit.data -# and edit_notification_settings_form.validate()): -# user.setting_job_status_mail_notification_level = \ -# UserSettingJobStatusMailNotificationLevel[ -# edit_notification_settings_form.job_status_mail_notification_level.data # noqa -# ] -# db.session.commit() -# flash('Your changes have been saved') -# return redirect(url_for('.edit_user', user_id=user.id)) -# return render_template( -# 'admin/edit_user.html.j2', -# admin_edit_user_form=admin_edit_user_form, -# edit_profile_settings_form=edit_profile_settings_form, -# edit_notification_settings_form=edit_notification_settings_form, -# title='Edit user', -# user=user -# ) diff --git a/app/api/schemas.py b/app/api/schemas.py index f0792f7c4a32c5ca8c92b3b6da06bf55feee1c54..74f4cb2acd234ebce50edadb7a2c258658c3744d 100644 --- a/app/api/schemas.py +++ b/app/api/schemas.py @@ -2,7 +2,6 @@ from apifairy.fields import FileField from marshmallow import validate, validates, ValidationError from marshmallow.decorators import post_dump from app import ma -from app.auth import USERNAME_REGEX from app.models import ( Job, JobStatus, @@ -142,7 +141,10 @@ class UserSchema(ma.SQLAlchemySchema): username = ma.auto_field( validate=[ validate.Length(min=1, max=64), - validate.Regexp(USERNAME_REGEX, error='Usernames must have only letters, numbers, dots or underscores') + validate.Regexp( + User.username_pattern, + error='Usernames must have only letters, numbers, dots or underscores' + ) ] ) email = ma.auto_field(validate=validate.Email()) diff --git a/app/auth/__init__.py b/app/auth/__init__.py index 505e7e424b80a144a62ef2c6caaac7f2796c68f5..6f6ba82da7ab031509e4865c49b9f0c13c0f8f5a 100644 --- a/app/auth/__init__.py +++ b/app/auth/__init__.py @@ -1,8 +1,5 @@ from flask import Blueprint -USERNAME_REGEX = '^[A-Za-zÄÖÜäöüß0-9_.]*$' - - bp = Blueprint('auth', __name__) from . import routes diff --git a/app/auth/forms.py b/app/auth/forms.py index 5655b3af9fbcb697757ad39e39d4c5c5c0c08441..43db510ae5b29a5926e7891825fbe161e22e76c6 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -8,7 +8,6 @@ from wtforms import ( from wtforms.validators import InputRequired, Email, EqualTo, Length, Regexp from app.forms import NopaqueForm from app.models import User -from . import USERNAME_REGEX class RegistrationForm(NopaqueForm): @@ -22,7 +21,7 @@ class RegistrationForm(NopaqueForm): InputRequired(), Length(max=64), Regexp( - USERNAME_REGEX, + User.username_pattern, message=( 'Usernames must have only letters, numbers, dots or ' 'underscores' diff --git a/app/forms.py b/app/forms.py index 55e80d01ec1b2d4a960a2c6f631a3246bba7671d..6ac58347d86dab0bd7980573224c8d8f53cefd96 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,9 +1,26 @@ from flask_wtf import FlaskForm +from wtforms.validators import ValidationError import re +form_prefix_pattern = re.compile(r'(?<!^)(?=[A-Z])') + + +def LimitFileSize(max_size_mb): + max_size_b = max_size_mb * 1024 * 1024 + def file_length_check(form, field): + if len(field.data.read()) >= max_size_b: + raise ValidationError( + f'File size must be less or equal than {max_size_mb} MB' + ) + field.data.seek(0) + return file_length_check + + class NopaqueForm(FlaskForm): def __init__(self, *args, **kwargs): if 'prefix' not in kwargs: - kwargs['prefix'] = re.sub(r'(?<!^)(?=[A-Z])', '-', self.__class__.__name__).lower() + kwargs['prefix'] = \ + form_prefix_pattern.sub('-', self.__class__.__name__).lower() super().__init__(*args, **kwargs) + diff --git a/app/settings/forms.py b/app/settings/forms.py index 2cfccf5fe3dc8c187c4b9271265c84107cfa6d3a..6778bbe6ad236a9a7d5f7044588dac5511e9437e 100644 --- a/app/settings/forms.py +++ b/app/settings/forms.py @@ -1,6 +1,6 @@ from flask_login import current_user +from flask_wtf.file import FileField, FileRequired from wtforms import ( - FileField, PasswordField, SelectField, StringField, @@ -15,13 +15,11 @@ from wtforms.validators import ( Length, Regexp ) -from app.forms import NopaqueForm +from app.forms import NopaqueForm, LimitFileSize from app.models import User, UserSettingJobStatusMailNotificationLevel -from app.auth import USERNAME_REGEX -from app.wtf_validators import FileSizeLimit -class EditAccountForm(NopaqueForm): +class UpdateAccountInformationForm(NopaqueForm): email = StringField( 'E-Mail', validators=[DataRequired(), Length(max=254), Email()] @@ -32,7 +30,7 @@ class EditAccountForm(NopaqueForm): DataRequired(), Length(max=64), Regexp( - USERNAME_REGEX, + User.username_pattern, message=( 'Usernames must have only letters, numbers, dots or ' 'underscores' @@ -42,8 +40,7 @@ class EditAccountForm(NopaqueForm): ) submit = SubmitField() - def __init__(self, *args, **kwargs): - user = kwargs.get('user', current_user._get_current_object()) + def __init__(self, *args, user=current_user, **kwargs): if 'data' not in kwargs: kwargs['data'] = user.to_json_serializeable() super().__init__(*args, **kwargs) @@ -59,15 +56,8 @@ class EditAccountForm(NopaqueForm): and User.query.filter_by(username=field.data).first()): raise ValidationError('Username already in use') - def validate_on_submit(self): - return self.submit.data and self.validate() - -class EditProfileForm(NopaqueForm): - avatar = FileField( - 'Image File', - [FileSizeLimit(max_size_in_mb=2)] - ) +class UpdateProfileInformationForm(NopaqueForm): full_name = StringField( 'Full name', validators=[Length(max=128)] @@ -98,21 +88,22 @@ class EditProfileForm(NopaqueForm): ) submit = SubmitField() - def __init__(self, *args, **kwargs): + def __init__(self, *args, user=current_user, **kwargs): if 'data' not in kwargs: - user = current_user._get_current_object() kwargs['data'] = user.to_json_serializeable() super().__init__(*args, **kwargs) - def validate_image_file(self, field): - if not field.data.filename.lower().endswith('.jpg' or '.png' or '.jpeg'): - raise ValidationError('only .jpg, .png and .jpeg!') - def validate_on_submit(self): - return self.submit.data and self.validate() +class UpdateAvatarForm(NopaqueForm): + avatar = FileField('File', validators=[FileRequired(), LimitFileSize(2)]) + submit = SubmitField() + def validate_avatar(self, field): + valid_mimetypes = ['image/jpeg', 'image/png'] + if field.data.mimetype not in valid_mimetypes: + raise ValidationError('JPEG and PNG files only!') -class ChangePasswordForm(NopaqueForm): +class UpdatePasswordForm(NopaqueForm): password = PasswordField('Old password', validators=[DataRequired()]) new_password = PasswordField( 'New password', @@ -130,8 +121,7 @@ class ChangePasswordForm(NopaqueForm): ) submit = SubmitField() - def __init__(self, *args, **kwargs): - user = kwargs.get('user', current_user._get_current_object()) + def __init__(self, *args, user=current_user, **kwargs): super().__init__(*args, **kwargs) self.user = user @@ -139,11 +129,8 @@ class ChangePasswordForm(NopaqueForm): if not self.user.verify_password(field.data): raise ValidationError('Invalid password') - def validate_on_submit(self): - return self.submit.data and self.validate() - -class EditNotificationsForm(NopaqueForm): +class UpdateNotificationsForm(NopaqueForm): job_status_mail_notification_level = SelectField( 'Job status mail notification level', choices=[ @@ -154,11 +141,7 @@ class EditNotificationsForm(NopaqueForm): ) submit = SubmitField() - def __init__(self, *args, **kwargs): + def __init__(self, *args, user=current_user, **kwargs): if 'data' not in kwargs: - user = current_user._get_current_object() kwargs['data'] = user.to_json_serializeable() super().__init__(*args, **kwargs) - - def validate_on_submit(self): - return self.submit.data and self.validate() diff --git a/app/settings/routes.py b/app/settings/routes.py index 7a9b8a0938c5da374ce04302f8f739c5b01ceb53..70b69d6ca7e9b6d7aaea97fb2a158667a9d2c68c 100644 --- a/app/settings/routes.py +++ b/app/settings/routes.py @@ -5,10 +5,11 @@ from app import db from app.models import Avatar from . import bp from .forms import ( - ChangePasswordForm, - EditNotificationsForm, - EditAccountForm, - EditProfileForm + UpdateAvatarForm, + UpdatePasswordForm, + UpdateNotificationsForm, + UpdateAccountInformationForm, + UpdateProfileInformationForm ) @@ -16,61 +17,72 @@ from .forms import ( @register_breadcrumb(bp, '.', '<i class="material-icons left">settings</i>Settings') @login_required def settings(): - user = current_user._get_current_object() - # region forms - edit_account_form = EditAccountForm() - edit_profile_form = EditProfileForm() - change_password_form = ChangePasswordForm() - edit_notifications_form = EditNotificationsForm() - # endregion forms - # region handle edit profile settings form - if edit_account_form.validate_on_submit(): - user.email = edit_account_form.email.data - user.username = edit_account_form.username.data + user = current_user + update_account_information_form = UpdateAccountInformationForm() + update_profile_information_form = UpdateProfileInformationForm() + update_avatar_form = UpdateAvatarForm() + update_password_form = UpdatePasswordForm() + update_notifications_form = UpdateNotificationsForm() + + # region handle update profile information form + if update_profile_information_form.submit.data and update_profile_information_form.validate(): + user.about_me = update_profile_information_form.about_me.data + user.location = update_profile_information_form.location.data + user.organization = update_profile_information_form.organization.data + user.website = update_profile_information_form.website.data + user.full_name = update_profile_information_form.full_name.data db.session.commit() - flash('Profile settings updated') + flash('Your changes have been saved') return redirect(url_for('.settings')) - # endregion handle edit profile settings forms - # region handle edit public profile information form - if edit_profile_form.validate_on_submit(): - if edit_profile_form.avatar.data: - try: - Avatar.create( - edit_profile_form.avatar.data, - user=user - ) - except (AttributeError, OSError): - abort(500) - user.about_me = edit_profile_form.about_me.data - user.location = edit_profile_form.location.data - user.organization = edit_profile_form.organization.data - user.website = edit_profile_form.website.data - user.full_name = edit_profile_form.full_name.data + # endregion handle update profile information form + + # region handle update avatar form + if update_avatar_form.submit.data and update_avatar_form.validate(): + try: + Avatar.create( + update_avatar_form.avatar.data, + user=user + ) + except (AttributeError, OSError): + abort(500) db.session.commit() flash('Your changes have been saved') return redirect(url_for('.settings')) - # endregion handle edit public profile information form - # region handle change_password_form POST - if change_password_form.validate_on_submit(): - user.password = change_password_form.new_password.data + # endregion handle update avatar form + + # region handle update account information form + if update_account_information_form.submit.data and update_account_information_form.validate(): + user.email = update_account_information_form.email.data + user.username = update_account_information_form.username.data + db.session.commit() + flash('Profile settings updated') + return redirect(url_for('.settings')) + # endregion handle update account information form + + # region handle update password form + if update_password_form.submit.data and update_password_form.validate(): + user.password = update_password_form.new_password.data db.session.commit() flash('Your changes have been saved') return redirect(url_for('.settings')) - # endregion handle change_password_form POST - # region handle edit_notification_settings_form POST - if edit_notifications_form.validate_on_submit(): + # endregion handle update password form + + # region handle update notifications form + if update_notifications_form.submit.data and update_notifications_form.validate(): user.setting_job_status_mail_notification_level = \ - edit_notifications_form.job_status_mail_notification_level.data + update_notifications_form.job_status_mail_notification_level.data db.session.commit() flash('Your changes have been saved') return redirect(url_for('.settings')) - # endregion handle edit_notification_settings_form POST + # endregion handle update notifications form + return render_template( 'settings/settings.html.j2', title='Settings', - change_password_form=change_password_form, - edit_account_form=edit_account_form, - edit_notifications_form=edit_notifications_form, - edit_profile_form=edit_profile_form, + update_account_information_form=update_account_information_form, + update_avatar_form=update_avatar_form, + update_notifications_form=update_notifications_form, + update_password_form=update_password_form, + update_profile_information_form=update_profile_information_form, user=user ) diff --git a/app/static/js/Requests/admin/admin.js b/app/static/js/Requests/admin/admin.js new file mode 100644 index 0000000000000000000000000000000000000000..77fdb6b12105658507dae35a14e807256448ca79 --- /dev/null +++ b/app/static/js/Requests/admin/admin.js @@ -0,0 +1,20 @@ +/***************************************************************************** +* Admin * +* Fetch requests for /admin routes * +*****************************************************************************/ +Requests.admin = {}; + +Requests.admin.users = {}; + +Requests.admin.users.entity = {}; + +Requests.admin.users.entity.confirmed = {}; + +Requests.admin.users.entity.confirmed.update = (userId, value) => { + let input = `/admin/users/${userId}/confirmed`; + let init = { + method: 'PUT', + body: JSON.stringify(value) + }; + return Requests.JSONfetch(input, init); +}; diff --git a/app/templates/_scripts.html.j2 b/app/templates/_scripts.html.j2 index bed2aef2ce9a4c91e1ec8ca55d83453bbc5fe142..45e5457bf6ba668d964c658867a543fb2a386b6f 100644 --- a/app/templates/_scripts.html.j2 +++ b/app/templates/_scripts.html.j2 @@ -58,6 +58,7 @@ filters='rjsmin', output='gen/Requests.%(version)s.js', 'js/Requests/Requests.js', + 'js/Requests/admin/admin.js', 'js/Requests/contributions/contributions.js', 'js/Requests/contributions/spacy_nlp_pipeline_models.js', 'js/Requests/contributions/tesseract_ocr_pipeline_models.js', diff --git a/app/templates/admin/user.html.j2 b/app/templates/admin/user.html.j2 index dc5af986209a98928940fd0a96d5d6fccc4a9b2b..0512db75e03488fee1bbdb4a0b0da791708f5685 100644 --- a/app/templates/admin/user.html.j2 +++ b/app/templates/admin/user.html.j2 @@ -96,7 +96,7 @@ </div> <div class="modal-footer"> <a class="btn modal-close waves-effect waves-light">Cancel</a> - <a href="{{ url_for('.delete_user', user_id=user.id) }}" class="btn red modal-close waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a> + <a class="btn red modal-close waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a> </div> </div> {% endblock modals %} diff --git a/app/templates/admin/user_settings.html.j2 b/app/templates/admin/user_settings.html.j2 index 49d8ba4d0f669b2ef50e743945e31345f4f147db..66d6250e5d95daaf679e036bdbe04ffe2e65c2ac 100644 --- a/app/templates/admin/user_settings.html.j2 +++ b/app/templates/admin/user_settings.html.j2 @@ -1,6 +1,74 @@ {% extends "settings/settings.html.j2" %} -{% block page_content %} +{% block admin_settings %} +<div class="col s12 l4"> + <h4>Administrator Settings</h4> + <p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, + sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. + Stet clita kasd gubergren, no sea tak</p> +</div> +<div class="col s12 l8"> + <br> + <ul class="collapsible no-autoinit settings-collapsible"> + <li> + <div class="collapsible-header" style="justify-content: space-between;"> + <span>Confirmation status</span> + <i class="caret material-icons">keyboard_arrow_right</i> + </div> + <div class="collapsible-body"> + <div class="row"> + <div class="col s12 l1"> + <p><i class="material-icons">check</i></p> + </div> + <div class="col s12 l7"> + <p> + Confirmed<br> + <span class="light">Change confirmation status manually.</span> + </p> + </div> + <div class="col s3 l4"> + <div class="switch"> + <label> + unconfirmed + <input {% if user.confirmed %}checked{% endif %} id="user-confirmed-switch" type="checkbox"> + <span class="lever"></span> + confirmed + </label> + </div> + </div> + </div> + </div> + </li> + <li> + <div class="collapsible-header" style="justify-content: space-between;"> + <span>Role</span> + <i class="caret material-icons">keyboard_arrow_right</i> + </div> + <div class="collapsible-body"> + <form method="POST"> + {{ update_user_form.hidden_tag() }} + {{ wtf.render_field(update_user_form.role, material_icon='manage_accounts') }} + <div class="right-align"> + {{ wtf.render_field(update_user_form.submit, material_icon='send') }} + </div> + </form> + </div> + </li> + </ul> +</div> +{% endblock admin_settings %} + +{% block scripts %} {{ super() }} -ADMIN ADDITIONS -{% endblock page_content %} +<script> +let userConfirmedSwitchElement = document.querySelector('#user-confirmed-switch'); +userConfirmedSwitchElement.addEventListener('change', (event) => { + let newConfirmed = userConfirmedSwitchElement.checked; + Requests.admin.users.entity.confirmed.update({{ user.hashid|tojson }}, newConfirmed) + .catch((response) => { + userConfirmedSwitchElement.checked = !userConfirmedSwitchElement; + }); +}); +</script> +{% endblock scripts %} diff --git a/app/templates/base.html.j2 b/app/templates/base.html.j2 index a924e9b76d5574b67f1e3e747992f7570ee1abf7..82132828d8562d1196ca96a0fc9413e55603a944 100644 --- a/app/templates/base.html.j2 +++ b/app/templates/base.html.j2 @@ -50,8 +50,4 @@ {% block scripts %} {{ super() }} {% include "_scripts.html.j2" %} -{% set page_script = self._TemplateReference__context.name|replace('.html.j2', '.js.j2') %} -<script> -{% include page_script ignore missing %} -</script> {% endblock scripts %} diff --git a/app/templates/settings/settings.html.j2 b/app/templates/settings/settings.html.j2 index b6ec4fa53e86e3d9d872a8a4a10c4a53da534629..004a553add1c526962247b273eba179ca4a26992 100644 --- a/app/templates/settings/settings.html.j2 +++ b/app/templates/settings/settings.html.j2 @@ -7,8 +7,7 @@ <div class="col s12"> <h1 id="title">{{ title }}</h1> </div> - </div> - <div class="row"> + <div class="col s12 l4"> <h4>Profile Settings</h4> <p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam @@ -21,7 +20,7 @@ <ul class="collapsible no-autoinit settings-collapsible"> <li> <div class="collapsible-header" style="justify-content: space-between;"> - <span>Profile Privacy Settings</span> + <span>Profile Privacy</span> <i class="material-icons caret">keyboard_arrow_right</i> </div> <div class="collapsible-body"> @@ -62,38 +61,47 @@ <span>Profile information</span> <i class="material-icons caret">keyboard_arrow_right</i> </div> + <div class="collapsible-body"> + <form method="POST"> + {{ update_profile_information_form.hidden_tag() }} + {{ wtf.render_field(update_profile_information_form.full_name, material_icon='badge') }} + {{ wtf.render_field(update_profile_information_form.about_me, material_icon='description', id='about-me-textfield') }} + {{ wtf.render_field(update_profile_information_form.website, material_icon='laptop') }} + {{ wtf.render_field(update_profile_information_form.organization, material_icon='business') }} + {{ wtf.render_field(update_profile_information_form.location, material_icon='location_on') }} + <div class="right-align"> + {{ wtf.render_field(update_profile_information_form.submit, material_icon='send') }} + </div> + </form> + </div> + </li> + <li> + <div class="collapsible-header" style="justify-content: space-between;"> + <span>Avatar</span> + <i class="material-icons caret">keyboard_arrow_right</i> + </div> <div class="collapsible-body"> <form method="POST" enctype="multipart/form-data"> - {{ edit_profile_form.hidden_tag() }} - {{ wtf.render_field(edit_profile_form.full_name, material_icon='badge') }} - {{ wtf.render_field(edit_profile_form.about_me, material_icon='description', id='about-me-textfield') }} - {{ wtf.render_field(edit_profile_form.website, material_icon='laptop') }} - {{ wtf.render_field(edit_profile_form.organization, material_icon='business') }} - {{ wtf.render_field(edit_profile_form.location, material_icon='location_on') }} - <p></p> + {{ update_avatar_form.hidden_tag() }} <div class="row"> - <div class="col s12 m2"> - <img src="{{ url_for('users.user_avatar', user_id=user.id) }}" alt="user-image" class="circle responsive-img" id="avatar"> - </div> - <div class="col s12 m6"> - {{ wtf.render_field(edit_profile_form.avatar, accept='image/jpeg, image/png, image/gif', placeholder='Choose an image file', id='avatar-upload') }} + <div class="col s12 l2"> + <img src="{{ url_for('users.user_avatar', user_id=user.id) }}" alt="Avatar" class="circle responsive-img" id="update-avatar-form-avatar-preview"> </div> - <div class="col s12 m1"> - <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 class="col s12 l10"> + <br class="hide-on-med-and-down"> + {{ wtf.render_field(update_avatar_form.avatar, accept='image/jpeg, image/png, image/gif', placeholder='Choose a JPEG or PNG file') }} </div> </div> - <br> - <p></p> <div class="right-align"> - {{ wtf.render_field(edit_profile_form.submit, material_icon='send') }} + <a class="btn red waves-effect waves-light modal-trigger" href="#delete-avatar-modal"><i class="material-icons left">delete</i>Delete</a> + {{ wtf.render_field(update_avatar_form.submit, material_icon='send') }} </div> </form> </div> </li> </ul> </div> - </div> - <div class="row"> + <div class="col s12 l4"> <h4>General Settings</h4> <p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam @@ -110,60 +118,53 @@ <i class="caret material-icons">keyboard_arrow_right</i> </div> <div class="collapsible-body"> - <form method="POST" enctype="multipart/form-data"> - {{ edit_account_form.hidden_tag() }} - {{ wtf.render_field(edit_account_form.username, material_icon='person') }} - {{ wtf.render_field(edit_account_form.email, material_icon='email') }} + <form method="POST"> + {{ update_account_information_form.hidden_tag() }} + {{ wtf.render_field(update_account_information_form.username, material_icon='person') }} + {{ wtf.render_field(update_account_information_form.email, material_icon='email') }} <div class="right-align"> - {{ wtf.render_field(edit_account_form.submit, material_icon='send') }} + <a class="btn red waves-effect waves-light modal-trigger" href="#delete-user"><i class="material-icons left">delete</i>Delete</a> + {{ wtf.render_field(update_account_information_form.submit, material_icon='send') }} </div> </form> - <br> - <div class="divider"></div> - <p>Deleting an account has the following effects:</p> - <ul> - <li>All data associated with your corpora and jobs will be permanently deleted.</li> - <li>All settings will be permanently deleted.</li> - </ul> - <div class="right-align"> - <a class="btn red waves-effect waves-light modal-trigger" href="#delete-user"><i class="material-icons left">delete</i>Delete</a> - </div> </div> </li> <li> <div class="collapsible-header" style="justify-content: space-between;"> - <span>Notifications</span> + <span>Change Password</span> <i class="caret material-icons">keyboard_arrow_right</i> - </div> + </div> <div class="collapsible-body"> <form method="POST"> - {{ edit_notifications_form.hidden_tag() }} - {{ wtf.render_field(edit_notifications_form.job_status_mail_notification_level, material_icon='notifications') }} + {{ update_password_form.hidden_tag() }} + {{ wtf.render_field(update_password_form.password, material_icon='vpn_key') }} + {{ wtf.render_field(update_password_form.new_password, material_icon='vpn_key') }} + {{ wtf.render_field(update_password_form.new_password_2, material_icon='vpn_key') }} <div class="right-align"> - {{ wtf.render_field(edit_notifications_form.submit, material_icon='send') }} + {{ wtf.render_field(update_password_form.submit, material_icon='send') }} </div> </form> </div> </li> <li> <div class="collapsible-header" style="justify-content: space-between;"> - <span>Change Password</span> + <span>Notifications</span> <i class="caret material-icons">keyboard_arrow_right</i> - </div> + </div> <div class="collapsible-body"> <form method="POST"> - {{ change_password_form.hidden_tag() }} - {{ wtf.render_field(change_password_form.password, material_icon='vpn_key') }} - {{ wtf.render_field(change_password_form.new_password, material_icon='vpn_key') }} - {{ wtf.render_field(change_password_form.new_password_2, material_icon='vpn_key') }} + {{ update_notifications_form.hidden_tag() }} + {{ wtf.render_field(update_notifications_form.job_status_mail_notification_level, material_icon='notifications') }} <div class="right-align"> - {{ wtf.render_field(change_password_form.submit, material_icon='send') }} + {{ wtf.render_field(update_notifications_form.submit, material_icon='send') }} </div> </form> </div> </li> </ul> </div> + + {% block admin_settings %}{% endblock admin_settings %} </div> </div> {% endblock page_content %} @@ -184,11 +185,16 @@ <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>{{ user.username }}</b>? All files will be permanently deleted!</p> + <p>Do you really want to delete the User <b>{{ user.username }}</b>?</p> + <p>Deleting an account has the following effects:</p> + <ul> + <li>All data (Jobs, Corpora, ...) associated with the account will be permanently deleted.</li> + <li>All settings will be permanently deleted.</li> + </ul> </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> + <a class="btn modal-close red waves-effect waves-light" id="delete-user">Delete</a> </div> </div> {% endblock modals %} @@ -196,25 +202,25 @@ {% block scripts %} {{ super() }} <script> -let deleteButtonElement = document.querySelector('#delete-avatar'); -let avatarElement = document.querySelector('#avatar'); -let avatarUploadElement = document.querySelector('#avatar-upload'); +let deleteAvatarButtonElement = document.querySelector('#delete-avatar'); +let avatarPreviewElement = document.querySelector('#update-avatar-form-avatar-preview'); +let avatarUploadElement = document.querySelector('#update-avatar-form-avatar'); avatarUploadElement.addEventListener('change', () => { let file = avatarUploadElement.files[0]; - avatarElement.src = URL.createObjectURL(file); + avatarPreviewElement.src = URL.createObjectURL(file); }); -deleteButtonElement.addEventListener('click', () => { +deleteAvatarButtonElement.addEventListener('click', () => { Requests.settings.entity.deleteAvatar({{ user.hashid|tojson }}) .then( (response) => { - avatarElement.src = {{ url_for('static', filename='images/user_avatar.png')|tojson }}; + avatarPreviewElement.src = {{ url_for('static', filename='images/user_avatar.png')|tojson }}; } ); }); -document.querySelector('#delete-user-button').addEventListener('click', (event) => { +document.querySelector('#delete-user').addEventListener('click', (event) => { Requests.settings.entity.delete({{ user.hashid|tojson }}) .then((response) => {window.location.href = '/';}); }); diff --git a/app/wtf_validators.py b/app/wtf_validators.py deleted file mode 100644 index 326ef7e709c7d13fb28327dd5daaa95febd117a9..0000000000000000000000000000000000000000 --- a/app/wtf_validators.py +++ /dev/null @@ -1,9 +0,0 @@ -from wtforms.validators import ValidationError - -def FileSizeLimit(max_size_in_mb): - max_bytes = max_size_in_mb*1024*1024 - def file_length_check(form, field): - if len(field.data.read()) > max_bytes: - raise ValidationError(f"File size must be less than {max_size_in_mb}MB") - field.data.seek(0) - return file_length_check