diff --git a/app/auth/views.py b/app/auth/views.py index 9470480f..6b87f744 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/corpora/views.py b/app/corpora/views.py index 07872c8f..6d78c8b4 100644 --- a/app/corpora/views.py +++ b/app/corpora/views.py @@ -26,13 +26,12 @@ def add_corpus(): try: os.makedirs(dir) except OSError: - flash('[ERROR]: Could not add corpus!') + flash('[ERROR]: Could not add corpus!', 'corpus') corpus.delete() else: - corpus_url = url_for('corpora.corpus', corpus_id=corpus.id) - flash('book' - '[{}] added'.format(corpus_url, - corpus.title)) + url = url_for('corpora.corpus', corpus_id=corpus.id) + flash('[{}] added'.format(url, corpus.title), + 'corpus') return redirect(url_for('corpora.corpus', corpus_id=corpus.id)) return render_template('corpora/add_corpus.html.j2', add_corpus_form=add_corpus_form, @@ -82,7 +81,7 @@ def delete_corpus(corpus_id): if not (corpus.creator == current_user or current_user.is_administrator()): abort(403) tasks.delete_corpus(corpus_id) - flash('Corpus deleted!') + flash('Corpus deleted!', 'corpus') return redirect(url_for('main.dashboard')) @@ -121,7 +120,7 @@ def add_corpus_file(corpus_id): db.session.add(corpus_file) corpus.status = 'unprepared' db.session.commit() - flash('Corpus file added!') + flash('Corpus file added!', 'corpus') return make_response( {'redirect_url': url_for('corpora.corpus', corpus_id=corpus.id)}, 201) @@ -141,7 +140,7 @@ def delete_corpus_file(corpus_id, corpus_file_id): or current_user.is_administrator()): abort(403) tasks.delete_corpus_file(corpus_file_id) - flash('Corpus file deleted!') + flash('Corpus file deleted!', 'corpus') return redirect(url_for('corpora.corpus', corpus_id=corpus_id)) @@ -188,7 +187,7 @@ def edit_corpus_file(corpus_id, corpus_file_id): corpus_file.title = edit_corpus_file_form.title.data corpus.status = 'unprepared' db.session.commit() - flash('Corpus file edited!') + flash('Corpus file edited!', 'corpus') return redirect(url_for('corpora.corpus', corpus_id=corpus_id)) # If no form is submitted or valid, fill out fields with current values edit_corpus_file_form.address.data = corpus_file.address @@ -217,7 +216,7 @@ def prepare_corpus(corpus_id): abort(403) if corpus.files.all(): tasks.build_corpus(corpus_id) - flash('Corpus gets build now.') + flash('Corpus gets build now.', 'corpus') else: - flash('Can not build corpus, please add corpus file(s).') + flash('Can not build corpus, please add corpus file(s).', 'corpus') return redirect(url_for('corpora.corpus', corpus_id=corpus_id)) diff --git a/app/email.py b/app/email.py index 88effaf9..b6dd4e4e 100644 --- a/app/email.py +++ b/app/email.py @@ -16,6 +16,7 @@ def create_message(recipient, subject, template, **kwargs): @background -def send(app, msg): +def send(msg, *args, **kwargs): + app = kwargs['app'] with app.app_context(): mail.send(msg) diff --git a/app/jobs/views.py b/app/jobs/views.py index fe5ac9b2..1708f1c7 100644 --- a/app/jobs/views.py +++ b/app/jobs/views.py @@ -23,7 +23,7 @@ def delete_job(job_id): if not (job.creator == current_user or current_user.is_administrator()): abort(403) tasks.delete_job(job_id) - flash('Job has been deleted!') + flash('Job has been deleted!', 'job') return redirect(url_for('main.dashboard')) diff --git a/app/models.py b/app/models.py index e5c29953..7866ef3c 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 60257805..14517e29 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 89d127df..f156715f 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/services/views.py b/app/services/views.py index 688254d3..3c8d0b08 100644 --- a/app/services/views.py +++ b/app/services/views.py @@ -61,7 +61,7 @@ def service(service): os.makedirs(absolut_dir) except OSError: job.delete() - flash('Internal Server Error') + flash('Internal Server Error', 'job') return make_response({'redirect_url': url_for('services.service', service=service)}, 500) @@ -74,9 +74,8 @@ def service(service): db.session.add(job_input) job.status = 'submitted' db.session.commit() - job_url = url_for('jobs.job', job_id=job.id) - flash('work' - '[{}] added'.format(job_url, job.title)) + url = url_for('jobs.job', job_id=job.id) + flash('[{}] added'.format(url, job.title), 'job') return make_response( {'redirect_url': url_for('jobs.job', job_id=job.id)}, 201) return render_template('services/{}.html.j2'.format(service), diff --git a/app/static/js/nopaque.CorpusAnalysisClient.js b/app/static/js/nopaque.CorpusAnalysisClient.js index 14df19ce..5e60f3a7 100644 --- a/app/static/js/nopaque.CorpusAnalysisClient.js +++ b/app/static/js/nopaque.CorpusAnalysisClient.js @@ -67,7 +67,7 @@ class CorpusAnalysisClient { } } else { errorText = `Error ${response.payload.code} - ${response.payload.msg}`; - nopaque.flash("error", errorText); + nopaque.flash(errorText, "error"); if (this.displays.query.errorContainer != undefined) { this.displays.query.errorContainer.innerHTML = `
`+ `error ${errorText}
`; diff --git a/app/static/js/nopaque.js b/app/static/js/nopaque.js index 2cba2c1b..2a689a75 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(`book[${nopaque.corpora[pathArray[0]].title}] 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(`work[${nopaque.jobs[pathArray[0]].title}] 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(`[${nopaque.jobs[pathArray[0]].title}] New status: ${operation.value}`, "job"); + } } } - nopaque.jobs = jsonpatch.apply_patch(nopaque.jobs, patch); for (let subscriber of nopaque.jobsSubscribers) {subscriber._update(patch);} }); @@ -188,23 +184,28 @@ nopaque.flash = function() { message = arguments[0]; break; case 2: - category = arguments[0]; - message = arguments[1]; + message = arguments[0]; + category = arguments[1]; break; default: - console.error("Usage: nopaque.flash(message) or nopaque.flash(category, message)") + console.error("Usage: nopaque.flash(message) or nopaque.flash(message, category)") } switch (category) { + case "corpus": + message = `book${message}`; + break; case "error": - classes = "red"; + message = `error${message}`; + break; + case "job": + message = `work${message}`; break; default: - classes = ""; + message = `notifications${message}`; } - toast = M.toast({classes: classes, - html: `${message} + toast = M.toast({html: `${message} `}); @@ -229,7 +230,8 @@ document.addEventListener("DOMContentLoaded", function() { nopaque.Forms.init(); nopaque.Navigation.init(); while (nopaque.flashedMessages.length) { - nopaque.flash(...nopaque.flashedMessages.shift()); + flashedMessage = nopaque.flashedMessages.shift(); + nopaque.flash(flashedMessage[1], flashedMessage[0]); } if (nopaque.user.isAuthenticated) { if (nopaque.user.settings.darkMode) { diff --git a/app/static/js/nopaque.lists.js b/app/static/js/nopaque.lists.js index d62a11d3..c569c76d 100644 --- a/app/static/js/nopaque.lists.js +++ b/app/static/js/nopaque.lists.js @@ -378,7 +378,7 @@ class ResultsList extends List { if (expertModeSwitchElement.checked) { this.expertModeOn("query-display"); // page holds new result rows, so add new tooltips } - nopaque.flash("Updated matches per page.") + nopaque.flash("Updated matches per page.", "corpus") } catch (e) { // console.log(e); // console.log("resultsList has no results right now."); @@ -394,7 +394,7 @@ class ResultsList extends List { let rc; try { if (event.type === "change") { - nopaque.flash("Updated context per match!"); + nopaque.flash("Updated context per match!", "corpus"); } } catch (e) { } finally { diff --git a/app/templates/admin/user.html.j2 b/app/templates/admin/user.html.j2 index 15de85da..f6825976 100644 --- a/app/templates/admin/user.html.j2 +++ b/app/templates/admin/user.html.j2 @@ -15,8 +15,9 @@- {{ field(*args, **kwargs) }} -
-{% endmacro %} - {% macro render_file_field(field) %} {% set placeholder = kwargs.pop('placeholder', '') %}Activate dark mode to ease your eyes.
notificationsEmail notifications
-Receive emails when a job completes.
+notificationsJob status site notifications
+Receive site notifications about job status changes.
notificationsJob status mail notifications
+Receive mail notifications about job status changes.
+