diff --git a/app/auth/views.py b/app/auth/views.py index 9470480f6ca27367331f20879a550bf26734be45..6b87f7440fb4c41360654e99b2d0aeb433e18003 100644 --- a/app/auth/views.py +++ b/app/auth/views.py @@ -17,10 +17,13 @@ def before_request(): Checks if a user is unconfirmed when visiting specific sites. Redirects to unconfirmed view if user is unconfirmed. """ - if (current_user.is_authenticated and not current_user.confirmed - and request.blueprint != 'auth' - and request.endpoint != 'static'): - return redirect(url_for('auth.unconfirmed')) + if current_user.is_authenticated: + current_user.ping() + if not current_user.confirmed \ + and request.endpoint \ + and request.blueprint != 'auth' \ + and request.endpoint != 'static': + return redirect(url_for('auth.unconfirmed')) @auth.route('/login', methods=['GET', 'POST']) diff --git a/app/models.py b/app/models.py index e5c29953232c22e8335ee0ebc5b4d4660b361b74..7866ef3c07280988b633363f13b163c857376a11 100644 --- a/app/models.py +++ b/app/models.py @@ -106,13 +106,18 @@ class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) # Fields confirmed = db.Column(db.Boolean, default=False) + last_seen = db.Column(db.DateTime(), default=datetime.utcnow) email = db.Column(db.String(254), unique=True, index=True) password_hash = db.Column(db.String(128)) - registration_date = db.Column(db.DateTime(), default=datetime.utcnow) + member_since = db.Column(db.DateTime(), default=datetime.utcnow) role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) username = db.Column(db.String(64), unique=True, index=True) # Setting Fields setting_dark_mode = db.Column(db.Boolean, default=False) + setting_job_status_mail_notifications = db.Column(db.String(16), + default='end') + setting_job_status_site_notifications = db.Column(db.String(16), + default='all') # Relationships corpora = db.relationship('Corpus', backref='creator', lazy='dynamic', cascade='save-update, merge, delete') @@ -205,6 +210,10 @@ class User(UserMixin, db.Model): """ return self.can(Permission.ADMIN) + def ping(self): + self.last_seen = datetime.utcnow() + db.session.add(self) + def delete(self): """ Delete the user and its corpora and jobs from database and filesystem. diff --git a/app/profile/forms.py b/app/profile/forms.py index 60257805ed908eccb813ed9328f3153a32f65dce..14517e293ed0d48ba89e1d75fe43eb1b9ac4d6de 100644 --- a/app/profile/forms.py +++ b/app/profile/forms.py @@ -1,6 +1,6 @@ from flask_wtf import FlaskForm -from wtforms import (BooleanField, PasswordField, StringField, SubmitField, - ValidationError) +from wtforms import (BooleanField, PasswordField, SelectField, StringField, + SubmitField, ValidationError) from wtforms.validators import DataRequired, Email, EqualTo @@ -11,6 +11,20 @@ class EditEmailForm(FlaskForm): 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') diff --git a/app/profile/views.py b/app/profile/views.py index 89d127dfed9fed9cfc0d790ee0aaa5122f55d59d..f156715fbf824382c2d36a52a78482d12444f96c 100644 --- a/app/profile/views.py +++ b/app/profile/views.py @@ -11,8 +11,7 @@ from .. import db def settings(): edit_email_form = EditEmailForm(prefix='edit-email-form') edit_general_settings_form = EditGeneralSettingsForm( - prefix='edit-general-settings-form' - ) + 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 @@ -25,7 +24,12 @@ def 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_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.') @@ -41,6 +45,10 @@ def 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, diff --git a/app/static/js/nopaque.js b/app/static/js/nopaque.js index 2cba2c1b3e3f4a16ca14f4c3da91acaabd4541a6..d2c6ddbfc3d1bc561bb6b24b269ed42eb757d3fc 100644 --- a/app/static/js/nopaque.js +++ b/app/static/js/nopaque.js @@ -43,13 +43,6 @@ nopaque.socket.init = function() { var patch; patch = JSON.parse(msg); - for (operation of patch) { - /* "/corpusId/valueName" -> ["corpusId", "valueName"] */ - pathArray = operation.path.split("/").slice(1); - if (operation.op === "replace" && pathArray[1] === "status") { - nopaque.flash(`<i class="left material-icons">book</i>[<a href="/jobs/${pathArray[0]}">${nopaque.corpora[pathArray[0]].title}</a>] New status: ${operation.value}`); - } - } nopaque.corpora = jsonpatch.apply_patch(nopaque.corpora, patch); for (let subscriber of nopaque.corporaSubscribers) {subscriber._update(patch);} }); @@ -58,14 +51,17 @@ nopaque.socket.init = function() { var patch; patch = JSON.parse(msg); - for (operation of patch) { - /* "/jobId/valueName" -> ["jobId", "valueName"] */ - pathArray = operation.path.split("/").slice(1); - if (operation.op === "replace" && pathArray[1] === "status") { - nopaque.flash(`<i class="left material-icons">work</i>[<a href="/jobs/${pathArray[0]}">${nopaque.jobs[pathArray[0]].title}</a>] New status: ${operation.value}`); + nopaque.jobs = jsonpatch.apply_patch(nopaque.jobs, patch); + if (["all", "end"].includes(nopaque.user.settings.jobStatusSiteNotifications)) { + for (operation of patch) { + /* "/jobId/valueName" -> ["jobId", "valueName"] */ + pathArray = operation.path.split("/").slice(1); + if (operation.op === "replace" && pathArray[1] === "status") { + if (nopaque.user.settings.jobStatusSiteNotifications === "end" && !["complete", "failed"].includes(operation.value)) {continue;} + nopaque.flash(`<i class="left material-icons">work</i>[<a href="/jobs/${pathArray[0]}">${nopaque.jobs[pathArray[0]].title}</a>] New status: ${operation.value}`); + } } } - nopaque.jobs = jsonpatch.apply_patch(nopaque.jobs, patch); for (let subscriber of nopaque.jobsSubscribers) {subscriber._update(patch);} }); diff --git a/app/templates/admin/user.html.j2 b/app/templates/admin/user.html.j2 index 15de85da11430ff16121d5d3dd2af57584c981ee..f68259763ad128eed883a413997e1da5936a2d34 100644 --- a/app/templates/admin/user.html.j2 +++ b/app/templates/admin/user.html.j2 @@ -15,8 +15,9 @@ <li>Username: {{ user.username }}</li> <li>Email: {{ user.email }}</li> <li>ID: {{ user.id }}</li> - <li>Registration date: {{ user.registration_date.strftime('%m/%d/%Y, %H:%M:%S %p') }}</li> + <li>Member sinse: {{ user.member_since.strftime('%m/%d/%Y, %H:%M:%S %p') }}</li> <li>Confirmed status: {{ user.confirmed }}</li> + <li>Last seen: {{ user.last_seen.strftime('%m/%d/%Y, %H:%M:%S %p') }}</li> <li>Role ID: {{ user.role_id }}</li> <li>Permissions as Int: {{ user.role.permissions }}</li> <li>Role name: {{ user.role.name }}</li> diff --git a/app/templates/jobs/job.html.j2 b/app/templates/jobs/job.html.j2 index 5b45d19a2b4e212f133792fef3c0c2c672fe0a11..5be1ed992e2e9865ba8bfc386d075a9eacd4f7b3 100644 --- a/app/templates/jobs/job.html.j2 +++ b/app/templates/jobs/job.html.j2 @@ -175,7 +175,6 @@ for (let operation of patch) { /* "/jobId/valueName" -> ["jobId", "valueName"] */ - console.log(operation.value); pathArray = operation.path.split("/").slice(1); if (pathArray[0] != this.jobId) {continue;} switch(operation.op) { diff --git a/app/templates/macros/materialize.html.j2 b/app/templates/macros/materialize.html.j2 index 40785de613658fc2fc7eee2a8fed71dfa05b7614..bf14c6e738c3ca4913a388e034996a5bca713e3c 100644 --- a/app/templates/macros/materialize.html.j2 +++ b/app/templates/macros/materialize.html.j2 @@ -29,12 +29,15 @@ {% endmacro %} {% macro render_boolean_field(field) %} + {% set label = kwargs.pop('label', True) %} <div class="switch"> {% if 'material_icon' in kwargs %} <i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i> {% endif %} <label> + {% if label %} {{ field.label.text }} + {% endif %} {{ field(*args, **kwargs) }} <span class="lever"></span> </label> @@ -44,12 +47,6 @@ </div> {% endmacro %} -{% macro render_decimal_range_field(field) %} - <p class="range-field"> - {{ field(*args, **kwargs) }} - </p> -{% endmacro %} - {% macro render_file_field(field) %} {% set placeholder = kwargs.pop('placeholder', '') %} <div class="file-field input-field"> @@ -64,12 +61,15 @@ {% endmacro %} {% macro render_generic_field(field) %} + {% set label = kwargs.pop('label', True) %} <div class="input-field"> {% if 'material_icon' in kwargs %} <i class="material-icons prefix">{{ kwargs.pop('material_icon') }}</i> {% endif %} {{ field(*args, **kwargs) }} + {% if label %} {{ field.label }} + {% endif %} {% for error in field.errors %} <span class="helper-text red-text">{{ error }}</span> {% endfor %} diff --git a/app/templates/nopaque.html.j2 b/app/templates/nopaque.html.j2 index 8c305ad66349ea5d126f4ab75c208e5b830d83f0..f608ac188d672febe83d33f5d737d98ca3a91883 100644 --- a/app/templates/nopaque.html.j2 +++ b/app/templates/nopaque.html.j2 @@ -49,9 +49,15 @@ <script src="{{ url_for('static', filename='js/nopaque.js') }}"></script> <script src="{{ url_for('static', filename='js/nopaque.lists.js') }}"></script> <script> - nopaque.user.isAuthenticated = {{ current_user.is_authenticated|tojson }}; - nopaque.user.settings.darkMode = {{ (current_user.is_authenticated and current_user.setting_dark_mode)|tojson }}; - nopaque.flashedMessages = {{ get_flashed_messages(with_categories=true)|tojson }}; + {% if current_user.is_authenticated %} + nopaque.user.isAuthenticated = true; + nopaque.user.settings.darkMode = {{ current_user.setting_dark_mode|tojson }}; + nopaque.user.settings.jobStatusMailNotifications = {{ current_user.setting_job_status_mail_notifications|tojson }}; + nopaque.user.settings.jobStatusSiteNotifications = {{ current_user.setting_job_status_site_notifications|tojson }}; + {% else %} + nopaque.user.isAuthenticated = false; + {% endif %} + nopaque.flashedMessages = {{ get_flashed_messages(with_categories=True)|tojson }}; </script> </head> <body> diff --git a/app/templates/profile/settings.html.j2 b/app/templates/profile/settings.html.j2 index 3ab20a0d51b4214a61f33f9234bfff5bdfda80e1..b5897ebba2082da545141f88a9dff95e0cafb58f 100644 --- a/app/templates/profile/settings.html.j2 +++ b/app/templates/profile/settings.html.j2 @@ -27,16 +27,21 @@ <div class="col s12 divider"></div> <div class="col s12"><p> </p></div> <div class="col s9"> - <p><i class="material-icons left">notifications</i>Email notifications</p> - <p class="light">Receive emails when a job completes.</p> + <p><i class="material-icons left">notifications</i>Job status site notifications</p> + <p class="light">Receive site notifications about job status changes.</p> </div> <div class="col s3 right-align"> - <div class="switch"> - <label> - <input disabled type="checkbox"> - <span class="lever"></span> - </label> - </div> + {{ M.render_field(edit_general_settings_form.job_status_site_notifications, label=False) }} + </div> + <div class="col s12"><p> </p></div> + <div class="col s12 divider"></div> + <div class="col s12"><p> </p></div> + <div class="col s9"> + <p><i class="material-icons left">notifications</i>Job status mail notifications</p> + <p class="light">Receive mail notifications about job status changes.</p> + </div> + <div class="col s3 right-align"> + {{ M.render_field(edit_general_settings_form.job_status_mail_notifications, label=False) }} </div> <!-- Seperate each setting with the following two elements diff --git a/migrations/versions/099037c4aa06_.py b/migrations/versions/099037c4aa06_.py new file mode 100644 index 0000000000000000000000000000000000000000..d9adf506911c4a5de16821610da734b634621cf1 --- /dev/null +++ b/migrations/versions/099037c4aa06_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 099037c4aa06 +Revises: 66253783654f +Create Date: 2020-04-27 09:17:15.039728 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '099037c4aa06' +down_revision = '66253783654f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('last_seen', sa.DateTime(), nullable=True)) + op.add_column('users', sa.Column('setting_site_job_status_notifications', sa.String(length=16), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'setting_site_job_status_notifications') + op.drop_column('users', 'last_seen') + # ### end Alembic commands ### diff --git a/migrations/versions/49a42c69e523_.py b/migrations/versions/49a42c69e523_.py new file mode 100644 index 0000000000000000000000000000000000000000..36281de5c9784ec7ce32eafe37c0b1e80d073ec8 --- /dev/null +++ b/migrations/versions/49a42c69e523_.py @@ -0,0 +1,36 @@ +"""empty message + +Revision ID: 49a42c69e523 +Revises: 099037c4aa06 +Create Date: 2020-04-27 11:18:32.999099 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '49a42c69e523' +down_revision = '099037c4aa06' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('member_since', sa.DateTime(), nullable=True)) + op.add_column('users', sa.Column('setting_job_status_mail_notifications', sa.String(length=16), nullable=True)) + op.add_column('users', sa.Column('setting_job_status_site_notifications', sa.String(length=16), nullable=True)) + op.drop_column('users', 'setting_site_job_status_notifications') + op.drop_column('users', 'registration_date') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('registration_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('setting_site_job_status_notifications', sa.VARCHAR(length=16), autoincrement=False, nullable=True)) + op.drop_column('users', 'setting_job_status_site_notifications') + op.drop_column('users', 'setting_job_status_mail_notifications') + op.drop_column('users', 'member_since') + # ### end Alembic commands ###