Merge branch 'development' of gitlab.ub.uni-bielefeld.de:sfb1288inf/opaque into development

This commit is contained in:
Stephan Porada 2020-04-27 16:16:59 +02:00
commit eb5e5c3253
18 changed files with 185 additions and 78 deletions

View File

@ -17,9 +17,12 @@ 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'):
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'))

View File

@ -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('<i class="left material-icons">book</i>'
'[<a href="{}">{}</a>] added'.format(corpus_url,
corpus.title))
url = url_for('corpora.corpus', corpus_id=corpus.id)
flash('[<a href="{}">{}</a>] 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))

View File

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

View File

@ -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'))

View File

@ -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.

View File

@ -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')

View File

@ -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,

View File

@ -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('<i class="left material-icons">work</i>'
'[<a href="{}">{}</a>] added'.format(job_url, job.title))
url = url_for('jobs.job', job_id=job.id)
flash('[<a href="{}">{}</a>] 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),

View File

@ -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 = `<p class="red-text">`+
`<i class="material-icons tiny">error</i> ${errorText}</p>`;

View File

@ -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);
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") {
nopaque.flash(`<i class="left material-icons">work</i>[<a href="/jobs/${pathArray[0]}">${nopaque.jobs[pathArray[0]].title}</a>] New status: ${operation.value}`);
if (nopaque.user.settings.jobStatusSiteNotifications === "end" && !["complete", "failed"].includes(operation.value)) {continue;}
nopaque.flash(`[<a href="/jobs/${pathArray[0]}">${nopaque.jobs[pathArray[0]].title}</a>] 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 = `<i class="left material-icons">book</i>${message}`;
break;
case "error":
classes = "red";
message = `<i class="left material-icons red-text">error</i>${message}`;
break;
case "job":
message = `<i class="left material-icons">work</i>${message}`;
break;
default:
classes = "";
message = `<i class="left material-icons">notifications</i>${message}`;
}
toast = M.toast({classes: classes,
html: `<span>${message}</span>
toast = M.toast({html: `<span>${message}</span>
<button data-action="close" class="btn-flat toast-action white-text">
<i class="material-icons">close</i>
</button>`});
@ -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) {

View File

@ -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 {

View File

@ -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>

View File

@ -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) {

View File

@ -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 %}

View File

@ -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>

View File

@ -16,27 +16,27 @@
<p class="light">Activate dark mode to ease your eyes.</p>
</div>
<div class="col s3 right-align">
<div class="switch">
<label>
{{ edit_general_settings_form.dark_mode() }}
<span class="lever"></span>
</label>
</div>
{{ M.render_field(edit_general_settings_form.dark_mode, label=False) }}
</div>
<div class="col s12"><p>&nbsp;</p></div>
<div class="col s12 divider"></div>
<div class="col s12"><p>&nbsp;</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>
<div class="col s12 m8">
<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 class="col s12 m4 right-align">
{{ M.render_field(edit_general_settings_form.job_status_site_notifications, label=False) }}
</div>
<div class="col s12"><p>&nbsp;</p></div>
<div class="col s12 divider"></div>
<div class="col s12"><p>&nbsp;</p></div>
<div class="col s12 m8">
<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 s12 m4 right-align">
{{ M.render_field(edit_general_settings_form.job_status_mail_notifications, label=False) }}
</div>
<!--
Seperate each setting with the following two elements

View File

@ -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 ###

View File

@ -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 ###