diff --git a/web/app/__init__.py b/web/app/__init__.py index 7a5d8b68..a39a51a5 100644 --- a/web/app/__init__.py +++ b/web/app/__init__.py @@ -28,22 +28,23 @@ def create_app(config_name): socketio.init_app( app, message_queue=config[config_name].SOCKETIO_MESSAGE_QUEUE_URI) - from . import events - from .admin import admin as admin_blueprint + with app.app_context(): + from . import events + from .admin import admin as admin_blueprint + from .auth import auth as auth_blueprint + from .corpora import corpora as corpora_blueprint + from .errors import errors as errors_blueprint + from .jobs import jobs as jobs_blueprint + from .main import main as main_blueprint + from .services import services as services_blueprint + from .settings import settings as settings_blueprint app.register_blueprint(admin_blueprint, url_prefix='/admin') - from .auth import auth as auth_blueprint app.register_blueprint(auth_blueprint, url_prefix='/auth') - from .corpora import corpora as corpora_blueprint app.register_blueprint(corpora_blueprint, url_prefix='/corpora') - from .errors import errors as errors_blueprint app.register_blueprint(errors_blueprint) - from .jobs import jobs as jobs_blueprint app.register_blueprint(jobs_blueprint, url_prefix='/jobs') - from .main import main as main_blueprint app.register_blueprint(main_blueprint) - from .profile import profile as profile_blueprint - app.register_blueprint(profile_blueprint, url_prefix='/profile') - from .services import services as services_blueprint app.register_blueprint(services_blueprint, url_prefix='/services') + app.register_blueprint(settings_blueprint, url_prefix='/settings') return app diff --git a/web/app/admin/views.py b/web/app/admin/views.py index e8d0d916..3046a63c 100644 --- a/web/app/admin/views.py +++ b/web/app/admin/views.py @@ -8,10 +8,10 @@ from ..models import Role, User from ..profile import tasks as profile_tasks -@admin.route('/') +@admin.route('/users') @login_required @admin_required -def index(): +def users(): users = User.query.all() users = [dict(username=u.username, email=u.email, @@ -19,21 +19,18 @@ def index(): confirmed=u.confirmed, id=u.id) for u in users] - return render_template('admin/index.html.j2', - title='Administration tools', - users=users) + return render_template('admin/users.html.j2', title='Users', users=users) -@admin.route('/user/') +@admin.route('/users/') @login_required @admin_required def user(user_id): user = User.query.get_or_404(user_id) - return render_template('admin/user.html.j2', title='Administration: User', - user=user) + return render_template('admin/user.html.j2', title='Edit user', user=user) -@admin.route('/user//delete') +@admin.route('/users//delete') @login_required @admin_required def delete_user(user_id): @@ -42,7 +39,7 @@ def delete_user(user_id): return redirect(url_for('admin.index')) -@admin.route('/user//edit', methods=['GET', 'POST']) +@admin.route('/users//edit', methods=['GET', 'POST']) @login_required @admin_required def edit_user(user_id): @@ -63,4 +60,5 @@ def edit_user(user_id): edit_user_form.role.data = user.role_id return render_template('admin/edit_user.html.j2', edit_user_form=edit_user_form, - title='Administration: Edit user', user=user) + title='Edit user', + user=user) diff --git a/web/app/auth/forms.py b/web/app/auth/forms.py index e3f5c276..3344096b 100644 --- a/web/app/auth/forms.py +++ b/web/app/auth/forms.py @@ -1,3 +1,4 @@ +from flask import current_app from ..models import User from flask_wtf import FlaskForm from wtforms import (BooleanField, PasswordField, StringField, SubmitField, @@ -17,9 +18,9 @@ class RegistrationForm(FlaskForm): username = StringField( 'Username', validators=[DataRequired(), Length(1, 64), - Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, - 'Usernames must have only letters, numbers, dots ' - 'or underscores')] + Regexp(current_app.config['ALLOWED_USERNAME_REGEX'], + message='Usernames must have only letters, numbers,' + ' dots or underscores')] ) password = PasswordField( 'Password', diff --git a/web/app/profile/forms.py b/web/app/profile/forms.py deleted file mode 100644 index 14517e29..00000000 --- a/web/app/profile/forms.py +++ /dev/null @@ -1,52 +0,0 @@ -from flask_wtf import FlaskForm -from wtforms import (BooleanField, PasswordField, SelectField, StringField, - SubmitField, ValidationError) -from wtforms.validators import DataRequired, Email, EqualTo - - -class EditEmailForm(FlaskForm): - email = StringField('New email', validators=[Email(), DataRequired()]) - save_email = SubmitField('Save email') - - -class EditGeneralSettingsForm(FlaskForm): - dark_mode = BooleanField('Dark mode') - job_status_mail_notifications = SelectField( - 'Job status mail notifications', - choices=[('', 'Choose your option'), - ('all', 'Notify on all status changes'), - ('end', 'Notify only when a job ended'), - ('none', 'No status update notifications')], - validators=[DataRequired()]) - job_status_site_notifications = SelectField( - 'Job status site notifications', - choices=[('', 'Choose your option'), - ('all', 'Notify on all status changes'), - ('end', 'Notify only when a job ended'), - ('none', 'No status update notifications')], - validators=[DataRequired()]) - save_settings = SubmitField('Save settings') - - -class EditPasswordForm(FlaskForm): - current_password = PasswordField('Current password', - validators=[DataRequired()]) - password = PasswordField( - 'New password', - validators=[DataRequired(), EqualTo('password_confirmation', - message='Passwords must match.')] - ) - password_confirmation = PasswordField( - 'Password confirmation', - validators=[DataRequired(), - EqualTo('password', message='Passwords must match.')] - ) - save_password = SubmitField('Save password') - - def __init__(self, user, *args, **kwargs): - super(EditPasswordForm, self).__init__(*args, **kwargs) - self.user = user - - def validate_current_password(self, field): - if not self.user.verify_password(field.data): - raise ValidationError('Invalid password.') diff --git a/web/app/profile/tasks.py b/web/app/profile/tasks.py deleted file mode 100644 index 61f737c5..00000000 --- a/web/app/profile/tasks.py +++ /dev/null @@ -1,13 +0,0 @@ -from .. import db -from ..decorators import background -from ..models import User - - -@background -def delete_user(user_id, *args, **kwargs): - with kwargs['app'].app_context(): - user = User.query.get(user_id) - if user is None: - raise Exception('User {} not found'.format(user_id)) - user.delete() - db.session.commit() diff --git a/web/app/profile/views.py b/web/app/profile/views.py deleted file mode 100644 index f156715f..00000000 --- a/web/app/profile/views.py +++ /dev/null @@ -1,69 +0,0 @@ -from flask import flash, redirect, render_template, url_for -from flask_login import current_user, login_required, logout_user -from . import profile -from . import tasks -from .forms import EditEmailForm, EditGeneralSettingsForm, EditPasswordForm -from .. import db - - -@profile.route('/settings', methods=['GET', 'POST']) -@login_required -def settings(): - edit_email_form = EditEmailForm(prefix='edit-email-form') - edit_general_settings_form = EditGeneralSettingsForm( - prefix='edit-general-settings-form') - edit_password_form = EditPasswordForm(prefix='edit-password-form', - user=current_user) - # Check if edit_email_form is submitted and valid - if (edit_email_form.save_email.data - and edit_email_form.validate_on_submit()): - db.session.add(current_user) - db.session.commit() - flash('Your email address has been updated.') - return redirect(url_for('profile.settings')) - # Check if edit_settings_form is submitted and valid - if (edit_general_settings_form.save_settings.data - and edit_general_settings_form.validate_on_submit()): - current_user.setting_dark_mode = \ - edit_general_settings_form.dark_mode.data - current_user.setting_job_status_mail_notifications = \ - edit_general_settings_form.job_status_mail_notifications.data - current_user.setting_job_status_site_notifications = \ - edit_general_settings_form.job_status_site_notifications.data - db.session.add(current_user) - db.session.commit() - flash('Your settings have been updated.') - return redirect(url_for('profile.settings')) - # Check if edit_password_form is submitted and valid - if (edit_password_form.save_password.data - and edit_password_form.validate_on_submit()): - current_user.password = edit_password_form.password.data - db.session.add(current_user) - db.session.commit() - flash('Your password has been updated.') - return redirect(url_for('profile.settings')) - # If no form is submitted or valid, fill out fields with current values - edit_email_form.email.data = current_user.email - edit_general_settings_form.dark_mode.data = current_user.setting_dark_mode - edit_general_settings_form.job_status_site_notifications.data = \ - current_user.setting_job_status_site_notifications - edit_general_settings_form.job_status_mail_notifications.data = \ - current_user.setting_job_status_mail_notifications - return render_template( - 'profile/settings.html.j2', - edit_email_form=edit_email_form, - edit_password_form=edit_password_form, - edit_general_settings_form=edit_general_settings_form, - title='Settings') - - -@profile.route('/delete', methods=['GET', 'POST']) -@login_required -def delete(): - """ - View to delete yourslef and all associated data. - """ - tasks.delete_user(current_user.id) - logout_user() - flash('Your account has been deleted!') - return redirect(url_for('main.index')) diff --git a/web/app/profile/__init__.py b/web/app/settings/__init__.py similarity index 57% rename from web/app/profile/__init__.py rename to web/app/settings/__init__.py index 85b9f61c..2329d880 100644 --- a/web/app/profile/__init__.py +++ b/web/app/settings/__init__.py @@ -1,5 +1,5 @@ from flask import Blueprint -profile = Blueprint('profile', __name__) +settings = Blueprint('settings', __name__) from . import views # noqa diff --git a/web/app/settings/forms.py b/web/app/settings/forms.py new file mode 100644 index 00000000..8cf96415 --- /dev/null +++ b/web/app/settings/forms.py @@ -0,0 +1,86 @@ +from flask import current_app +from flask_login import current_user +from flask_wtf import FlaskForm +from wtforms import (BooleanField, PasswordField, SelectField, StringField, + SubmitField, ValidationError) +from wtforms.validators import DataRequired, Email, EqualTo, Length, Regexp + + +class ChangePasswordForm(FlaskForm): + password = PasswordField('Old password', validators=[DataRequired()]) + new_password = PasswordField( + 'New password', + validators=[DataRequired(), EqualTo('password_confirmation', + message='Passwords must match.')] + ) + new_password2 = PasswordField( + 'Confirm new password', validators=[DataRequired()]) + submit = SubmitField('Change password') + + def __init__(self, user=current_user, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + + def validate_current_password(self, field): + if not self.user.verify_password(field.data): + raise ValidationError('Invalid password.') + + +class EditGeneralSettingsForm(FlaskForm): + dark_mode = BooleanField('Dark mode') + email = StringField('E-Mail', + validators=[DataRequired(), Length(1, 254), Email()]) + username = StringField( + 'Benutzername', + validators=[DataRequired(), + Length(1, 64), + Regexp(current_app.config['ALLOWED_USERNAME_REGEX'], + message='Usernames must have only letters, numbers,' + ' dots or underscores')] + ) + submit = SubmitField('Submit') + + def __init__(self, user=current_user, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + self.email.data = self.email.data or user.email + self.dark_mode.data = self.dark_mode.data or user.setting_dark_mode + self.username.data = self.username.data or user.username + + def validate_email(self, field): + if (field.data != self.user.email + and User.query.filter_by(email=field.data).first()): + raise ValidationError('Email already registered.') + + def validate_username(self, field): + if (field.data != self.user.username + and User.query.filter_by(username=field.data).first()): + raise ValidationError('Username already in use.') + + +class EditNotificationSettingsForm(FlaskForm): + job_status_mail_notifications = SelectField( + 'Job status mail notifications', + choices=[('', 'Choose your option'), + ('all', 'Notify on all status changes'), + ('end', 'Notify only when a job ended'), + ('none', 'No status update notifications')], + validators=[DataRequired()]) + job_status_site_notifications = SelectField( + 'Job status site notifications', + choices=[('', 'Choose your option'), + ('all', 'Notify on all status changes'), + ('end', 'Notify only when a job ended'), + ('none', 'No status update notifications')], + validators=[DataRequired()]) + submit = SubmitField('Save settings') + + def __init__(self, user=current_user, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + self.job_status_mail_notifications.data = \ + self.job_status_mail_notifications.data \ + or user.setting_job_status_mail_notifications + self.job_status_site_notifications.data = \ + self.job_status_site_notifications.data \ + or user.setting_job_status_site_notifications diff --git a/web/app/settings/views.py b/web/app/settings/views.py new file mode 100644 index 00000000..a90d8ab2 --- /dev/null +++ b/web/app/settings/views.py @@ -0,0 +1,73 @@ +from flask import current_app, flash, redirect, render_template, url_for +from flask_login import current_user, login_required +from . import settings +from .forms import (ChangePasswordForm, EditGeneralSettingsForm, + EditNotificationSettingsForm) +from .. import db +from ..decorators import admin_required +from ..models import Role, User +import os +import uuid + + +@settings.route('/') +@login_required +def index(): + return redirect(url_for('.edit_general_settings')) + + +@settings.route('/change_password', methods=['GET', 'POST']) +@login_required +def change_password(): + form = ChangePasswordForm() + if form.validate_on_submit(): + current_user.password = form.new_password.data + db.session.commit() + flash('Your password has been updated.') + return redirect(url_for('.change_password')) + return render_template('settings/change_password.html.j2', + form=form, + title='Change password') + + +@settings.route('/edit_general_settings') +@login_required +def edit_general_settings(): + form = EditGeneralSettingsForm() + if form.validate_on_submit(): + current_user.email = form.email.data + current_user.setting_dark_mode = form.dark_mode.data + current_user.username = form.username.data + db.session.commit() + flash('Your changes have been saved.') + return render_template('settings/edit_general_settings.html.j2', + form=form, + title='General settings') + + +@settings.route('/edit_notification_settings') +@login_required +def edit_notification_settings(): + form = EditNotificationSettingsForm() + if form.validate_on_submit(): + current_user.setting_job_status_mail_notifications = \ + form.job_status_mail_notifications.data + current_user.setting_job_status_site_notifications = \ + form.job_status_site_notifications.data + db.session.commit() + flash('Your changes have been saved.') + return render_template('settings/edit_notification_settings.html.j2', + form=form, + title='Notification settings') + + +@settings.route('/delete') +@login_required +def delete(): + """ + View to delete current_user and all associated data. + """ + tasks.delete_user(current_user.id) + logout_user() + flash('Your account has been deleted!') + return redirect(url_for('main.index')) diff --git a/web/app/static/js/nopaque.lists.js b/web/app/static/js/nopaque.lists.js index d91186dc..603f81b1 100644 --- a/web/app/static/js/nopaque.lists.js +++ b/web/app/static/js/nopaque.lists.js @@ -131,11 +131,11 @@ RessourceList.dataMappers = { confirmed: user.confirmed, email: user.email, id: user.id, - link: `user/${user.id}`, + link: `users/${user.id}`, role_id: user.role_id, username: user.username, username2: user.username, - "delete-link": `/admin/user/${user.id}/delete`, + "delete-link": `/admin/users/${user.id}/delete`, "delete-modal": `delete-user-${user.id}-modal`, "delete-modal-trigger": `delete-user-${user.id}-modal`, }), diff --git a/web/app/templates/admin/edit_user.html.j2 b/web/app/templates/admin/edit_user.html.j2 index 3a48adf8..6e47db5f 100644 --- a/web/app/templates/admin/edit_user.html.j2 +++ b/web/app/templates/admin/edit_user.html.j2 @@ -1,27 +1,35 @@ {% extends "nopaque.html.j2" %} +{% import 'materialize/wtf.html.j2' as wtf %} {% block page_content %} -
-

{{ user.username }}

-

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,

- arrow_backBack to user administration -
+
+
+
+

Edit user

+
-
-
-
-
- {{ edit_user_form.hidden_tag() }} - {{ M.render_field(edit_user_form.username, data_length='64', material_icon='account_circle') }} - {{ M.render_field(edit_user_form.email, class_='validate', material_icon='email', type='email') }} - {{ M.render_field(edit_user_form.role, material_icon='swap_vert') }} - {{ M.render_field(edit_user_form.confirmed, material_icon='check') }} +
+

{{ user.username }}

+

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,

+ arrow_backBack to user administration +
+ +
+
+ +
+ {{ edit_user_form.hidden_tag() }} + {{ wtf.render_field(edit_user_form.username, data_length='64', material_icon='account_circle') }} + {{ wtf.render_field(edit_user_form.email, class_='validate', material_icon='email', type='email') }} + {{ wtf.render_field(edit_user_form.role, material_icon='swap_vert') }} + {{ wtf.render_field(edit_user_form.confirmed, material_icon='check') }} +
+
+ {{ wtf.render_field(edit_user_form.submit, material_icon='send') }} +
+
-
- {{ M.render_field(edit_user_form.submit, material_icon='send') }} -
- +
- {% endblock %} diff --git a/web/app/templates/admin/index.html.j2 b/web/app/templates/admin/index.html.j2 deleted file mode 100644 index 9fca5b58..00000000 --- a/web/app/templates/admin/index.html.j2 +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "nopaque.html.j2" %} - -{% set full_width = True %} - -{% block page_content %} -
-
-
- User list -
- search - - -
-
    - - - - - - - - - - - - - -
    UsernameEmailRoleConfirmed StatusId{# Actions #}
    -
      -
      -
      -
      - - -{% endblock %} diff --git a/web/app/templates/admin/user.html.j2 b/web/app/templates/admin/user.html.j2 index c1e53a18..81fc243a 100644 --- a/web/app/templates/admin/user.html.j2 +++ b/web/app/templates/admin/user.html.j2 @@ -1,34 +1,40 @@ {% extends "nopaque.html.j2" %} {% block page_content %} -
      -

      {{ user.username }}

      -

      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,

      - arrow_backBack to admin board -
      +
      +
      +
      +

      {{ title }}

      +
      -
      -
      -
      - User information -
        -
      • Username: {{ user.username }}
      • -
      • Email: {{ user.email }}
      • -
      • ID: {{ user.id }}
      • -
      • Member since: {{ user.member_since.strftime('%m/%d/%Y, %H:%M:%S %p') }}
      • -
      • Confirmed status: {{ user.confirmed }}
      • -
      • Last seen: {{ user.last_seen.strftime('%m/%d/%Y, %H:%M:%S %p') }}
      • -
      • Role ID: {{ user.role_id }}
      • -
      • Permissions as Int: {{ user.role.permissions }}
      • -
      • Role name: {{ user.role.name }}
      • -
      +
      +

      {{ user.username }}

      +

      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,

      + arrow_backBack to Users
      -
      - editEdit - deleteDelete + +
      +
      +
      + User information +
        +
      • Username: {{ user.username }}
      • +
      • Email: {{ user.email }}
      • +
      • ID: {{ user.id }}
      • +
      • Member since: {{ user.member_since.strftime('%m/%d/%Y, %H:%M:%S %p') }}
      • +
      • Confirmed status: {{ user.confirmed }}
      • +
      • Last seen: {{ user.last_seen.strftime('%m/%d/%Y, %H:%M:%S %p') }}
      • +
      • Role ID: {{ user.role_id }}
      • +
      • Permissions as Int: {{ user.role.permissions }}
      • +
      • Role name: {{ user.role.name }}
      • +
      +
      + +
      -
      -

      Corpora

      @@ -92,21 +98,23 @@ - - {% endblock %} + + +{% block scripts %} +{{ super() }} + +{% endblock scripts %} diff --git a/web/app/templates/admin/users.html.j2 b/web/app/templates/admin/users.html.j2 new file mode 100644 index 00000000..539ce0f1 --- /dev/null +++ b/web/app/templates/admin/users.html.j2 @@ -0,0 +1,48 @@ +{% extends "nopaque.html.j2" %} + +{% block page_content %} +
      +
      +
      +

      {{ title }}

      +
      + +
      +
      +
      +
      + search + + +
      +
        + + + + + + + + + + + + + +
        UsernameEmailRoleConfirmed StatusId{# Actions #}
        +
          +
          +
          +
          +
          +
          +{% endblock %} + +{% block scripts %} +{{ super() }} + +{% endblock scripts %} diff --git a/web/app/templates/auth/login.html.j2 b/web/app/templates/auth/login.html.j2 index a477da4f..657da25f 100644 --- a/web/app/templates/auth/login.html.j2 +++ b/web/app/templates/auth/login.html.j2 @@ -18,7 +18,7 @@
          -

          {{ title }}

          +

          {{ title }}

          Want to boost your research and get going? nopaque is free and no download is needed. Register now!

          diff --git a/web/app/templates/auth/register.html.j2 b/web/app/templates/auth/register.html.j2 index db4ef2c0..9909cd60 100644 --- a/web/app/templates/auth/register.html.j2 +++ b/web/app/templates/auth/register.html.j2 @@ -18,7 +18,7 @@
          -

          Register

          +

          {{ title }}

          Simply enter a username and password to receive your registration email. After that you can start right away.

          It goes without saying that the General Data Protection Regulation applies, only necessary data is stored.

          Please also read our terms of use before signing up for nopaque!

          diff --git a/web/app/templates/auth/reset_password.html.j2 b/web/app/templates/auth/reset_password.html.j2 index 414a2d10..f3e4f26e 100644 --- a/web/app/templates/auth/reset_password.html.j2 +++ b/web/app/templates/auth/reset_password.html.j2 @@ -5,11 +5,11 @@
          -

          {{ title }}

          +

          {{ title }}

          -

          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,

          +

          Enter a new password and confirm it! After that, the entered password is your new one!

          diff --git a/web/app/templates/auth/reset_password_request.html.j2 b/web/app/templates/auth/reset_password_request.html.j2 index 549af6cd..403f1388 100644 --- a/web/app/templates/auth/reset_password_request.html.j2 +++ b/web/app/templates/auth/reset_password_request.html.j2 @@ -5,7 +5,7 @@
          -

          {{ title }}

          +

          {{ title }}

          diff --git a/web/app/templates/auth/unconfirmed.html.j2 b/web/app/templates/auth/unconfirmed.html.j2 index 98ce4de4..613c51a4 100644 --- a/web/app/templates/auth/unconfirmed.html.j2 +++ b/web/app/templates/auth/unconfirmed.html.j2 @@ -4,7 +4,7 @@
          -

          {{ title }}

          +

          {{ title }}

          @@ -13,10 +13,10 @@ Hello, {{ current_user.username }}!

          You have not confirmed your account yet.

          Before you can access this site you need to confirm your account. Check your inbox, you should have received an email with a confirmation link.

          -

          Need another confirmation email? Click here

          +

          Need another confirmation email? Click the button below!

          diff --git a/web/app/templates/errors/403.html.j2 b/web/app/templates/errors/403.html.j2 index 7a4eca4e..0ebbea86 100644 --- a/web/app/templates/errors/403.html.j2 +++ b/web/app/templates/errors/403.html.j2 @@ -2,14 +2,14 @@ {% block page_content %}
          -

          {{ title }}

          +

          {{ title }}

          {{ request.path }}

          Alternatively, you can visit the Main Page or read more information about this type of error.