Use enums where appropriate. This commit includes new migrations that are NOT compatible with older nopaque instances

This commit is contained in:
Patrick Jentsch 2022-02-08 12:26:20 +01:00
parent fe938c0ca2
commit df6ab3991c
110 changed files with 1389 additions and 2301 deletions

View File

@ -1,13 +1,16 @@
from wtforms import BooleanField, SelectField
from ..models import Role
from ..settings.forms import EditGeneralSettingsForm
from app.models import Role
from flask_wtf import FlaskForm
from wtforms import BooleanField, SelectField, SubmitField
class EditGeneralSettingsAdminForm(EditGeneralSettingsForm):
class AdminEditUserForm(FlaskForm):
confirmed = BooleanField('Confirmed')
role = SelectField('Role', coerce=int)
role = SelectField('Role')
submit = SubmitField('Submit')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.role.choices = [(role.id, role.name)
for role in Role.query.order_by(Role.name).all()]
self.role.choices = [
(role.hashid, role.name)
for role in Role.query.order_by(Role.name).all()
]

View File

@ -1,11 +1,16 @@
from app import db, hashids
from app.decorators import admin_required
from app.models import JobStatusMailNotificationLevel, Role, User
from app.settings import tasks as settings_tasks
from app.settings.forms import (
EditGeneralSettingsForm,
EditInterfaceSettingsForm,
EditNotificationSettingsForm
)
from flask import flash, redirect, render_template, url_for
from flask_login import login_required
from . import bp
from .forms import EditGeneralSettingsAdminForm
from .. import db
from ..decorators import admin_required
from ..models import Role, User
from ..settings import tasks as settings_tasks
from .forms import AdminEditUserForm
@bp.before_request
@ -26,10 +31,15 @@ def index():
@bp.route('/users')
def users():
dict_users = {user.id: user.to_dict(backrefs=True, relationships=False)
for user in User.query.all()}
dict_users = {
user.id: user.to_dict(backrefs=True, relationships=False)
for user in User.query.all()
}
return render_template(
'admin/users.html.j2', title='Users', dict_users=dict_users)
'admin/users.html.j2',
dict_users=dict_users,
title='Users'
)
@bp.route('/users/<hashid:user_id>')
@ -41,27 +51,76 @@ def user(user_id):
@bp.route('/users/<hashid:user_id>/delete')
def delete_user(user_id):
settings_tasks.delete_user(user_id)
flash('User has been marked for deletion!')
flash('User has been marked for deletion')
return redirect(url_for('.users'))
@bp.route('/users/<hashid:user_id>/edit', methods=['GET', 'POST']) # noqa
@bp.route('/users/<hashid:user_id>/edit', methods=['GET', 'POST'])
def edit_user(user_id):
user = User.query.get_or_404(user_id)
form = EditGeneralSettingsAdminForm(user)
if form.validate_on_submit():
user.setting_dark_mode = form.dark_mode.data
user.email = form.email.data
user.username = form.username.data
user.confirmed = form.confirmed.data
user.role = Role.query.get(form.role.data)
db.session.commit()
flash('Settings have been updated.')
admin_edit_user_form = AdminEditUserForm(
prefix='admin_edit_user_form'
)
edit_general_settings_form = EditGeneralSettingsForm(
user,
prefix='edit_general_settings_form'
)
edit_interface_settings_form = EditInterfaceSettingsForm(
prefix='edit_interface_settings_form'
)
edit_notification_settings_form = EditNotificationSettingsForm(
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)
flash('Your changes have been saved')
return redirect(url_for('.edit_user', user_id=user.id))
form.confirmed.data = user.confirmed
form.dark_mode.data = user.setting_dark_mode
form.email.data = user.email
form.role.data = user.role_id
form.username.data = user.username
if (
edit_general_settings_form.submit.data
and edit_general_settings_form.validate()
):
user.email = edit_general_settings_form.email.data
user.username = edit_general_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_interface_settings_form.submit.data
and edit_interface_settings_form.validate()
):
user.setting_dark_mode = edit_interface_settings_form.dark_mode.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 = \
JobStatusMailNotificationLevel[
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))
admin_edit_user_form.confirmed.data = user.confirmed
admin_edit_user_form.role.data = user.role.hashid
edit_general_settings_form.email.data = user.email
edit_general_settings_form.username.data = user.username
edit_interface_settings_form.dark_mode.data = user.setting_dark_mode
edit_notification_settings_form.job_status_mail_notification_level.data = \
user.setting_job_status_mail_notification_level.name
return render_template(
'admin/edit_user.html.j2', form=form, title='Edit user', user=user)
'admin/edit_user.html.j2',
admin_edit_user_form=admin_edit_user_form,
edit_general_settings_form=edit_general_settings_form,
edit_interface_settings_form=edit_interface_settings_form,
edit_notification_settings_form=edit_notification_settings_form,
title='Edit user',
user=user
)

View File

@ -1,7 +1,7 @@
from app.models import User
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
from sqlalchemy import or_
from werkzeug.http import HTTP_STATUS_CODES
from ..models import User
basic_auth = HTTPBasicAuth()
token_auth = HTTPTokenAuth()

View File

@ -1,6 +1,6 @@
from app import db
from flask_restx import Namespace, Resource
from .auth import basic_auth, token_auth
from .. import db
ns = Namespace('tokens', description='Token operations')

View File

@ -1,9 +1,14 @@
from . import USERNAME_REGEX
from ..models import User
from app.models import User
from flask_wtf import FlaskForm
from wtforms import (BooleanField, PasswordField, StringField, SubmitField,
ValidationError)
from wtforms import (
BooleanField,
PasswordField,
StringField,
SubmitField,
ValidationError
)
from wtforms.validators import DataRequired, Email, EqualTo, Length, Regexp
from . import USERNAME_REGEX
class LoginForm(FlaskForm):
@ -17,42 +22,54 @@ class RegistrationForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
username = StringField(
'Username',
validators=[DataRequired(), Length(1, 64),
Regexp(USERNAME_REGEX,
message='Usernames must have only letters, numbers,'
' dots or underscores')]
validators=[
DataRequired(),
Length(1, 64),
Regexp(
USERNAME_REGEX,
message='Usernames must have only letters, numbers, dots or underscores' # noqa
)
]
)
password = PasswordField(
'Password',
validators=[DataRequired(), EqualTo('password_confirmation',
message='Passwords must match.')]
validators=[
DataRequired(),
EqualTo('password_confirmation', message='Passwords must match')
]
)
password_confirmation = PasswordField(
'Password confirmation',
validators=[DataRequired(), EqualTo('password',
message='Passwords must match.')]
validators=[
DataRequired(),
EqualTo('password', message='Passwords must match')
]
)
submit = SubmitField('Register')
def validate_email(self, field):
if User.query.filter_by(email=field.data.lower()).first():
raise ValidationError('Email already registered.')
raise ValidationError('Email already registered')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use.')
raise ValidationError('Username already in use')
class ResetPasswordForm(FlaskForm):
password = PasswordField(
'New password',
validators=[DataRequired(), EqualTo('password_confirmation',
message='Passwords must match.')]
validators=[
DataRequired(),
EqualTo('password_confirmation', message='Passwords must match')
]
)
password_confirmation = PasswordField(
'Password confirmation',
validators=[DataRequired(),
EqualTo('password', message='Passwords must match.')]
validators=[
DataRequired(),
EqualTo('password', message='Passwords must match')
]
)
submit = SubmitField('Reset Password')

View File

@ -1,15 +1,25 @@
from app import db
from app.email import create_message, send
from app.models import User
from datetime import datetime
from flask import (abort, current_app, flash, redirect, render_template,
request, url_for)
from flask import (
abort,
current_app,
flash,
redirect,
render_template,
request,
url_for
)
from flask_login import current_user, login_user, login_required, logout_user
from sqlalchemy import or_
from . import bp
from .forms import (LoginForm, ResetPasswordForm, ResetPasswordRequestForm,
RegistrationForm)
from .. import db
from ..email import create_message, send
from ..models import User
import os
from .forms import (
LoginForm,
ResetPasswordForm,
ResetPasswordRequestForm,
RegistrationForm
)
@bp.before_app_request
@ -21,10 +31,12 @@ def before_request():
if current_user.is_authenticated:
current_user.last_seen = datetime.utcnow()
db.session.commit()
if (not current_user.confirmed
if (
not current_user.confirmed
and request.endpoint
and request.blueprint != 'auth'
and request.endpoint != 'static'):
and request.endpoint != 'static'
):
return redirect(url_for('auth.unconfirmed'))
@ -34,15 +46,19 @@ def login():
return redirect(url_for('main.dashboard'))
form = LoginForm(prefix='login-form')
if form.validate_on_submit():
user = User.query.filter(or_(User.username == form.user.data,
User.email == form.user.data.lower())).first()
user = User.query.filter(
or_(
User.username == form.user.data,
User.email == form.user.data.lower()
)
).first()
if user and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
next = request.args.get('next')
if next is None or not next.startswith('/'):
next = url_for('main.dashboard')
return redirect(next)
flash('Invalid email/username or password.')
flash('Invalid email/username or password', category='error')
return render_template('auth/login.html.j2', form=form, title='Log in')
@ -50,7 +66,7 @@ def login():
@login_required
def logout():
logout_user()
flash('You have been logged out.')
flash('You have been logged out')
return redirect(url_for('main.index'))
@ -73,8 +89,8 @@ def register():
except OSError as e:
current_app.logger.error(e)
db.session.rollback()
flash('Internal Server Error', category='error')
abort(500)
else:
token = user.generate_confirmation_token()
msg = create_message(
user.email,
@ -84,7 +100,8 @@ def register():
user=user
)
send(msg)
flash('A confirmation email has been sent to you by email.')
flash('A confirmation email has been sent to you by email')
db.session.commit()
return redirect(url_for('.login'))
return render_template(
'auth/register.html.j2',
@ -100,10 +117,13 @@ def confirm(token):
return redirect(url_for('main.dashboard'))
if current_user.confirm(token):
db.session.commit()
flash('You have confirmed your account. Thanks!')
flash('You have confirmed your account')
return redirect(url_for('main.dashboard'))
else:
flash('The confirmation link is invalid or has expired.')
flash(
'The confirmation link is invalid or has expired',
category='error'
)
return redirect(url_for('.unconfirmed'))
@ -120,10 +140,15 @@ def unconfirmed():
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
msg = create_message(current_user.email, 'Confirm Your Account',
'auth/email/confirm', token=token, user=current_user)
msg = create_message(
current_user.email,
'Confirm Your Account',
'auth/email/confirm',
token=token,
user=current_user
)
send(msg)
flash('A new confirmation email has been sent to you by email.')
flash('A new confirmation email has been sent to you by email')
return redirect(url_for('auth.unconfirmed'))
@ -136,14 +161,23 @@ def reset_password_request():
user = User.query.filter_by(email=form.email.data.lower()).first()
if user is not None:
token = user.generate_reset_token()
msg = create_message(user.email, 'Reset Your Password',
'auth/email/reset_password', token=token,
user=user)
msg = create_message(
user.email,
'Reset Your Password',
'auth/email/reset_password',
token=token,
user=user
)
send(msg)
flash('An email with instructions to reset your password has been sent to you.') # noqa
flash(
'An email with instructions to reset your password has been sent to you' # noqa
)
return redirect(url_for('.login'))
return render_template('auth/reset_password_request.html.j2', form=form,
title='Password Reset')
return render_template(
'auth/reset_password_request.html.j2',
form=form,
title='Password Reset'
)
@bp.route('/reset/<token>', methods=['GET', 'POST'])
@ -154,9 +188,13 @@ def reset_password(token):
if form.validate_on_submit():
if User.reset_password(token, form.password.data):
db.session.commit()
flash('Your password has been updated.')
flash('Your password has been updated')
return redirect(url_for('.login'))
else:
return redirect(url_for('main.index'))
return render_template('auth/reset_password.html.j2', form=form,
title='Password Reset', token=token)
return render_template(
'auth/reset_password.html.j2',
form=form,
title='Password Reset',
token=token
)

View File

@ -1,10 +1,10 @@
from app import db
from app.decorators import permission_required
from app.models import Permission, Role, User
from app.settings import tasks as settings_tasks
from flask import flash, redirect, render_template, url_for
from flask_login import login_required
from . import bp
from .. import db
from ..decorators import permission_required
from ..models import Permission, Role, User
from ..settings import tasks as settings_tasks
@bp.before_request

View File

@ -1,6 +1,6 @@
from app import db, hashids, socketio
from app.decorators import socketio_login_required
from app.models import Corpus
from app.models import Corpus, CorpusStatus
from flask import session
from flask_login import current_user
from flask_socketio import ConnectionRefusedError
@ -65,7 +65,12 @@ def connect(auth):
if not (corpus.user == current_user or current_user.is_administrator()):
# return {'code': 403, 'msg': 'Forbidden'}
raise ConnectionRefusedError('Forbidden')
if corpus.status not in ['prepared', 'start analysis', 'analysing', 'stop analysis']:
if corpus.status not in [
CorpusStatus.BUILT,
CorpusStatus.STARTING_ANALYSIS_SESSION,
CorpusStatus.RUNNING_ANALYSIS_SESSION,
CorpusStatus.CANCELING_ANALYSIS_SESSION
]:
# return {'code': 424, 'msg': 'Failed Dependency'}
raise ConnectionRefusedError('Failed Dependency')
if corpus.num_analysis_sessions is None:
@ -74,7 +79,7 @@ def connect(auth):
corpus.num_analysis_sessions = Corpus.num_analysis_sessions + 1
db.session.commit()
retry_counter = 20
while corpus.status != 'analysing':
while corpus.status != CorpusStatus.RUNNING_ANALYSIS_SESSION:
if retry_counter == 0:
corpus.num_analysis_sessions = Corpus.num_analysis_sessions - 1
db.session.commit()

View File

@ -1,7 +1,12 @@
from flask_wtf import FlaskForm
from werkzeug.utils import secure_filename
from wtforms import (FileField, StringField, SubmitField,
ValidationError, IntegerField)
from wtforms import (
FileField,
StringField,
SubmitField,
ValidationError,
IntegerField
)
from wtforms.validators import DataRequired, Length

View File

@ -58,7 +58,7 @@ def add_query_result():
query_result_file_content.pop('cpos_lookup')
query_result.query_metadata = query_result_file_content
db.session.commit()
flash('Query result added!', 'result')
flash('Query result added', 'result')
return make_response({'redirect_url': url_for('.query_result', query_result_id=query_result.id)}, 201) # noqa
return render_template('corpora/query_results/add_query_result.html.j2',
form=form, title='Add query result')
@ -117,7 +117,7 @@ def delete_query_result(query_result_id):
if not (query_result.user == current_user
or current_user.is_administrator()):
abort(403)
flash(f'Query result "{query_result}" marked for deletion!', 'result')
flash(f'Query result "{query_result}" marked for deletion', 'result')
tasks.delete_query_result(query_result_id)
return redirect(url_for('services.service', service="corpus_analysis"))

View File

@ -0,0 +1,13 @@
from .. import db
from ..decorators import background
from ..models import QueryResult
@background
def delete_query_result(query_result_id, *args, **kwargs):
with kwargs['app'].app_context():
query_result = QueryResult.query.get(query_result_id)
if query_result is None:
raise Exception(f'QueryResult {query_result_id} not found')
query_result.delete()
db.session.commit()

View File

@ -1,19 +1,31 @@
from flask import (abort, current_app, flash, make_response, redirect,
render_template, url_for, send_from_directory)
from app import db
from app.models import Corpus, CorpusFile, CorpusStatus
from flask import (
abort,
current_app,
flash,
make_response,
redirect,
render_template,
url_for,
send_from_directory
)
from flask_login import current_user, login_required
from werkzeug.utils import secure_filename
from zipfile import ZipFile
from . import bp
from . import tasks
from .forms import (AddCorpusFileForm, AddCorpusForm, EditCorpusFileForm,
ImportCorpusForm)
from .. import db
from ..models import Corpus, CorpusFile
from .forms import (
AddCorpusFileForm,
AddCorpusForm,
EditCorpusFileForm,
ImportCorpusForm
)
from .import_corpus import check_zip_contents
import os
import shutil
import glob
import xml.etree.ElementTree as ET
from zipfile import ZipFile
from .import_corpus import check_zip_contents
@bp.route('/add', methods=['GET', 'POST'])
@ -34,10 +46,10 @@ def add_corpus():
except OSError as e:
current_app.logger.error(e)
db.session.rollback()
flash('Internal Server Error', 'error')
flash('Internal Server Error', category='error')
abort(500)
db.session.commit()
flash(f'Corpus "{corpus.title}" added', 'corpus')
flash(f'Corpus "{corpus.title}" added', category='corpus')
return redirect(url_for('.corpus', corpus_id=corpus.id))
return render_template(
'corpora/add_corpus.html.j2',
@ -46,6 +58,21 @@ def add_corpus():
)
@bp.route('/<hashid:corpus_id>/export')
@login_required
def export_corpus(corpus_id):
abort(503)
corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user or current_user.is_administrator()):
abort(403)
return send_from_directory(
as_attachment=True,
directory=os.path.join(corpus.user.path, 'corpora'),
filename=corpus.archive_file,
mimetype='zip'
)
@bp.route('/import', methods=['GET', 'POST'])
@login_required
def import_corpus():
@ -65,11 +92,10 @@ def import_corpus():
try:
os.makedirs(corpus.path)
except OSError as e:
current_app.logger.error(f'Could not import corpus: {e}')
current_app.logger.error(e)
db.session.rollback()
flash('Internal Server Error', 'error')
return make_response(
{'redirect_url': url_for('.import_corpus')}, 500)
flash('Internal Server Error', category='error')
return make_response({'redirect_url': url_for('.import_corpus')}, 500) # noqa
# Upload zip
archive_file = os.path.join(corpus.path, form.file.data.filename)
form.file.data.save(archive_file)
@ -102,20 +128,25 @@ def import_corpus():
)
db.session.add(corpus_file)
# finish import and redirect to imported corpus
corpus.status = 'prepared'
corpus.status = CorpusStatus.BUILT
db.session.commit()
os.remove(archive_file)
flash(f'Corpus "{corpus.title}" imported!', 'corpus')
flash(f'Corpus "{corpus.title}" imported', 'corpus')
return make_response(
{'redirect_url': url_for('.corpus', corpus_id=corpus.id)}, 201)
else:
# If imported zip is not valid delete corpus and give feedback
flash('Can not import corpus "{}" not imported: Invalid archive file!', 'error') # noqa
flash(
f'Can\'t import corpus "{corpus.title}": Invalid archive file',
category='error'
)
tasks.delete_corpus(corpus.id)
return make_response(
{'redirect_url': url_for('.import_corpus')}, 201)
return render_template('corpora/import_corpus.html.j2', form=form,
title='Import Corpus')
return make_response({'redirect_url': url_for('.import_corpus')}, 201) # noqa
return render_template(
'corpora/import_corpus.html.j2',
form=form,
title='Import Corpus'
)
@bp.route('/<hashid:corpus_id>')
@ -124,9 +155,11 @@ def corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user or current_user.is_administrator()):
abort(403)
corpus_files = [corpus_file.to_dict() for corpus_file in corpus.files]
return render_template('corpora/corpus.html.j2', corpus=corpus,
corpus_files=corpus_files, title='Corpus')
return render_template(
'corpora/corpus.html.j2',
corpus=corpus,
title='Corpus'
)
@bp.route('/<hashid:corpus_id>/analyse')
@ -140,28 +173,13 @@ def analyse_corpus(corpus_id):
)
@bp.route('/<hashid:corpus_id>/download')
@login_required
def download_corpus(corpus_id):
abort(503)
corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user or current_user.is_administrator()):
abort(403)
return send_from_directory(
as_attachment=True,
directory=os.path.join(corpus.user.path, 'corpora'),
filename=corpus.archive_file,
mimetype='zip'
)
@bp.route('/<hashid:corpus_id>/delete')
@login_required
def delete_corpus(corpus_id):
corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user or current_user.is_administrator()):
abort(403)
flash(f'Corpus "{corpus.title}" marked for deletion!', 'corpus')
flash(f'Corpus "{corpus.title}" marked for deletion', 'corpus')
tasks.delete_corpus(corpus_id)
return redirect(url_for('main.dashboard'))
@ -203,11 +221,11 @@ def add_corpus_file(corpus_id):
except OSError as e:
current_app.logger.error(e)
db.session.rollback()
flash('Internal Server Error', 'error')
flash('Internal Server Error', category='error')
return make_response({'redirect_url': url_for('.add_corpus_file', corpus_id=corpus.id)}, 500) # noqa
corpus.status = 'unprepared'
corpus.status = CorpusStatus.UNPREPARED
db.session.commit()
flash(f'Corpus file "{corpus_file.title}" added!', 'corpus')
flash(f'Corpus file "{corpus_file.filename}" added', category='corpus')
return make_response({'redirect_url': url_for('.corpus', corpus_id=corpus.id)}, 201) # noqa
return render_template(
'corpora/add_corpus_file.html.j2',
@ -220,14 +238,19 @@ def add_corpus_file(corpus_id):
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/delete')
@login_required
def delete_corpus_file(corpus_id, corpus_file_id):
corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
if not corpus_file.corpus_id == corpus_id:
abort(404)
if not (corpus_file.corpus.user == current_user
or current_user.is_administrator()):
corpus_file = CorpusFile.query.filter(
CorpusFile.corpus_id == corpus_id,
CorpusFile.id == corpus_file_id
).first_or_404()
if not (
corpus_file.corpus.user == current_user
or current_user.is_administrator()
):
abort(403)
flash(
f'Corpus file "{corpus_file.filename}" marked for deletion!', 'corpus')
f'Corpus file "{corpus_file.filename}" marked for deletion',
category='corpus'
)
tasks.delete_corpus_file(corpus_file_id)
return redirect(url_for('.corpus', corpus_id=corpus_id))
@ -235,26 +258,34 @@ def delete_corpus_file(corpus_id, corpus_file_id):
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>/download')
@login_required
def download_corpus_file(corpus_id, corpus_file_id):
corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
if not corpus_file.corpus_id == corpus_id:
abort(404)
if not (corpus_file.corpus.user == current_user
or current_user.is_administrator()):
corpus_file = CorpusFile.query.filter(
CorpusFile.corpus_id == corpus_id,
CorpusFile.id == corpus_file_id
).first_or_404()
if not (
corpus_file.corpus.user == current_user
or current_user.is_administrator()
):
abort(403)
return send_from_directory(as_attachment=True,
return send_from_directory(
as_attachment=True,
directory=os.path.dirname(corpus_file.path),
filename=corpus_file.filename)
filename=corpus_file.filename
)
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST'])
@bp.route('/<hashid:corpus_id>/files/<hashid:corpus_file_id>', methods=['GET', 'POST']) # noqa
@login_required
def corpus_file(corpus_id, corpus_file_id):
corpus = Corpus.query.get_or_404(corpus_id)
if not (corpus.user == current_user or current_user.is_administrator()):
corpus_file = CorpusFile.query.filter(
CorpusFile.corpus_id == corpus_id,
CorpusFile.id == corpus_file_id
).first_or_404()
if not (
corpus_file.corpus.user == current_user
or current_user.is_administrator()
):
abort(403)
corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
if corpus_file.corpus != corpus:
abort(404)
form = EditCorpusFileForm(prefix='edit-corpus-file-form')
if form.validate_on_submit():
corpus_file.address = form.address.data
@ -269,9 +300,9 @@ def corpus_file(corpus_id, corpus_file_id):
corpus_file.publishing_year = form.publishing_year.data
corpus_file.school = form.school.data
corpus_file.title = form.title.data
corpus.status = 'unprepared'
corpus_file.corpus.status = CorpusStatus.UNPREPARED
db.session.commit()
flash(f'Corpus file "{corpus_file.filename}" edited!', 'corpus')
flash(f'Corpus file "{corpus_file.filename}" edited', category='corpus') # noqa
return redirect(url_for('.corpus', corpus_id=corpus_id))
# If no form is submitted or valid, fill out fields with current values
form.address.data = corpus_file.address
@ -286,9 +317,13 @@ def corpus_file(corpus_id, corpus_file_id):
form.publishing_year.data = corpus_file.publishing_year
form.school.data = corpus_file.school
form.title.data = corpus_file.title
return render_template('corpora/corpus_file.html.j2', corpus=corpus,
corpus_file=corpus_file, form=form,
title='Edit corpus file')
return render_template(
'corpora/corpus_file.html.j2',
corpus=corpus,
corpus_file=corpus_file,
form=form,
title='Edit corpus file'
)
@bp.route('/<hashid:corpus_id>/build')
@ -299,7 +334,13 @@ def build_corpus(corpus_id):
abort(403)
if corpus.files.all():
tasks.build_corpus(corpus_id)
flash(f'Corpus "{corpus.title}" marked for building!', 'corpus')
flash(
f'Corpus "{corpus.title}" marked for building',
category='corpus'
)
else:
flash(f'Can\'t build corpus "{corpus.title}": No corpus file(s)!', 'error') # noqa
flash(
f'Can\'t build corpus "{corpus.title}": No corpus file(s)',
category='error'
)
return redirect(url_for('.corpus', corpus_id=corpus_id))

View File

@ -1,6 +1,6 @@
from .. import db
from ..decorators import background
from ..models import Corpus, CorpusFile, QueryResult
from app import db
from app.decorators import background
from app.models import Corpus, CorpusFile
@background
@ -32,13 +32,3 @@ def delete_corpus_file(corpus_file_id, *args, **kwargs):
raise Exception(f'Corpus file {corpus_file_id} not found')
corpus_file.delete()
db.session.commit()
@background
def delete_query_result(query_result_id, *args, **kwargs):
with kwargs['app'].app_context():
query_result = QueryResult.query.get(query_result_id)
if query_result is None:
raise Exception(f'QueryResult {query_result_id} not found')
query_result.delete()
db.session.commit()

View File

@ -1,5 +1,5 @@
from app.models import Corpus, CorpusStatus
from flask import current_app
from ..models import Corpus
import docker
import os
import shutil
@ -8,19 +8,19 @@ import shutil
class CheckCorporaMixin:
def check_corpora(self):
corpora = Corpus.query.all()
for corpus in (x for x in corpora if x.status == 'submitted'):
for corpus in (x for x in corpora if x.status == CorpusStatus.SUBMITTED): # noqa
self.create_build_corpus_service(corpus)
for corpus in (x for x in corpora if x.status == 'queued' or x.status == 'running'): # noqa
for corpus in (x for x in corpora if x.status == CorpusStatus.QUEUED or x.status == CorpusStatus.BUILDING): # noqa
self.checkout_build_corpus_service(corpus)
for corpus in (x for x in corpora if x.status == 'prepared' and x.num_analysis_sessions > 0): # noqa
corpus.status = 'start analysis'
for corpus in (x for x in corpora if x.status == 'analysing' and x.num_analysis_sessions == 0): # noqa
corpus.status = 'stop analysis'
for corpus in (x for x in corpora if x.status == 'analysing'):
for corpus in (x for x in corpora if x.status == CorpusStatus.BUILT and x.num_analysis_sessions > 0): # noqa
corpus.status = CorpusStatus.STARTING_ANALYSIS_SESSION
for corpus in (x for x in corpora if x.status == CorpusStatus.RUNNING_ANALYSIS_SESSION and x.num_analysis_sessions == 0): # noqa
corpus.status = CorpusStatus.CANCELING_ANALYSIS_SESSION
for corpus in (x for x in corpora if x.status == CorpusStatus.RUNNING_ANALYSIS_SESSION): # noqa
self.checkout_analysing_corpus_container(corpus)
for corpus in (x for x in corpora if x.status == 'start analysis'):
for corpus in (x for x in corpora if x.status == CorpusStatus.STARTING_ANALYSIS_SESSION): # noqa
self.create_cqpserver_container(corpus)
for corpus in (x for x in corpora if x.status == 'stop analysis'):
for corpus in (x for x in corpora if x.status == CorpusStatus.CANCELING_ANALYSIS_SESSION): # noqa
self.remove_cqpserver_container(corpus)
def create_build_corpus_service(self, corpus):
@ -95,7 +95,7 @@ class CheckCorporaMixin:
f'due to "docker.errors.APIError": {e}'
)
return
corpus.status = 'queued'
corpus.status = CorpusStatus.QUEUED
def checkout_build_corpus_service(self, corpus):
service_name = f'build-corpus_{corpus.id}'
@ -106,7 +106,7 @@ class CheckCorporaMixin:
f'Get service "{service_name}" failed '
f'due to "docker.errors.NotFound": {e}'
)
corpus.status = 'failed'
corpus.status = CorpusStatus.FAILED
return
except docker.errors.APIError as e:
current_app.logger.error(
@ -117,22 +117,22 @@ class CheckCorporaMixin:
if not service_tasks:
return
task_state = service_tasks[0].get('Status').get('State')
if corpus.status == 'queued' and task_state != 'pending':
corpus.status = 'running'
if corpus.status == CorpusStatus.QUEUED and task_state != 'pending': # noqa
corpus.status = CorpusStatus.BUILDING
return
elif corpus.status == 'running' and task_state == 'complete':
corpus.status = 'prepared'
elif corpus.status == 'running' and task_state == 'failed':
corpus.status = 'failed'
elif corpus.status == CorpusStatus.BUILDING and task_state == 'complete': # noqa
corpus.status = CorpusStatus.BUILT
elif corpus.status == CorpusStatus.BUILDING and task_state == 'failed': # noqa
corpus.status = CorpusStatus.FAILED
else:
return
# try:
# service.remove()
# except docker.errors.APIError as e:
# current_app.logger.error(
# f'Remove service "{service_name}" failed '
# f'due to "docker.errors.APIError": {e}'
# )
try:
service.remove()
except docker.errors.APIError as e:
current_app.logger.error(
f'Remove service "{service_name}" failed '
f'due to "docker.errors.APIError": {e}'
)
def create_cqpserver_container(self, corpus):
''' # Docker container settings # '''
@ -203,7 +203,7 @@ class CheckCorporaMixin:
f'Run container "{name}" failed '
f'due to "docker.errors.ImageNotFound" error: {e}'
)
corpus.status = 'failed'
corpus.status = CorpusStatus.FAILED
return
except docker.errors.APIError as e:
current_app.logger.error(
@ -211,7 +211,7 @@ class CheckCorporaMixin:
f'due to "docker.errors.APIError" error: {e}'
)
return
corpus.status = 'analysing'
corpus.status = CorpusStatus.RUNNING_ANALYSIS_SESSION
def checkout_analysing_corpus_container(self, corpus):
container_name = f'cqpserver_{corpus.id}'
@ -223,7 +223,7 @@ class CheckCorporaMixin:
f'due to "docker.errors.NotFound": {e}'
)
corpus.num_analysis_sessions = 0
corpus.status = 'prepared'
corpus.status = CorpusStatus.BUILT
except docker.errors.APIError as e:
current_app.logger.error(
f'Get container "{container_name}" failed '
@ -235,7 +235,7 @@ class CheckCorporaMixin:
try:
container = self.docker.containers.get(container_name)
except docker.errors.NotFound:
corpus.status = 'prepared'
corpus.status = CorpusStatus.BUILT
return
except docker.errors.APIError as e:
current_app.logger.error(

View File

@ -1,8 +1,8 @@
from app import db
from app.models import Job, JobResult, JobStatus, TesseractOCRModel
from datetime import datetime
from flask import current_app
from werkzeug.utils import secure_filename
from .. import db
from ..models import Job, JobResult, TesseractOCRModel
import docker
import json
import os
@ -12,11 +12,11 @@ import shutil
class CheckJobsMixin:
def check_jobs(self):
jobs = Job.query.all()
for job in (x for x in jobs if x.status == 'submitted'):
for job in (x for x in jobs if x.status == JobStatus.SUBMITTED):
self.create_job_service(job)
for job in (x for x in jobs if x.status in ['queued', 'running']):
for job in (x for x in jobs if x.status in [JobStatus.QUEUED, JobStatus.RUNNING]): # noqa
self.checkout_job_service(job)
for job in (x for x in jobs if x.status == 'canceling'):
for job in (x for x in jobs if x.status == JobStatus.CANCELING):
self.remove_job_service(job)
def create_job_service(self, job):
@ -74,7 +74,7 @@ class CheckJobsMixin:
service_args = json.loads(job.service_args)
model = TesseractOCRModel.query.get(service_args['model'])
if model is None:
job.status = 'failed'
job.status = JobStatus.FAILED
return
models_mount_source = model.path
models_mount_target = f'/usr/local/share/tessdata/{model.filename}'
@ -122,7 +122,7 @@ class CheckJobsMixin:
f'due to "docker.errors.APIError": {e}'
)
return
job.status = 'queued'
job.status = JobStatus.QUEUED
def checkout_job_service(self, job):
service_name = f'job_{job.id}'
@ -133,7 +133,7 @@ class CheckJobsMixin:
f'Get service "{service_name}" failed '
f'due to "docker.errors.NotFound": {e}'
)
job.status = 'failed'
job.status = JobStatus.FAILED
return
except docker.errors.APIError as e:
current_app.logger.error(
@ -145,11 +145,11 @@ class CheckJobsMixin:
if not service_tasks:
return
task_state = service_tasks[0].get('Status').get('State')
if job.status == 'queued' and task_state != 'pending':
job.status = 'running'
if job.status == JobStatus.QUEUED and task_state != 'pending':
job.status = JobStatus.RUNNING
return
elif job.status == 'running' and task_state == 'complete':
job.status = 'complete'
elif job.status == JobStatus.RUNNING and task_state == 'complete': # noqa
job.status = JobStatus.COMPLETED
results_dir = os.path.join(job.path, 'results')
with open(os.path.join(results_dir, 'outputs.json')) as f:
outputs = json.load(f)
@ -169,8 +169,8 @@ class CheckJobsMixin:
os.path.join(results_dir, output['file']),
job_result.path
)
elif job.status == 'running' and task_state == 'failed':
job.status = 'failed'
elif job.status == JobStatus.RUNNING and task_state == 'failed':
job.status = JobStatus.FAILED
else:
return
job.end_date = datetime.utcnow()
@ -187,7 +187,7 @@ class CheckJobsMixin:
try:
service = self.docker.services.get(service_name)
except docker.errors.NotFound:
job.status = 'canceled'
job.status = JobStatus.CANCELED
return
except docker.errors.APIError as e:
current_app.logger.error(

View File

@ -1,9 +1,8 @@
from app import hashids
from app import hashids, socketio
from app.decorators import socketio_login_required
from app.models import User
from flask_login import current_user
from flask_socketio import join_room
from .. import socketio
from ..decorators import socketio_login_required
from ..models import User
###############################################################################

View File

@ -1,8 +1,15 @@
from app import db, mail, socketio
from app.email import create_message
from app.models import (
Corpus,
CorpusFile,
Job,
JobInput,
JobResult,
JobStatus,
JobStatusMailNotificationLevel
)
from datetime import datetime
from flask import current_app
from .. import db, mail, socketio
from ..email import create_message
from ..models import Corpus, CorpusFile, Job, JobInput, JobResult, QueryResult
###############################################################################
@ -13,7 +20,6 @@ from ..models import Corpus, CorpusFile, Job, JobInput, JobResult, QueryResult
@db.event.listens_for(Job, 'after_delete')
@db.event.listens_for(JobInput, 'after_delete')
@db.event.listens_for(JobResult, 'after_delete')
@db.event.listens_for(QueryResult, 'after_delete')
def ressource_after_delete(mapper, connection, ressource):
jsonpatch = [{'op': 'remove', 'path': ressource.jsonpatch_path}]
room = f'users.{ressource.user_hashid}'
@ -25,14 +31,10 @@ def ressource_after_delete(mapper, connection, ressource):
@db.event.listens_for(Job, 'after_insert')
@db.event.listens_for(JobInput, 'after_insert')
@db.event.listens_for(JobResult, 'after_insert')
@db.event.listens_for(QueryResult, 'after_insert')
def ressource_after_insert_handler(mapper, connection, ressource):
value = ressource.to_dict(backrefs=False, relationships=False)
if isinstance(ressource, Job):
value['inputs'] = {}
value['results'] = {}
elif isinstance(ressource, Corpus):
value['files'] = {}
for relationship in mapper.relationships:
value[relationship.key] = {}
jsonpatch = [
{'op': 'add', 'path': ressource.jsonpatch_path, 'value': value}
]
@ -45,35 +47,43 @@ def ressource_after_insert_handler(mapper, connection, ressource):
@db.event.listens_for(Job, 'after_update')
@db.event.listens_for(JobInput, 'after_update')
@db.event.listens_for(JobResult, 'after_update')
@db.event.listens_for(QueryResult, 'after_update')
def ressource_after_update_handler(mapper, connection, ressource):
jsonpatch = []
for attr in db.inspect(ressource).attrs:
# We don't want to handle changes in relationship fields
# TODO: Find a way to handle this without a hardcoded list
if attr.key in ['files', 'inputs', 'results']:
# Don't handle changes in relationship fields
if attr.key in mapper.relationships:
continue
# Check if their are changes for the current field
history = attr.load_history()
if not history.has_changes():
continue
new_value = history.added[0]
if isinstance(history.added[0], datetime):
# In order to be JSON serializable, DateTime attributes must be
# converted to a string
if isinstance(new_value, datetime):
new_value = new_value.isoformat() + 'Z'
attr_name = attr.key
value = history.added[0].isoformat() + 'Z'
elif attr.key.endswith('_enum_value'):
# Handling fake enum attributes
attr_name = attr.key[:-11]
value = getattr(ressource, attr_name).name
else:
attr_name = attr.key
value = history.added[0]
jsonpatch.append(
{
'op': 'replace',
'path': f'{ressource.jsonpatch_path}/{attr.key}',
'value': new_value
'path': f'{ressource.jsonpatch_path}/{attr_name}',
'value': value
}
)
# Job status update notification if it changed and wanted by the user
if isinstance(ressource, Job) and attr.key == 'status':
if ressource.user.setting_job_status_mail_notifications == 'none': # noqa
if isinstance(ressource, Job) and attr_name == 'status':
if ressource.user.setting_job_status_mail_notification_level == JobStatusMailNotificationLevel.NONE: # noqa
pass
elif (ressource.user.setting_job_status_mail_notifications == 'end' # noqa
and ressource.status not in ['complete', 'failed']):
elif (
ressource.user.setting_job_status_mail_notification_level == JobStatusMailNotificationLevel.END # noqa
and ressource.status not in [JobStatus.COMPLETED, JobStatus.FAILED] # noqa
):
pass
else:
msg = create_message(

View File

@ -1,10 +1,16 @@
from flask import (abort, flash, redirect, render_template,
send_from_directory, url_for)
from app.decorators import admin_required
from app.models import Job, JobInput, JobResult, JobStatus
from flask import (
abort,
flash,
redirect,
render_template,
send_from_directory,
url_for
)
from flask_login import current_user, login_required
from . import bp
from . import tasks
from ..decorators import admin_required
from ..models import Job, JobInput, JobResult
import os
@ -14,9 +20,11 @@ def job(job_id):
job = Job.query.get_or_404(job_id)
if not (job.user == current_user or current_user.is_administrator()):
abort(403)
job_inputs = [job_input.to_dict() for job_input in job.inputs]
return render_template('jobs/job.html.j2', job=job, job_inputs=job_inputs,
title='Job')
return render_template(
'jobs/job.html.j2',
job=job,
title='Job'
)
@bp.route('/<hashid:job_id>/delete')
@ -26,15 +34,21 @@ def delete_job(job_id):
if not (job.user == current_user or current_user.is_administrator()):
abort(403)
tasks.delete_job(job_id)
flash('Job has been marked for deletion!', 'job')
flash(f'Job "{job.title}" marked for deletion', 'job')
return redirect(url_for('main.dashboard'))
@bp.route('/<hashid:job_id>/inputs/<hashid:job_input_id>/download')
@login_required
def download_job_input(job_id, job_input_id):
job_input = JobInput.query.filter(JobInput.job_id == job_id, JobInput.id == job_input_id).first_or_404() # noqa
if not (job_input.job.user == current_user or current_user.is_administrator()): # noqa
job_input = JobInput.query.filter(
JobInput.job_id == job_id,
JobInput.id == job_input_id
).first_or_404()
if not (
job_input.job.user == current_user
or current_user.is_administrator()
):
abort(403)
return send_from_directory(
as_attachment=True,
@ -49,19 +63,28 @@ def download_job_input(job_id, job_input_id):
@admin_required
def restart(job_id):
job = Job.query.get_or_404(job_id)
if job.status not in ['complete', 'failed']:
flash(f'Can not restart job "{job.title}": Status is not "complete/failed"', 'error') # noqa
if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
flash(
f'Can\'t restart job "{job.title}": Status is not "Completed/Failed"', # noqa
category='error'
)
else:
tasks.restart_job(job_id)
flash(f'Job "{job.title}" marked to get restarted!', 'job')
flash(f'Job "{job.title}" marked to get restarted', category='job')
return redirect(url_for('.job', job_id=job_id))
@bp.route('/<hashid:job_id>/results/<hashid:job_result_id>/download')
@login_required
def download_job_result(job_id, job_result_id):
job_result = JobResult.query.filter(JobResult.job_id == job_id, JobResult.id == job_result_id).first_or_404() # noqa
if not (job_result.job.user == current_user or current_user.is_administrator()): # noqa
job_result = JobResult.query.filter(
JobResult.job_id == job_id,
JobResult.id == job_result_id
).first_or_404()
if not (
job_result.job.user == current_user
or current_user.is_administrator()
):
abort(403)
return send_from_directory(
as_attachment=True,

View File

@ -1,6 +1,6 @@
from .. import db
from ..decorators import background
from ..models import Job
from app import db
from app.decorators import background
from app.models import Job
@background

View File

@ -1,8 +1,8 @@
from app.auth.forms import LoginForm
from app.models import User
from flask import flash, redirect, render_template, url_for
from flask_login import login_required, login_user
from . import bp
from ..auth.forms import LoginForm
from ..models import User
@bp.route('/', methods=['GET', 'POST'])
@ -21,8 +21,10 @@ def index():
@bp.route('/faq')
def faq():
return render_template('main/faq.html.j2',
title='Frequently Asked Questions')
return render_template(
'main/faq.html.j2',
title='Frequently Asked Questions'
)
@bp.route('/dashboard')
@ -38,8 +40,10 @@ def news():
@bp.route('/privacy_policy')
def privacy_policy():
return render_template('main/privacy_policy.html.j2',
title='Privacy statement (GDPR)')
return render_template(
'main/privacy_policy.html.j2',
title='Privacy statement (GDPR)'
)
@bp.route('/terms_of_use')

View File

@ -1,4 +1,5 @@
from datetime import datetime, timedelta
from enum import IntEnum
from flask import current_app, url_for
from flask_hashids import HashidMixin
from flask_login import UserMixin
@ -8,7 +9,6 @@ from tqdm import tqdm
from werkzeug.security import generate_password_hash, check_password_hash
from . import db, login
import base64
import enum
import json
import os
import requests
@ -17,7 +17,36 @@ import xml.etree.ElementTree as ET
import yaml
class Permission(enum.IntEnum):
class CorpusStatus(IntEnum):
UNPREPARED = 1
SUBMITTED = 2
QUEUED = 3
BUILDING = 4
BUILT = 5
FAILED = 6
STARTING_ANALYSIS_SESSION = 7
RUNNING_ANALYSIS_SESSION = 8
CANCELING_ANALYSIS_SESSION = 9
class JobStatus(IntEnum):
INITIALIZING = 1
SUBMITTED = 2
QUEUED = 3
RUNNING = 4
CANCELING = 5
CANCELED = 6
COMPLETED = 7
FAILED = 8
class JobStatusMailNotificationLevel(IntEnum):
NONE = 1
END = 2
ALL = 3
class Permission(IntEnum):
'''
Defines User permissions as integers by the power of 2. User permission
can be evaluated using the bitwise operator &.
@ -130,10 +159,11 @@ class User(HashidMixin, UserMixin, db.Model):
token_expiration = db.Column(db.DateTime)
username = db.Column(db.String(64), unique=True, index=True)
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')
setting_job_status_mail_notification_level_enum_value = db.Column(
'setting_job_status_mail_notification_level',
db.Integer,
default=2
)
# Backrefs: role: Role
# Relationships
tesseract_ocr_models = db.relationship(
@ -154,12 +184,6 @@ class User(HashidMixin, UserMixin, db.Model):
cascade='all, delete-orphan',
lazy='dynamic'
)
query_results = db.relationship(
'QueryResult',
backref='user',
cascade='all, delete-orphan',
lazy='dynamic'
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
@ -188,7 +212,20 @@ class User(HashidMixin, UserMixin, db.Model):
@property
def path(self):
return os.path.join(
current_app.config.get('NOPAQUE_DATA_DIR'), str(self.id))
current_app.config.get('NOPAQUE_DATA_DIR'), 'users', str(self.id))
@property
def setting_job_status_mail_notification_level(self):
return JobStatusMailNotificationLevel(
self.setting_job_status_mail_notification_level_enum_value
)
@setting_job_status_mail_notification_level.setter
def setting_job_status_mail_notification_level(self, enum_member):
if not isinstance(enum_member, JobStatusMailNotificationLevel):
return TypeError()
self.setting_job_status_mail_notification_level_enum_value = \
enum_member.value
def can(self, permission):
return self.role.has_permission(permission)
@ -251,10 +288,8 @@ class User(HashidMixin, UserMixin, db.Model):
'username': self.username,
'settings': {
'dark_mode': self.setting_dark_mode,
'job_status_mail_notifications':
self.setting_job_status_mail_notifications,
'job_status_site_notifications':
self.setting_job_status_site_notifications
'job_status_mail_notification_level':
self.setting_job_status_mail_notification_level.name
}
}
if backrefs:
@ -269,9 +304,9 @@ class User(HashidMixin, UserMixin, db.Model):
x.hashid: x.to_dict(backrefs=False, relationships=True)
for x in self.jobs
}
dict_user['query_results'] = {
dict_user['tesseract_ocr_models'] = {
x.hashid: x.to_dict(backrefs=False, relationships=True)
for x in self.query_results
for x in self.tesseract_ocr_models
}
return dict_user
@ -338,6 +373,25 @@ class TesseractOCRModel(FileMixin, HashidMixin, db.Model):
str(self.id)
)
def to_dict(self, backrefs=False, relationships=False):
compatible_service_versions = json.loads(self.compatible_service_versions) # noqa
dict_tesseract_ocr_model = {
'id': self.hashid,
'user_id': self.user.hashid,
'compatible_service_versions': compatible_service_versions,
'description': self.description,
'publisher': self.publisher,
'publishing_year': self.publishing_year,
'title': self.title,
**self.file_mixin_to_dict()
}
if backrefs:
dict_tesseract_ocr_model['user'] = self.user.to_dict(
backrefs=True, relationships=False)
if relationships:
pass
return dict_tesseract_ocr_model
@staticmethod
def insert_defaults():
user = User.query.filter_by(username='nopaque').first()
@ -519,7 +573,7 @@ class Job(HashidMixin, db.Model):
'''
service_args = db.Column(db.String(255))
service_version = db.Column(db.String(16))
status = db.Column(db.String(16))
status_enum_value = db.Column('status', db.Integer, default=1)
title = db.Column(db.String(32))
# Backrefs: user: User
# Relationships
@ -547,6 +601,16 @@ class Job(HashidMixin, db.Model):
def path(self):
return os.path.join(self.user.path, 'jobs', str(self.id))
@property
def status(self):
return JobStatus(self.status_enum_value)
@status.setter
def status(self, enum_member):
if not isinstance(enum_member, JobStatus):
return TypeError()
self.status_enum_value = enum_member.value
@property
def url(self):
return url_for('jobs.job', job_id=self.id)
@ -559,13 +623,13 @@ class Job(HashidMixin, db.Model):
'''
Delete the job and its inputs and results from the database.
'''
if self.status not in ['complete', 'failed']:
self.status = 'canceling'
if self.status not in [JobStatus.COMPLETED, JobStatus.FAILED]: # noqa
self.status = JobStatus.CANCELING
db.session.commit()
while self.status != 'canceled':
while self.status != JobStatus.CANCELED:
# In case the daemon handled a job in any way
if self.status != 'canceling':
self.status = 'canceling'
if self.status != JobStatus.CANCELING:
self.status = JobStatus.CANCELING
db.session.commit()
sleep(1)
db.session.refresh(self)
@ -583,14 +647,14 @@ class Job(HashidMixin, db.Model):
Restart a job - only if the status is complete or failed
'''
if self.status not in ['complete', 'failed']:
raise Exception('Could not restart job: status is not "complete/failed"') # noqa
if self.status not in [JobStatus.COMPLETED, JobStatus.FAILED]: # noqa
raise Exception('Could not restart job: status is not "completed/failed"') # noqa
shutil.rmtree(os.path.join(self.path, 'results'), ignore_errors=True)
shutil.rmtree(os.path.join(self.path, 'pyflow.data'), ignore_errors=True) # noqa
for result in self.results:
db.session.delete(result)
self.end_date = None
self.status = 'submitted'
self.status = JobStatus.SUBMITTED
def to_dict(self, backrefs=False, relationships=False):
service_args = json.loads(self.service_args)
@ -606,7 +670,7 @@ class Job(HashidMixin, db.Model):
'service': self.service,
'service_args': service_args,
'service_version': self.service_version,
'status': self.status,
'status': self.status.name,
'title': self.title,
'url': self.url
}
@ -687,7 +751,7 @@ class CorpusFile(FileMixin, HashidMixin, db.Model):
)
pass
db.session.delete(self)
self.corpus.status = 'unprepared'
self.corpus.status = CorpusStatus.UNPREPARED
def to_dict(self, backrefs=False, relationships=False):
dict_corpus_file = {
@ -729,7 +793,7 @@ class Corpus(HashidMixin, db.Model):
creation_date = db.Column(db.DateTime(), default=datetime.utcnow)
description = db.Column(db.String(255))
last_edited_date = db.Column(db.DateTime(), default=datetime.utcnow)
status = db.Column(db.String(16), default='unprepared')
status_enum_value = db.Column('status', db.Integer, default=1)
title = db.Column(db.String(32))
num_analysis_sessions = db.Column(db.Integer, default=0)
num_tokens = db.Column(db.Integer, default=0)
@ -742,7 +806,7 @@ class Corpus(HashidMixin, db.Model):
lazy='dynamic',
cascade='all, delete-orphan'
)
# Python class variables
# "static" attributes
max_num_tokens = 2147483647
def __repr__(self):
@ -760,6 +824,16 @@ class Corpus(HashidMixin, db.Model):
def path(self):
return os.path.join(self.user.path, 'corpora', str(self.id))
@property
def status(self):
return CorpusStatus(self.status_enum_value)
@status.setter
def status(self, enum_member):
if not isinstance(enum_member, CorpusStatus):
return TypeError()
self.status_enum_value = enum_member.value
@property
def url(self):
return url_for('corpora.corpus', corpus_id=self.id)
@ -791,7 +865,7 @@ class Corpus(HashidMixin, db.Model):
encoding='utf-8'
)
self.last_edited_date = datetime.utcnow()
self.status = 'submitted'
self.status = CorpusStatus.SUBMITTED
def delete(self):
shutil.rmtree(self.path, ignore_errors=True)
@ -815,7 +889,7 @@ class Corpus(HashidMixin, db.Model):
'max_num_tokens': self.max_num_tokens,
'num_analysis_sessions': self.num_analysis_sessions,
'num_tokens': self.num_tokens,
'status': self.status,
'status': self.status.name,
'last_edited_date': self.last_edited_date.isoformat() + 'Z',
'title': self.title
}
@ -830,70 +904,6 @@ class Corpus(HashidMixin, db.Model):
return dict_corpus
class QueryResult(FileMixin, HashidMixin, db.Model):
__tablename__ = 'query_results'
# Primary key
id = db.Column(db.Integer, primary_key=True)
# Foreign keys
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
# Fields
description = db.Column(db.String(255))
query_metadata = db.Column(db.JSON())
title = db.Column(db.String(32))
# Backrefs: user: User
def __repr__(self):
'''
String representation of the QueryResult. For human readability.
'''
return f'<QueryResult {self.title}>'
@property
def download_url(self):
return url_for(
'corpora.download_query_result', query_result_id=self.id)
@property
def jsonpatch_path(self):
return f'{self.user.jsonpatch_path}/query_results/{self.hashid}'
@property
def path(self):
return os.path.join(
self.user.path, 'query_results', str(self.id), self.filename)
@property
def url(self):
return url_for('corpora.query_result', query_result_id=self.id)
@property
def user_hashid(self):
return self.user.hashid
def delete(self):
shutil.rmtree(self.path, ignore_errors=True)
db.session.delete(self)
def to_dict(self, backrefs=False, relationships=False):
dict_query_result = {
'id': self.hashid,
'user_id': self.user.hashid,
'download_url': self.download_url,
'url': self.url,
'corpus_title': self.query_metadata['corpus_name'],
'description': self.description,
'filename': self.filename,
'query': self.query_metadata['query'],
'query_metadata': self.query_metadata,
'title': self.title,
**self.file_mixin_to_dict(
backrefs=backrefs, relationships=relationships)
}
if backrefs:
dict_query_result['user'] = self.user.to_dict(
backrefs=True, relationships=False)
@login.user_loader
def load_user(user_id):
return User.query.get(int(user_id))

View File

@ -0,0 +1,62 @@
class QueryResult(FileMixin, HashidMixin, db.Model):
__tablename__ = 'query_results'
# Primary key
id = db.Column(db.Integer, primary_key=True)
# Foreign keys
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
# Fields
description = db.Column(db.String(255))
query_metadata = db.Column(db.JSON())
title = db.Column(db.String(32))
# Backrefs: user: User
def __repr__(self):
'''
String representation of the QueryResult. For human readability.
'''
return f'<QueryResult {self.title}>'
@property
def download_url(self):
return url_for(
'corpora.download_query_result', query_result_id=self.id)
@property
def jsonpatch_path(self):
return f'{self.user.jsonpatch_path}/query_results/{self.hashid}'
@property
def path(self):
return os.path.join(
self.user.path, 'query_results', str(self.id), self.filename)
@property
def url(self):
return url_for('corpora.query_result', query_result_id=self.id)
@property
def user_hashid(self):
return self.user.hashid
def delete(self):
shutil.rmtree(self.path, ignore_errors=True)
db.session.delete(self)
def to_dict(self, backrefs=False, relationships=False):
dict_query_result = {
'id': self.hashid,
'user_id': self.user.hashid,
'download_url': self.download_url,
'url': self.url,
'corpus_title': self.query_metadata['corpus_name'],
'description': self.description,
'filename': self.filename,
'query': self.query_metadata['query'],
'query_metadata': self.query_metadata,
'title': self.title,
**self.file_mixin_to_dict(
backrefs=backrefs, relationships=relationships)
}
if backrefs:
dict_query_result['user'] = self.user.to_dict(
backrefs=True, relationships=False)

View File

@ -1,7 +1,13 @@
from app.models import TesseractOCRModel
from flask_wtf import FlaskForm
from wtforms import (BooleanField, MultipleFileField, SelectField, StringField,
SubmitField, ValidationError)
from wtforms import (
BooleanField,
MultipleFileField,
SelectField,
StringField,
SubmitField,
ValidationError
)
from wtforms.validators import DataRequired, Length
from . import SERVICES
@ -25,7 +31,7 @@ class AddSpacyNLPJobForm(AddJobForm):
def validate_encoding_detection(self, field):
service_info = SERVICES['spacy-nlp']['versions'][self.version.data]
if field.data and 'encoding_detection' not in service_info:
if field.data and 'encoding_detection' not in service_info['methods']:
raise ValidationError('Encoding detection is not available')
def validate_files(form, field):
@ -41,7 +47,7 @@ class AddSpacyNLPJobForm(AddJobForm):
version = kwargs.pop('version', SERVICES['spacy-nlp']['latest_version']) # noqa
super().__init__(*args, **kwargs)
service_info = SERVICES['spacy-nlp']['versions'][version]
if 'check_encoding' not in service_info['methods']:
if 'encoding_detection' not in service_info['methods']:
self.encoding_detection.render_kw = {'disabled': True}
self.model.choices += [(x, y) for x, y in service_info['models'].items()] # noqa
self.version.choices = [(x, x) for x in SERVICES['spacy-nlp']['versions']] # noqa
@ -60,7 +66,7 @@ class AddTesseractOCRJobForm(AddJobForm):
def validate_binarization(self, field):
service_info = SERVICES['tesseract-ocr']['versions'][self.version.data]
if field.data and 'binarization' not in service_info:
if field.data and 'binarization' not in service_info['methods']:
raise ValidationError('Binarization is not available')
def validate_files(self, field):

View File

@ -1,21 +1,29 @@
from app import hashids
from flask import (abort, current_app, flash, make_response, render_template,
request, url_for)
from app import db, hashids
from app.models import Job, JobInput, JobStatus
from flask import (
abort,
current_app,
flash,
make_response,
render_template,
request,
url_for
)
from flask_login import current_user, login_required
from werkzeug.utils import secure_filename
from . import bp
from . import SERVICES
from .. import db
from .forms import AddJobForms
from ..models import Job, JobInput
import json
@bp.route('/corpus-analysis')
@login_required
def corpus_analysis():
return render_template('services/corpus_analysis.html.j2',
title='Corpus analysis')
return render_template(
'services/corpus_analysis.html.j2',
title='Corpus analysis'
)
@bp.route('/<service>', methods=['GET', 'POST'])
@ -47,7 +55,6 @@ def service(service):
service=service,
service_args=json.dumps(service_args),
service_version=form.version.data,
status='preparing',
title=form.title.data
)
db.session.add(job)
@ -77,7 +84,7 @@ def service(service):
db.session.rollback()
flash('Internal Server Error', 'error')
return make_response({'redirect_url': url_for('.service', service=service)}, 500) # noqa
job.status = 'submitted'
job.status = JobStatus.SUBMITTED
db.session.commit()
flash(f'Job "{job.title}" added', 'job')
return make_response({'redirect_url': url_for('jobs.job', job_id=job.id)}, 201) # noqa

View File

@ -1,21 +1,37 @@
from app.auth import USERNAME_REGEX
from app.models import JobStatusMailNotificationLevel, User
from flask_wtf import FlaskForm
from wtforms import (BooleanField, PasswordField, SelectField, StringField,
SubmitField, ValidationError)
from wtforms import (
BooleanField,
PasswordField,
SelectField,
StringField,
SubmitField,
ValidationError
)
from wtforms.validators import DataRequired, Email, EqualTo, Length, Regexp
from ..auth import USERNAME_REGEX
from ..models import User
class ChangePasswordForm(FlaskForm):
password = PasswordField('Old password', validators=[DataRequired()])
new_password = PasswordField(
'New password',
validators=[DataRequired(), EqualTo('password_confirmation',
message='Passwords must match.')]
validators=[
DataRequired(),
EqualTo(
'new_password_confirmation',
message='Passwords must match'
)
new_password2 = PasswordField(
'Confirm new password', validators=[DataRequired()])
submit = SubmitField('Change password')
]
)
new_password_confirmation = PasswordField(
'Confirm new password',
validators=[
DataRequired(),
EqualTo('new_password', message='Passwords must match')
]
)
submit = SubmitField('Submit')
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -23,20 +39,24 @@ class ChangePasswordForm(FlaskForm):
def validate_current_password(self, field):
if not self.user.verify_password(field.data):
raise ValidationError('Invalid password.')
raise ValidationError('Invalid password')
class EditGeneralSettingsForm(FlaskForm):
dark_mode = BooleanField('Dark mode')
email = StringField('E-Mail',
validators=[DataRequired(), Length(1, 254), Email()])
email = StringField(
'E-Mail',
validators=[DataRequired(), Length(1, 254), Email()]
)
username = StringField(
'Benutzername',
validators=[DataRequired(),
'Username',
validators=[
DataRequired(),
Length(1, 64),
Regexp(USERNAME_REGEX,
message='Usernames must have only letters, numbers,'
' dots or underscores')]
Regexp(
USERNAME_REGEX,
message='Usernames must have only letters, numbers, dots or underscores' # noqa
)
]
)
submit = SubmitField('Submit')
@ -45,29 +65,36 @@ class EditGeneralSettingsForm(FlaskForm):
self.user = user
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.')
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.')
if (
field.data != self.user.username
and User.query.filter_by(username=field.data).first()
):
raise ValidationError('Username already in use')
class EditInterfaceSettingsForm(FlaskForm):
dark_mode = BooleanField('Dark mode')
submit = SubmitField('Submit')
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')
job_status_mail_notification_level = SelectField(
'Job status mail notification level',
choices=[('', 'Choose your option')],
validators=[DataRequired()]
)
submit = SubmitField('Submit')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.job_status_mail_notification_level.choices += [
(enum_member.name, enum_member.name.capitalize())
for enum_member in JobStatusMailNotificationLevel
]

View File

@ -1,66 +1,82 @@
from flask import flash, redirect, render_template, url_for
from flask_login import current_user, login_required, logout_user
from . import bp, tasks
from .forms import (ChangePasswordForm, EditGeneralSettingsForm,
EditNotificationSettingsForm)
from .forms import (
ChangePasswordForm,
EditGeneralSettingsForm,
EditInterfaceSettingsForm,
EditNotificationSettingsForm
)
from .. import db
from ..models import JobStatusMailNotificationLevel
@bp.route('/')
@bp.route('', methods=['GET', 'POST'])
@login_required
def index():
return redirect(url_for('.edit_general_settings'))
change_password_form = ChangePasswordForm(
current_user._get_current_object(),
prefix='change_password_form'
)
edit_general_settings_form = EditGeneralSettingsForm(
current_user._get_current_object(),
prefix='edit_general_settings_form'
)
edit_interface_settings_form = EditInterfaceSettingsForm(
prefix='edit_interface_settings_form'
)
edit_notification_settings_form = EditNotificationSettingsForm(
prefix='edit_notification_settings_form'
)
@bp.route('/change_password', methods=['GET', 'POST'])
@login_required
def change_password():
form = ChangePasswordForm(current_user._get_current_object())
if form.validate_on_submit():
current_user.password = form.new_password.data
if change_password_form.submit.data and change_password_form.validate():
current_user.password = change_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')
@bp.route('/edit_general_settings', methods=['GET', 'POST'])
@login_required
def edit_general_settings():
form = EditGeneralSettingsForm(current_user._get_current_object())
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
flash('Your changes have been saved')
return redirect(url_for('.index'))
if (
edit_general_settings_form.submit.data
and edit_general_settings_form.validate()
):
current_user.email = edit_general_settings_form.email.data
current_user.username = edit_general_settings_form.username.data
db.session.commit()
flash('Your changes have been saved.')
return redirect(url_for('.edit_general_settings'))
form.dark_mode.data = current_user.setting_dark_mode
form.email.data = current_user.email
form.username.data = current_user.username
return render_template('settings/edit_general_settings.html.j2',
form=form, title='General settings')
@bp.route('/edit_notification_settings', methods=['GET', 'POST'])
@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
flash('Your changes have been saved')
return redirect(url_for('.index'))
if (
edit_interface_settings_form.submit.data
and edit_interface_settings_form.validate()
):
current_user.setting_dark_mode = \
edit_interface_settings_form.dark_mode.data
db.session.commit()
flash('Your changes have been saved.')
return redirect(url_for('.edit_notification_settings'))
form.job_status_mail_notifications.data = \
current_user.setting_job_status_mail_notifications
form.job_status_site_notifications.data = \
current_user.setting_job_status_site_notifications
return render_template('settings/edit_notification_settings.html.j2',
form=form, title='Notification settings')
flash('Your changes have been saved')
return redirect(url_for('.index'))
if (
edit_notification_settings_form.submit.data
and edit_notification_settings_form.validate()
):
current_user.setting_job_status_mail_notification_level = \
JobStatusMailNotificationLevel[
edit_notification_settings_form.job_status_mail_notification_level.data # noqa
]
db.session.commit()
flash('Your changes have been saved')
return redirect(url_for('.index'))
edit_general_settings_form.email.data = current_user.email
edit_general_settings_form.username.data = current_user.username
edit_interface_settings_form.dark_mode.data = \
current_user.setting_dark_mode
edit_notification_settings_form.job_status_mail_notification_level.data = \
current_user.setting_job_status_mail_notification_level.name
return render_template(
'settings/index.html.j2',
change_password_form=change_password_form,
edit_general_settings_form=edit_general_settings_form,
edit_interface_settings_form=edit_interface_settings_form,
edit_notification_settings_form=edit_notification_settings_form,
title='Settings'
)
@bp.route('/delete')
@ -71,5 +87,5 @@ def delete():
"""
tasks.delete_user(current_user.id)
logout_user()
flash('Your account has been marked for deletion!')
flash('Your account has been marked for deletion')
return redirect(url_for('main.index'))

View File

@ -1,6 +1,6 @@
from .. import db
from ..decorators import background
from ..models import User
from app import db
from app.decorators import background
from app.models import User
@background

View File

@ -1,3 +1,7 @@
:root {
--main-bg-color: brown;
}
/* Change navbar height bacause an extended and fixed navbar is used */
.navbar-fixed {
height: 112px;
@ -12,15 +16,6 @@
opacity: 1;
}
/* preloader circle in the size of a button icon */
.button-icon-spinner {
bottom: -5px !important;
right: 55px !important;
margin-right: 12px !important;
width: 19.5px !important;
height: 19.5px !important;
}
/*
* changes preoloader size etc. to fit visually better with the chip status
* indicator of jobs
@ -39,36 +34,37 @@
transform: scale(2);
}
.btn-scale-x2 .nopaque-icons.service-icon {
.btn-scale-x2 .nopaque-icon.nopaque-service-icon {
font-size: 2.5rem;
}
/* Fix material icon vertical alignment when nested in various elements */
h1 .nopaque-icons, h2 .nopaque-icons, h3 .nopaque-icons, h4 .nopaque-icons,
.tab .nopaque-icons, .tab .material-icons {
h1 .nopaque-icon, h2 .nopaque-icon, h3 .nopaque-icon, h4 .nopaque-icon, .tab .nopaque-icon, .tab .material-icons {
line-height: inherit;
}
.nopaque-icons.service-icon[data-service="corpus-analysis"]:empty:before {content: "H";}
.nopaque-icons.service-icon[data-service="file-setup"]:empty:before {content: "E";}
.nopaque-icons.service-icon[data-service="spacy-nlp"]:empty:before {content: "G";}
.nopaque-icons.service-icon[data-service="tesseract-ocr"]:empty:before {content: "F";}
.status-text[data-status]:empty:before {content: attr(data-status);}
.nopaque-icon.nopaque-service-icon[data-service="file-setup"]:empty:before {content: "E";}
.nopaque-icon.nopaque-service-icon[data-service="tesseract-ocr"]:empty:before {content: "F";}
.nopaque-icon.nopaque-service-icon[data-service="spacy-nlp"]:empty:before {content: "G";}
.nopaque-icon.nopaque-service-icon[data-service="corpus-analysis"]:empty:before {content: "H";}
.nopaque-corpus-status-text[data-corpus-status="UNPREPARED"]:empty:before {content: "Unprepared";}
.nopaque-corpus-status-text[data-corpus-status="SUBMITTED"]:empty:before {content: "Submitted";}
.nopaque-corpus-status-text[data-corpus-status="QUEUED"]:empty:before {content: "Queued";}
.nopaque-corpus-status-text[data-corpus-status="BUILDING"]:empty:before {content: "Building";}
.nopaque-corpus-status-text[data-corpus-status="BUILT"]:empty:before {content: "Built";}
.nopaque-corpus-status-text[data-corpus-status="STARTING_ANALYSIS_SESSION"]:empty:before {content: "Starting analysis session";}
.nopaque-corpus-status-text[data-corpus-status="RUNNING_ANALYSIS_SESSION"]:empty:before {content: "Running analysis session";}
.nopaque-corpus-status-text[data-corpus-status="CANCELING_ANALYSIS_SESSION"]:empty:before {content: "Canceling analysis session";}
.nopaque-job-status-text[data-job-status="INITIALIZING"]:empty:before {content: "Initializing";}
.nopaque-job-status-text[data-job-status="SUBMITTED"]:empty:before {content: "Submitted";}
.nopaque-job-status-text[data-job-status="QUEUED"]:empty:before {content: "Queued";}
.nopaque-job-status-text[data-job-status="RUNNING"]:empty:before {content: "Running";}
.nopaque-job-status-text[data-job-status="CANCELING"]:empty:before {content: "Canceling";}
.nopaque-job-status-text[data-job-status="CANCELED"]:empty:before {content: "Canceled";}
.nopaque-job-status-text[data-job-status="COMPLETED"]:empty:before {content: "Completed";}
.nopaque-job-status-text[data-job-status="FAILED"]:empty:before {content: "Failed";}
.hoverable {cursor: pointer;}
.s-attr.chip .p-attr.chip {background-color: inherit;}
.responsive-youtube-video-container {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%;
}
.responsive-youtube-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.chip.s-attr .chip.p-attr {background-color: inherit;}

View File

@ -1,14 +1,14 @@
@font-face {
font-family: 'nopaque Icons';
font-family: 'Nopaque Icons';
font-style: normal;
font-weight: 400;
src: local('nopaque Icons'),
local('nopaqueIcons-Regular'),
url(../fonts/nopaque_icons/nopaqueIcons-Regular.otf) format('opentype');
local('NopaqueIcons-Regular'),
url(../fonts/nopaque_icons/NopaqueIcons-Regular.otf) format('opentype');
}
.nopaque-icons {
font-family: 'nopaque Icons';
.nopaque-icon {
font-family: 'Nopaque Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */

View File

@ -31,7 +31,7 @@ class App {
iconPrefix = '<i class="error-color-text left material-icons">error</i>';
break;
case 'job':
iconPrefix = '<i class="left nopaque-icons">J</i>';
iconPrefix = '<i class="left nopaque-icon">J</i>';
break;
default:
iconPrefix = '<i class="left material-icons">notifications</i>';

View File

@ -16,7 +16,7 @@ class JobStatusNotifier {
.filter(operation => re.test(operation.path));
for (operation of filteredPatch) {
[match, jobId] = operation.path.match(re);
app.flash(`[<a href="/jobs/${jobId}">${app.users[this.userId].jobs[jobId].title}</a>] New status: ${operation.value}`, 'job');
app.flash(`[<a href="/jobs/${jobId}">${app.users[this.userId].jobs[jobId].title}</a>] New status: <span class="nopaque-job-status-text" data-job-status="${operation.value}"></span>`, 'job');
}
}
}

View File

@ -67,30 +67,29 @@ class CorpusDisplay extends RessourceDisplay {
let element;
let elements;
this.setElements(this.displayElement.querySelectorAll('.corpus-status'), status);
elements = this.displayElement.querySelectorAll('.analyse-corpus-trigger')
elements = this.displayElement.querySelectorAll('.corpus-analyse-trigger')
for (element of elements) {
if (['analysing', 'prepared', 'start analysis'].includes(status)) {
if (['BUILT', 'STARTING_ANALYSIS_SESSION', 'RUNNING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'].includes(status)) {
element.classList.remove('disabled');
} else {
element.classList.add('disabled');
}
}
elements = this.displayElement.querySelectorAll('.build-corpus-trigger');
elements = this.displayElement.querySelectorAll('.corpus-build-trigger');
for (element of elements) {
if (status === 'unprepared' && Object.values(app.users[this.userId].corpora[this.corpusId].files).length > 0) {
if (status === 'UNPREPARED' && Object.values(app.users[this.userId].corpora[this.corpusId].files).length > 0) {
element.classList.remove('disabled');
} else {
element.classList.add('disabled');
}
}
elements = this.displayElement.querySelectorAll('.status');
elements = this.displayElement.querySelectorAll('.corpus-status');
for (element of elements) {
element.dataset.status = status;
element.dataset.corpusStatus = status;
}
elements = this.displayElement.querySelectorAll('.status-spinner');
elements = this.displayElement.querySelectorAll('.corpus-status-spinner');
for (element of elements) {
if (['submitted', 'queued', 'running', 'canceling', 'start analysis', 'stop analysis'].includes(status)) {
if (['SUBMITTED', 'QUEUED', 'BUILDING', 'STARTING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'].includes(status)) {
element.classList.remove('hide');
} else {
element.classList.add('hide');

View File

@ -57,23 +57,21 @@ class JobDisplay extends RessourceDisplay {
let element;
let elements;
this.setElements(this.displayElement.querySelectorAll('.job-status'), status);
elements = this.displayElement.querySelectorAll('.status');
elements = this.displayElement.querySelectorAll('.job-status');
for (element of elements) {
element.dataset.status = status;
element.dataset.jobStatus = status;
}
elements = this.displayElement.querySelectorAll('.status-spinner');
elements = this.displayElement.querySelectorAll('.job-status-spinner');
for (element of elements) {
if (['complete', 'failed'].includes(status)) {
if (['COMPLETED', 'FAILED'].includes(status)) {
element.classList.add('hide');
} else {
element.classList.remove('hide');
}
}
elements = this.displayElement.querySelectorAll('.restart-job-trigger');
elements = this.displayElement.querySelectorAll('.job-restart-trigger');
for (element of elements) {
if (['complete', 'failed'].includes(status)) {
if (['COMPLETED', 'FAILED'].includes(status)) {
element.classList.remove('hide');
} else {
element.classList.add('hide');

View File

@ -5,31 +5,31 @@ class CorpusFileList extends RessourceList {
<td><span class="filename"></span></td>
<td><span class="author"></span></td>
<td><span class="title"></span></td>
<td><span class="publishing_year"></span></td>
<td><span class="publishing-year"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating service-color darken tooltipped waves-effect waves-light" data-action="download" data-position="top" data-service="corpus-analysis" data-tooltip="View"><i class="material-icons">file_download</i></a>
<a class="action-button btn-floating service-color darken tooltipped waves-effect waves-light" data-action="view" data-position="top" data-service="corpus-analysis" data-tooltip="View"><i class="material-icons">send</i></a>
<a class="action-button btn-floating tooltipped nopaque-service-color darken waves-effect waves-light" data-action="download" data-position="top" data-service="corpus-analysis" data-tooltip="View"><i class="material-icons">file_download</i></a>
<a class="action-button btn-floating tooltipped nopaque-service-color darken waves-effect waves-light" data-action="view" data-position="top" data-service="corpus-analysis" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim(),
ressourceMapper: corpusFile => {
return {
id: corpusFile.id,
author: corpusFile.author,
creationDate: corpusFile.creation_date,
filename: corpusFile.filename,
publishingYear: corpusFile.publishing_year,
title: corpusFile.title
'id': corpusFile.id,
'author': corpusFile.author,
'creation-date': corpusFile.creation_date,
'filename': corpusFile.filename,
'publishing-year': corpusFile.publishing_year,
'title': corpusFile.title
};
},
sortValueName: 'creationDate',
sortValueName: 'creation-date',
valueNames: [
{data: ['id']},
{data: ['creationDate']},
{data: ['creation-date']},
'author',
'filename',
'publishingYear',
'publishing-year',
'title'
]
};
@ -125,7 +125,7 @@ class CorpusFileList extends RessourceList {
re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)/(author|filename|publishing_year|title)$`);
if (re.test(operation.path)) {
[match, corpusFileId, valueName] = operation.path.match(re);
this.replace(corpusFileId, valueName, operation.value);
this.replace(corpusFileId, valueName.replace('_', '-'), operation.value);
}
break;
default:

View File

@ -2,29 +2,29 @@ class CorpusList extends RessourceList {
static options = {
item: `
<tr class="hoverable">
<td><a class="btn-floating disabled"><i class="material-icons service-color darken" data-service="corpus-analysis">book</i></a></td>
<td><a class="btn-floating disabled"><i class="material-icons nopaque-service-color darken" data-service="corpus-analysis">book</i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
<td><span class="status badge new nopaque-corpus-status-color nopaque-corpus-status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating service-color darken tooltipped waves-effect waves-light" data-action="view" data-position="top" data-service="corpus-analysis" data-tooltip="View"><i class="material-icons">send</i></a>
<a class="action-button btn-floating nopaque-service-color darken tooltipped waves-effect waves-light" data-action="view" data-position="top" data-service="corpus-analysis" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim(),
ressourceMapper: corpus => {
return {
id: corpus.id,
creationDate: corpus.creation_date,
description: corpus.description,
status: corpus.status,
title: corpus.title
'id': corpus.id,
'creation-date': corpus.creation_date,
'description': corpus.description,
'status': corpus.status,
'title': corpus.title
};
},
sortValueName: 'creationDate',
sortValueName: 'creation-date',
valueNames: [
{data: ['id']},
{data: ['creationDate']},
{name: 'status', attr: 'data-status'},
{data: ['creation-date']},
{name: 'status', attr: 'data-corpus-status'},
'description',
'title'
]

View File

@ -10,13 +10,17 @@ class JobInputList extends RessourceList {
`.trim(),
ressourceMapper: jobInput => {
return {
id: jobInput.id,
creationDate: jobInput.creation_date,
filename: jobInput.filename
'id': jobInput.id,
'creation-date': jobInput.creation_date,
'filename': jobInput.filename
};
},
sortValueName: 'creationDate',
valueNames: [{data: ['id']}, {data: ['creationDate']}, 'filename']
sortValueName: 'creation-date',
valueNames: [
{data: ['id']},
{data: ['creation-date']},
'filename'
]
};

View File

@ -1,36 +1,36 @@
class JobList extends RessourceList {
static options = {
item: `
<tr class="hoverable service-color lighten">
<td><a class="btn-floating disabled"><i class="nopaque-icons service-color darken serviceDuplicate1 service-icon"></i></a></td>
<tr class="hoverable nopaque-service-color lighten">
<td><a class="btn-floating disabled"><i class="service-1 nopaque-icon nopaque-service-color darken nopaque-service-icon"></i></a></td>
<td><b class="title"></b><br><i class="description"></i></td>
<td><span class="badge new status status-color status-text" data-badge-caption=""></span></td>
<td><span class="status badge new nopaque-job-status-color nopaque-job-status-text" data-badge-caption=""></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating serviceDuplicate2 service-color darken tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
<a class="service-2 action-button btn-floating nopaque-service-color darken tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
</td>
</tr>
`.trim(),
ressourceMapper: job => {
return {
id: job.id,
creationDate: job.creation_date,
description: job.description,
service: job.service,
serviceDuplicate1: job.service,
serviceDuplicate2: job.service,
status: job.status,
title: job.title
'id': job.id,
'creation-date': job.creation_date,
'description': job.description,
'service': job.service,
'service-1': job.service,
'service-2': job.service,
'status': job.status,
'title': job.title
};
},
sortValueName: 'creationDate',
sortValueName: 'creation-date',
valueNames: [
{data: ['id']},
{data: ['creationDate']},
{data: ['creation-date']},
{data: ['service']},
{name: 'serviceDuplicate1', attr: 'data-service'},
{name: 'serviceDuplicate2', attr: 'data-service'},
{name: 'status', attr: 'data-status'},
{name: 'service-1', attr: 'data-service'},
{name: 'service-2', attr: 'data-service'},
{name: 'status', attr: 'data-job-status'},
'description',
'title'
]

View File

@ -11,16 +11,16 @@ class JobResultList extends RessourceList {
`.trim(),
ressourceMapper: jobResult => {
return {
id: jobResult.id,
creationDate: jobResult.creation_date,
description: jobResult.description,
filename: jobResult.filename
'id': jobResult.id,
'creation-date': jobResult.creation_date,
'description': jobResult.description,
'filename': jobResult.filename
};
},
sortValueName: 'creationDate',
sortValueName: 'creation-date',
valueNames: [
{data: ['id']},
{data: ['creationDate']},
{data: ['creation-date']},
'description',
'filename'
]

View File

@ -3,7 +3,7 @@ class QueryResultList extends RessourceList {
item: `
<tr class="hoverable">
<td><b class="title"></b><br><i class="description"></i><br></td>
<td><span class="corpus_title"></span><br><span class="query"></span></td>
<td><span class="corpus-title"></span><br><span class="query"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
<a class="action-button btn-floating tooltipped waves-effect waves-light" data-action="view" data-position="top" data-tooltip="View"><i class="material-icons">send</i></a>
@ -12,19 +12,19 @@ class QueryResultList extends RessourceList {
`.trim(),
ressourceMapper: queryResult => {
return {
id: queryResult.id,
corpusTitle: queryResult.corpus_title,
creationDate: queryResult.creation_date,
description: queryResult.description,
query: queryResult.query,
title: queryResult.title
'id': queryResult.id,
'corpus-title': queryResult.corpus_title,
'creation-date': queryResult.creation_date,
'description': queryResult.description,
'query': queryResult.query,
'title': queryResult.title
};
},
sortValueName: 'creationDate',
sortValueName: 'creation-date',
valueNames: [
{data: ['id']},
{data: ['creationDate']},
'corpusTitle',
{data: ['creation-date']},
'corpus-title',
'description',
'query',
'title'
@ -118,7 +118,7 @@ class QueryResultList extends RessourceList {
re = new RegExp(`^/users/${this.userId}/query_results/([A-Za-z0-9]*)/(corpus_title|description|query|title)$`);
if (re.test(operation.path)) {
[match, queryResultId, valueName] = operation.path.match(re);
this.replace(queryResultId, valueName, operation.value);
this.replace(queryResultId, valueName.replace('_', '-'), operation.value);
}
break;
default:

View File

@ -2,10 +2,10 @@ class UserList extends RessourceList {
static options = {
item: `
<tr class="hoverable">
<td><span class="idDuplicate"></span></td>
<td><span class="id-1"></span></td>
<td><span class="username"></span></td>
<td><span class="email"></span></td>
<td><span class="last_seen"></span></td>
<td><span class="last-seen"></span></td>
<td><span class="role"></span></td>
<td class="right-align">
<a class="action-button btn-floating red tooltipped waves-effect waves-light" data-action="delete" data-position="top" data-tooltip="Delete"><i class="material-icons">delete</i></a>
@ -16,21 +16,22 @@ class UserList extends RessourceList {
`.trim(),
ressourceMapper: user => {
return {
id: user.id,
idDuplicate: user.id,
username: user.username,
email: user.email,
last_seen: new Date(user.last_seen).toLocaleString("en-US"),
role: user.role.name
'id': user.id,
'id-1': user.id,
'username': user.username,
'email': user.email,
'last-seen': new Date(user.last_seen).toLocaleString("en-US"),
'member-since': user.member_since,
'role': user.role.name
};
},
sortValueName: 'memberSince',
sortValueName: 'member-since',
valueNames: [
{data: ['id']},
{data: ['memberSince']},
{data: ['member-since']},
'email',
'idDuplicate',
'last_seen',
'id-1',
'last-seen',
'role',
'username'
]

View File

@ -32,14 +32,24 @@
} %}
{% set status = {
'unprepared': '#9e9e9e',
'submitted': '#9e9e9e',
'queued': '#2196f3',
'running': '#ffc107',
'complete': '#4caf50',
'failed': '#f44336',
'prepared': '#4caf50',
'start analysis': '#2196f3',
'analysing': '#4caf50',
'stop analysis': '#ff5722'
'corpus': {
'UNPREPARED': '#9e9e9e',
'QUEUED': '#2196f3',
'BUILDING': '#ffc107',
'BUILT': '#4caf50',
'FAILED': '#f44336',
'STARTING_ANALYSIS_SESSION': '#2196f3',
'RUNNING_ANALYSIS_SESSION': '#4caf50',
'CANCELING_ANALYSIS_SESSION': '#ff5722'
},
'job': {
'INITIALIZING': '#9e9e9e',
'SUBMITTED': '#9e9e9e',
'QUEUED': '#2196f3',
'RUNNING': '#ffc107',
'CANCELING': '#ff5722',
'CANCELED': '#ff5722',
'COMPLETED': '#4caf50',
'FAILED': '#f44336'
}
} %}

View File

@ -10,14 +10,14 @@
<li><a href="{{ url_for('main.news') }}"><i class="material-icons left">email</i>News</a></li>
<li><a href="#"><i class="material-icons">linear_scale</i>Workflow</a></li>
<li><a href="{{ url_for('main.dashboard') }}"><i class="material-icons">dashboard</i>Dashboard</a></li>
<li><a href="{{ url_for('main.dashboard', _anchor='corpora') }}" style="padding-left: 47px;"><i class="nopaque-icons">I</i>My Corpora</a></li>
<li><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" style="padding-left: 47px;"><i class="nopaque-icons">J</i>My Jobs</a></li>
<li><a href="{{ url_for('main.dashboard', _anchor='corpora') }}" style="padding-left: 47px;"><i class="nopaque-icon">I</i>My Corpora</a></li>
<li><a href="{{ url_for('main.dashboard', _anchor='jobs') }}" style="padding-left: 47px;"><i class="nopaque-icon">J</i>My Jobs</a></li>
<li><div class="divider"></div></li>
<li><a class="subheader">Processes & Services</a></li>
<li class="service-color service-color-border border-darken" data-service="file-setup" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='file-setup') }}"><i class="nopaque-icons service-icon" data-service="file-setup"></i>File setup</a></li>
<li class="service-color service-color-border border-darken" data-service="tesseract-ocr" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='tesseract-ocr') }}"><i class="nopaque-icons service-icon" data-service="tesseract-ocr"></i>OCR</a></li>
<li class="service-color service-color-border border-darken" data-service="spacy-nlp" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='spacy-nlp') }}"><i class="nopaque-icons service-icon" data-service="spacy-nlp"></i>NLP</a></li>
<li class="service-color service-color-border border-darken" data-service="corpus-analysis" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='corpus-analysis') }}"><i class="nopaque-icons service-icon" data-service="corpus-analysis"></i>Corpus analysis</a></li>
<li class="nopaque-service-color nopaque-service-color-border border-darken" data-service="file-setup" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='file-setup') }}"><i class="nopaque-icon nopaque-service-icon" data-service="file-setup"></i>File setup</a></li>
<li class="nopaque-service-color nopaque-service-color-border border-darken" data-service="tesseract-ocr" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='tesseract-ocr') }}"><i class="nopaque-icon nopaque-service-icon" data-service="tesseract-ocr"></i>OCR</a></li>
<li class="nopaque-service-color nopaque-service-color-border border-darken" data-service="spacy-nlp" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='spacy-nlp') }}"><i class="nopaque-icon nopaque-service-icon" data-service="spacy-nlp"></i>NLP</a></li>
<li class="nopaque-service-color nopaque-service-color-border border-darken" data-service="corpus-analysis" style="border-left: 10px solid; margin-top: 5px;"><a href="{{ url_for('services.service', service='corpus-analysis') }}"><i class="nopaque-icon nopaque-service-icon" data-service="corpus-analysis"></i>Corpus analysis</a></li>
<li><div class="divider"></div></li>
<li><a class="subheader">Account</a></li>
<li><a href="{{ url_for('settings.index') }}"><i class="material-icons">settings</i>Settings</a></li>

View File

@ -45,29 +45,31 @@
main .tabs .indicator {background-color: {{ colors['baseline']['secondary'] }};}
{% for service in colors['services'] %}
.service-scheme[data-service="{{ service }}"] {background-color: {{ colors['services'][service]['lighten'] }};}
.service-scheme[data-service="{{ service }}"] .btn, .service-scheme[data-service="{{ service }}"] .btn-small, .service-scheme[data-service="{{ service }}"] .btn-large, .service-scheme[data-service="{{ service }}"] .btn-floating {background-color: {{ colors['services'][service]['darken'] }};}
.service-scheme[data-service="{{ service }}"] .btn:hover, .service-scheme[data-service="{{ service }}"] .btn-large:hover, .service-scheme[data-service="{{ service }}"] .btn-small:hover, .service-scheme[data-service="{{ service }}"] .btn-floating:hover {background-color: {{ colors['services'][service]['base'] }};}
.service-scheme[data-service="{{ service }}"] .pagination li.active {background-color: {{ colors['services'][service]['darken'] }};}
.service-scheme[data-service="{{ service }}"] .table-of-contents a.active {border-color: {{ colors['services'][service]['darken'] }};}
.service-scheme[data-service="{{ service }}"] .tabs .tab a {color: inherit;}
.service-scheme[data-service="{{ service }}"] .tabs .tab.disabled a, .service-scheme[data-service="{{ service }}"] .tabs .tab.disabled a:hover {color: {{ colors['services'][service]['darken'] }}28;}
.service-scheme[data-service="{{ service }}"] .tabs .tab a:hover {color: {{ colors['services'][service]['darken'] }};}
.service-scheme[data-service="{{ service }}"] .tabs .tab a.active, .service-scheme[data-service="{{ service }}"] .tabs .tab a:focus.active {color: {{ colors['services'][service]['darken'] }}; background-color: {{ colors['services'][service]['darken'] }}28;}
.service-scheme[data-service="{{ service }}"] .tabs .indicator {background-color: {{ colors['services'][service]['darken'] }};}
.nopaque-service-scheme[data-service="{{ service }}"] {background-color: {{ colors['services'][service]['lighten'] }};}
.nopaque-service-scheme[data-service="{{ service }}"] .btn, .nopaque-service-scheme[data-service="{{ service }}"] .btn-small, .nopaque-service-scheme[data-service="{{ service }}"] .btn-large, .nopaque-service-scheme[data-service="{{ service }}"] .btn-floating {background-color: {{ colors['services'][service]['darken'] }};}
.nopaque-service-scheme[data-service="{{ service }}"] .btn:hover, .nopaque-service-scheme[data-service="{{ service }}"] .btn-large:hover, .nopaque-service-scheme[data-service="{{ service }}"] .btn-small:hover, .nopaque-service-scheme[data-service="{{ service }}"] .btn-floating:hover {background-color: {{ colors['services'][service]['base'] }};}
.nopaque-service-scheme[data-service="{{ service }}"] .pagination li.active {background-color: {{ colors['services'][service]['darken'] }};}
.nopaque-service-scheme[data-service="{{ service }}"] .table-of-contents a.active {border-color: {{ colors['services'][service]['darken'] }};}
.nopaque-service-scheme[data-service="{{ service }}"] .tabs .tab a {color: inherit;}
.nopaque-service-scheme[data-service="{{ service }}"] .tabs .tab.disabled a, .nopaque-service-scheme[data-service="{{ service }}"] .tabs .tab.disabled a:hover {color: {{ colors['services'][service]['darken'] }}28;}
.nopaque-service-scheme[data-service="{{ service }}"] .tabs .tab a:hover {color: {{ colors['services'][service]['darken'] }};}
.nopaque-service-scheme[data-service="{{ service }}"] .tabs .tab a.active, .nopaque-service-scheme[data-service="{{ service }}"] .tabs .tab a:focus.active {color: {{ colors['services'][service]['darken'] }}; background-color: {{ colors['services'][service]['darken'] }}28;}
.nopaque-service-scheme[data-service="{{ service }}"] .tabs .indicator {background-color: {{ colors['services'][service]['darken'] }};}
.service-color[data-service="{{ service }}"] {background-color: {{ colors['services'][service]['base'] }} !important;}
.service-color-text[data-service="{{ service }}"] {color: {{ colors['services'][service]['base'] }} !important;}
.service-color-border[data-service="{{ service }}"] {border-color: {{ colors['services'][service]['base'] }} !important;}
.service-color[data-service="{{ service }}"].darken {background-color: {{ colors['services'][service]['darken'] }} !important;}
.service-color-text[data-service="{{ service }}"].text-darken {color: {{ colors['services'][service]['darken'] }} !important;}
.service-color-border[data-service="{{ service }}"].border-darken {border-color: {{ colors['services'][service]['darken'] }} !important;}
.service-color[data-service="{{ service }}"].lighten {background-color: {{ colors['services'][service]['lighten'] }} !important;}
.service-color-text[data-service="{{ service }}"].text-lighten {color: {{ colors['services'][service]['lighten'] }} !important;}
.service-color-border[data-service="{{ service }}"].border-lighten {border-color: {{ colors['services'][service]['lighten'] }} !important;}
.nopaque-service-color[data-service="{{ service }}"] {background-color: {{ colors['services'][service]['base'] }} !important;}
.nopaque-service-color-text[data-service="{{ service }}"] {color: {{ colors['services'][service]['base'] }} !important;}
.nopaque-service-color-border[data-service="{{ service }}"] {border-color: {{ colors['services'][service]['base'] }} !important;}
.nopaque-service-color[data-service="{{ service }}"].darken {background-color: {{ colors['services'][service]['darken'] }} !important;}
.nopaque-service-color-text[data-service="{{ service }}"].text-darken {color: {{ colors['services'][service]['darken'] }} !important;}
.nopaque-service-color-border[data-service="{{ service }}"].border-darken {border-color: {{ colors['services'][service]['darken'] }} !important;}
.nopaque-service-color[data-service="{{ service }}"].lighten {background-color: {{ colors['services'][service]['lighten'] }} !important;}
.nopaque-service-color-text[data-service="{{ service }}"].text-lighten {color: {{ colors['services'][service]['lighten'] }} !important;}
.nopaque-service-color-border[data-service="{{ service }}"].border-lighten {border-color: {{ colors['services'][service]['lighten'] }} !important;}
{% endfor %}
{% for status in colors['status'] %}
.status-color[data-status="{{ status }}"] {background-color: {{ colors['status'][status] }} !important;}
{% for status_type in colors['status'] %}
{% for status in colors['status'][status_type] %}
.nopaque-{{ status_type }}-status-color[data-{{status_type}}-status="{{ status }}"] {background-color: {{ colors['status'][status_type][status] }} !important;}
{% endfor %}
{% endfor %}
</style>

View File

@ -6,54 +6,93 @@
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">Edit user</h1>
<h1 id="title">{{ title }}</h1>
</div>
<div class="col s12 m4">
<h2>{{ user.username }}</h2>
<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,</p>
<a class="waves-effect waves-light btn" href="{{ url_for('.user', user_id=user.hashid) }}"><i class="material-icons left">arrow_back</i>Back to user administration</a>
</div>
<div class="col s12 m8">
<div class="card">
<div class="col s12">
<form method="POST">
{{ edit_general_settings_form.hidden_tag() }}
<div class="card">
<div class="card-content">
{{ form.hidden_tag() }}
{{ wtf.render_field(form.username, data_length='64', material_icon='account_circle') }}
{{ wtf.render_field(form.email, class_='validate', material_icon='email', type='email') }}
{{ wtf.render_field(form.role, material_icon='swap_vert') }}
<span class="card-title">General settings</span>
{{ wtf.render_field(edit_general_settings_form.username, data_length='64', material_icon='person') }}
{{ wtf.render_field(edit_general_settings_form.email, data_length='254', material_icon='email') }}
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(edit_general_settings_form.submit, material_icon='send') }}
</div>
</div>
</form>
</div>
</form>
<form method="POST">
{{ edit_interface_settings_form.hidden_tag() }}
<div class="card">
<div class="card-content">
<span class="card-title">Interface settings</span>
<div class="row">
<div class="col s12"><p>&nbsp;</p></div>
<div class="col s1">
<p><i class="material-icons">brightness_3</i></p>
</div>
<div class="col s8">
<p>{{ form.dark_mode.label.text }}</p>
<p>{{ edit_interface_settings_form.dark_mode.label.text }}</p>
<p class="light">Enable dark mode to ease your eyes.</p>
</div>
<div class="col s3 right-align">
<div class="switch">
<label>
{{ form.dark_mode() }}
{{ edit_interface_settings_form.dark_mode() }}
<span class="lever"></span>
</label>
</div>
</div>
<div class="col s12"><p>&nbsp;</p></div>
<div class="col s12 divider"></div>
</div>
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(edit_interface_settings_form.submit, material_icon='send') }}
</div>
</div>
</div>
</form>
<form method="POST">
{{ edit_notification_settings_form.hidden_tag() }}
<div class="card">
<div class="card-content">
<span class="card-title">Notification settings</span>
{{ wtf.render_field(edit_notification_settings_form.job_status_mail_notification_level, material_icon='notifications') }}
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(edit_notification_settings_form.submit, material_icon='send') }}
</div>
</div>
</div>
</form>
<form method="POST">
{{ admin_edit_user_form.hidden_tag() }}
<div class="card">
<div class="card-content">
<span class="card-title">Administrator settings</span>
{{ wtf.render_field(admin_edit_user_form.role, material_icon='swap_vert') }}
<div class="row">
<div class="col s12"><p>&nbsp;</p></div>
<div class="col s1">
<p><i class="material-icons">check</i></p>
</div>
<div class="col s8">
<p>{{ form.confirmed.label.text }}</p>
<p>{{ admin_edit_user_form.confirmed.label.text }}</p>
<p class="light">Change confirmation status manually.</p>
</div>
<div class="col s3 right-align">
<div class="switch">
<label>
{{ form.confirmed() }}
{{ admin_edit_user_form.confirmed() }}
<span class="lever"></span>
</label>
</div>
@ -61,11 +100,38 @@
</div>
</div>
<div class="card-action right-align">
{{ wtf.render_field(form.submit, material_icon='send') }}
{{ wtf.render_field(admin_edit_user_form.submit, material_icon='send') }}
</div>
</div>
</form>
<div class="card">
<div class="card-content">
<span class="card-title">Delete account</span>
<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>
<div class="card-action right-align">
<a href="#delete-account-modal" class="btn modal-trigger red waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</div>
</div>
{% endblock page_content %}
{% block modals %}
{{ super() }}
<div class="modal" id="delete-account-modal">
<div class="modal-content">
<h4>Confirm deletion</h4>
<p>Do you really want to delete your account and all associated data? All associated corpora, jobs and files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a href="{{ url_for('.delete_user', user_id=user.id) }}" class="btn red waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
{% endblock modals %}

View File

@ -2,7 +2,7 @@
{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block page_content %}
<div class="container">

View File

@ -2,7 +2,7 @@
{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block page_content %}
<div class="container">

View File

@ -1,11 +1,11 @@
{% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis" id="corpus-analysis-app-container"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="corpus-analysis" id="corpus-analysis-app-container"{% endblock main_attribs %}
{% block page_content %}
<ul class="row tabs no-autoinit" id="corpus-analysis-app-extension-tabs">
<li class="tab col s3"><a class="active" href="#corpus-analysis-app-overview"><i class="nopaque-icons service-icon left" data-service="corpus-analysis"></i>Corpus analysis</a></li>
<li class="tab col s3"><a class="active" href="#corpus-analysis-app-overview"><i class="nopaque-icon nopaque-service-icon left" data-service="corpus-analysis"></i>Corpus analysis</a></li>
<li class="tab col s3"><a href="#concordance-extension-container"><i class="material-icons left">list_alt</i>Concordance</a></li>
<li class="tab col s3"><a href="#reader-extension-container"><i class="material-icons left">chrome_reader_mode</i>Reader</a></li>
</ul>

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %}
{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block page_content %}
<div class="container">
@ -14,8 +14,8 @@
<div class="col s4 m3 l2 right-align">
<p>&nbsp;</p>
<p>&nbsp;</p>
<span class="chip status status-color status-text white-text"></span>
<div class="active preloader-wrapper small status-spinner">
<span class="chip corpus-status nopaque-corpus-status-color nopaque-corpus-status-text white-text"></span>
<div class="active preloader-wrapper small corpus-status-spinner">
<div class="spinner-layer spinner-blue-only">
<div class="circle-clipper left">
<div class="circle"></div>
@ -31,7 +31,7 @@
</div>
</div>
<div class="card service-color-border border-darken" data-service="corpus-analysis" style="border-top: 10px solid">
<div class="card nopaque-service-color-border border-darken" data-service="corpus-analysis" style="border-top: 10px solid">
<div class="card-content">
<div class="row">
<div class="col s12">
@ -64,9 +64,9 @@
</div>
</div>
<div class="card-action right-align">
<a class="analyse-corpus-trigger btn disabled waves-effect waves-light" href="{{ url_for('corpora.analyse_corpus', corpus_id=corpus.id) }}"><i class="material-icons left">search</i>Analyze</a>
<a class="btn build-corpus-trigger disabled waves-effect waves-light" href="{{ url_for('corpora.build_corpus', corpus_id=corpus.id) }}"><i class="nopaque-icons left">K</i>Build</a>
<a class="btn disabled export-corpus-trigger waves-effect waves-light"><i class="material-icons left">import_export</i>Export</a>
<a class="btn corpus-analyse-trigger disabled waves-effect waves-light" href="{{ url_for('corpora.analyse_corpus', corpus_id=corpus.id) }}"><i class="material-icons left">search</i>Analyze</a>
<a class="btn corpus-build-trigger disabled waves-effect waves-light" href="{{ url_for('corpora.build_corpus', corpus_id=corpus.id) }}"><i class="nopaque-icon left">K</i>Build</a>
<a class="btn disabled export-corpus-trigger waves-effect waves-light" href="{{ url_for('corpora.export_corpus', corpus_id=corpus.id) }}"><i class="material-icons left">import_export</i>Export</a>
<a class="btn modal-trigger red waves-effect waves-light" data-target="delete-corpus-modal"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>

View File

@ -2,7 +2,7 @@
{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block page_content %}
<div class="container">

View File

@ -2,7 +2,7 @@
{% from "corpora/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block page_content %}
<div class="container">

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %}
{% from "jobs/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block main_attribs %} class="service-scheme" data-service="{{ job.service }}"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="{{ job.service }}"{% endblock main_attribs %}
{% block page_content %}
<div class="container">
@ -9,13 +9,13 @@
<div class="col s12" data-job-id="{{ job.hashid }}" data-user-id="{{ job.user.hashid }}" id="job-display">
<div class="row">
<div class="col s8 m9 l10">
<h1 id="title"><i style="font-size: inherit;" class="nopaque-icons service-icon" data-service="{{ job.service }}"></i> <span class="job-title"></span></h1>
<h1 id="title"><i style="font-size: inherit;" class="nopaque-icon nopaque-service-icon" data-service="{{ job.service }}"></i> <span class="job-title"></span></h1>
</div>
<div class="col s4 m3 l2 right-align">
<p>&nbsp;</p>
<p>&nbsp;</p>
<span class="chip status status-text status-color white-text"></span>
<div class="active preloader-wrapper small status-spinner">
<span class="chip job-status nopaque-job-status-text nopaque-job-status-color white-text"></span>
<div class="active preloader-wrapper small job-status-spinner">
<div class="spinner-layer spinner-blue-only">
<div class="circle-clipper left">
<div class="circle"></div>
@ -31,7 +31,7 @@
</div>
</div>
<div class="card service-color-border border-darken" data-service="{{ job.service }}" style="border-top: 10px solid">
<div class="card nopaque-service-color-border border-darken" data-service="{{ job.service }}" style="border-top: 10px solid">
<div class="card-content">
<div class="row">
<div class="col s12">

View File

@ -40,8 +40,8 @@
<ul class="pagination"></ul>
</div>
<div class="card-action right-align">
<a class="waves-effect waves-light btn" href="{{ url_for('corpora.import_corpus') }}"><i class="material-icons right">import_export</i>Import Corpus</a>
<a class="waves-effect waves-light btn" href="{{ url_for('corpora.add_corpus') }}">New corpus<i class="material-icons right">add</i></a>
<a class="btn disabled waves-effect waves-light" href="{{ url_for('corpora.import_corpus') }}"><i class="material-icons right">import_export</i>Import Corpus</a>
<a class="btn waves-effect waves-light" href="{{ url_for('corpora.add_corpus') }}">New corpus<i class="material-icons right">add</i></a>
</div>
</div>
</div>
@ -116,36 +116,36 @@
<div class="card-panel center-align hoverable">
<br>
<a href="{{ url_for('services.service', service='file-setup') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
<i class="nopaque-icons service-color darken service-icon" data-service="file-setup"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="file-setup"></i>
</a>
<br><br>
<p class="service-color-text darken" data-service="file-setup"><b>File setup</b></p>
<p class="nopaque-service-color-text darken" data-service="file-setup"><b>File setup</b></p>
<p class="light">Digital copies of text based research data (books, letters, etc.) often comprise various files and formats. nopaque converts and merges those files to facilitate further processing.</p>
<a href="{{ url_for('services.service', service='file-setup') }}" class="waves-effect waves-light btn service-color darken" data-service="file-setup">Create Job</a>
<a href="{{ url_for('services.service', service='file-setup') }}" class="waves-effect waves-light btn nopaque-service-color darken" data-service="file-setup">Create Job</a>
</div>
</div>
<div class="col s12 m4">
<div class="card-panel center-align hoverable">
<br>
<a href="{{ url_for('services.service', service='tesseract-ocr') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
<i class="nopaque-icons service-color darken service-icon" data-service="tesseract-ocr" style="font-size: 2.5rem;"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="tesseract-ocr" style="font-size: 2.5rem;"></i>
</a>
<br><br>
<p class="service-color-text darken" data-service="tesseract-ocr"><b>Optical Character Recognition</b></p>
<p class="nopaque-service-color-text darken" data-service="tesseract-ocr"><b>Optical Character Recognition</b></p>
<p class="light">nopaque converts your image data like photos or scans into text data through a process called OCR. This step enables you to proceed with further computational analysis of your documents.</p>
<a href="{{ url_for('services.service', service='tesseract-ocr') }}" class="waves-effect waves-light btn service-color darken" data-service="tesseract-ocr">Create Job</a>
<a href="{{ url_for('services.service', service='tesseract-ocr') }}" class="waves-effect waves-light btn nopaque-service-color darken" data-service="tesseract-ocr">Create Job</a>
</div>
</div>
<div class="col s12 m4">
<div class="card-panel center-align hoverable">
<br>
<a href="{{ url_for('services.service', service='spacy-nlp') }}" class="btn-floating btn-large waves-effect waves-light" style="transform: scale(2);">
<i class="nopaque-icons service-color darken service-icon" data-service="spacy-nlp" style="font-size: 2.5rem;"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="spacy-nlp" style="font-size: 2.5rem;"></i>
</a>
<br><br>
<p class="service-color-text darken" data-service="spacy-nlp"><b>Natural Language Processing</b></p>
<p class="nopaque-service-color-text darken" data-service="spacy-nlp"><b>Natural Language Processing</b></p>
<p class="light">By means of computational linguistic data processing (tokenization, lemmatization, part-of-speech tagging and named-entity recognition) nopaque extracts additional information from your text.</p>
<a href="{{ url_for('services.service', service='spacy-nlp') }}" class="waves-effect waves-light btn service-color darken" data-service="spacy-nlp">Create Job</a>
<a href="{{ url_for('services.service', service='spacy-nlp') }}" class="waves-effect waves-light btn nopaque-service-color darken" data-service="spacy-nlp">Create Job</a>
</div>
</div>
</div>

View File

@ -21,22 +21,22 @@
<div class="col s12">
<div class="row">
<div class="col s12 m6 l3 center-align">
<i class="large nopaque-icons secondary-color-text">A</i><br>
<i class="large nopaque-icon secondary-color-text">A</i><br>
<b class="primary-color-text">Speeds up your work</b>
<p class="light">All tools provided by nopaque are carefully selected to provide a complete tool suite without being held up by compatibility issues.</p>
</div>
<div class="col s12 m6 l3 center-align">
<i class="large nopaque-icons secondary-color-text">B</i><br>
<i class="large nopaque-icon secondary-color-text">B</i><br>
<b class="primary-color-text">Cloud infrastructure</b>
<p class="light">All computational work is processed within nopaques cloud infrastructure. You don't need to install any software. Great, right?</p>
</div>
<div class="col s12 m6 l3 center-align">
<i class="large nopaque-icons secondary-color-text">C</i><br>
<i class="large nopaque-icon secondary-color-text">C</i><br>
<b class="primary-color-text">User friendly</b>
<p class="light">You can start right away without having to read mile-long manuals. All services come with default settings that make it easy for you to just get going. Also great, right?</p>
</div>
<div class="col s12 m6 l3 center-align">
<i class="large nopaque-icons secondary-color-text">D</i><br>
<i class="large nopaque-icon secondary-color-text">D</i><br>
<b class="primary-color-text">Meshing processes</b>
<p class="light">No matter where you step in, nopaque facilitates and accompanies your research. Its workflow perfectly ties in with your research process.</p>
</div>
@ -77,34 +77,34 @@
<div class="row">
<div class="col s12 m6 l3 center-align">
<a href="{{ url_for('services.service', service='file-setup') }}" class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
<i class="nopaque-icons service-color darken service-icon" data-service="file-setup"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="file-setup"></i>
</a>
<br><br>
<p class="service-color-text text-darken" data-service="file-setup"><b>File setup</b></p>
<p class="nopaque-service-color-text text-darken" data-service="file-setup"><b>File setup</b></p>
<p class="light">Digital copies of text based research data (books, letters, etc.) often comprise various files and formats. nopaque converts and merges those files to facilitate further processing and the application of other services.</p>
</div>
<div class="col s12 m6 l3 center-align">
<a href="{{ url_for('services.service', service='tesseract-ocr') }}" class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
<i class="nopaque-icons service-color darken service-icon" data-service="tesseract-ocr"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="tesseract-ocr"></i>
</a>
<br><br>
<p class="service-color-text text-darken" data-service="tesseract-ocr"><b>Optical Character Recognition</b></p>
<p class="nopaque-service-color-text text-darken" data-service="tesseract-ocr"><b>Optical Character Recognition</b></p>
<p class="light">nopaque converts your image data like photos or scans into text data through OCR making it machine readable. This step enables you to proceed with further computational analysis of your documents.</p>
</div>
<div class="col s12 m6 l3 center-align">
<a href="{{ url_for('services.service', service='nlp') }}" class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
<i class="nopaque-icons service-color darken service-icon" data-service="nlp"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="nlp"></i>
</a>
<br><br>
<p class="service-color-text text-darken" data-service="nlp"><b>Natural Language Processing</b></p>
<p class="nopaque-service-color-text text-darken" data-service="nlp"><b>Natural Language Processing</b></p>
<p class="light">By means of computational linguistic data processing (tokenization, lemmatization, part-of-speech tagging and named-entity recognition) nopaque extracts additional information from your text.</p>
</div>
<div class="col s12 m6 l3 center-align">
<a href="{{ url_for('services.service', service='corpus_analysis') }}" class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
<i class="nopaque-icons service-color darken service-icon" data-service="corpus-analysis"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="corpus-analysis"></i>
</a>
<br><br>
<p class="service-color-text text-darken" data-service="corpus-analysis"><b>Corpus analysis</b></p>
<p class="nopaque-service-color-text text-darken" data-service="corpus-analysis"><b>Corpus analysis</b></p>
<p class="light">nopaque lets you create and upload as many text corpora as you want. It makes use of CQP Query Language, which allows for complex search requests with the aid of metadata and NLP tags.</p>
</div>
</div>
@ -192,8 +192,8 @@
<!-- <div class="col s12 m10"> -->
<div class="col s12">
<h3>Introduction video</h3>
<div class="responsive-youtube-video-container">
<iframe class="responsive-youtube-video" src="https://www.youtube-nocookie.com/embed/KPGZSW_7SWk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<div style="position: relative; width: 100%; height: 0; padding-bottom: 56.25%;">
<iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://www.youtube-nocookie.com/embed/KPGZSW_7SWk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
{% extends "base.html.j2" %}
{% from "services/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block page_content %}
<div class="container">
@ -13,7 +13,7 @@
<div class="col s12 m3 push-m9">
<div class="center-align">
<a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light" style="transform: scale(2);">
<i class="nopaque-icons service-color darken service-icon" data-service="corpus-analysis"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="corpus-analysis"></i>
</a>
</div>
</div>

View File

@ -2,7 +2,7 @@
{% from "services/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="file-setup"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="file-setup"{% endblock main_attribs %}
{% block page_content %}
<div class="container">
@ -16,13 +16,13 @@
<p class="hide-on-small-only">&nbsp;</p>
<p class="hide-on-small-only">&nbsp;</p>
<a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
<i class="nopaque-icons service-color darken service-icon" data-service="file-setup"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="file-setup"></i>
</a>
</div>
</div>
<div class="col s12 m9 pull-m3">
<div class="card service-color-border border-darken" data-service="file-setup" style="border-top: 10px solid;">
<div class="card nopaque-service-color-border border-darken" data-service="file-setup" style="border-top: 10px solid;">
<div class="card-content">
<div class="row">
<div class="col s12">

View File

@ -2,7 +2,7 @@
{% from "services/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="spacy-nlp"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="spacy-nlp"{% endblock main_attribs %}
{% block page_content %}
<div class="container">
@ -16,13 +16,13 @@
<p class="hide-on-small-only">&nbsp;</p>
<p class="hide-on-small-only">&nbsp;</p>
<a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
<i class="nopaque-icons service-color darken service-icon" data-service="spacy-nlp"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="spacy-nlp"></i>
</a>
</div>
</div>
<div class="col s12 m9 pull-m3">
<div class="card service-color-border border-darken" data-service="spacy-nlp" style="border-top: 10px solid;">
<div class="card nopaque-service-color-border border-darken" data-service="spacy-nlp" style="border-top: 10px solid;">
<div class="card-content">
<div class="row">
<div class="col s12 m6">

View File

@ -2,7 +2,7 @@
{% from "services/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="tesseract-ocr"{% endblock main_attribs %}
{% block main_attribs %} class="nopaque-service-scheme" data-service="tesseract-ocr"{% endblock main_attribs %}
{% block page_content %}
<div class="container">
@ -16,13 +16,13 @@
<p class="hide-on-small-only">&nbsp;</p>
<p class="hide-on-small-only">&nbsp;</p>
<a class="btn-floating btn-large btn-scale-x2 waves-effect waves-light">
<i class="nopaque-icons service-color darken service-icon" data-service="tesseract-ocr"></i>
<i class="nopaque-icon nopaque-service-color darken nopaque-service-icon" data-service="tesseract-ocr"></i>
</a>
</div>
</div>
<div class="col s12 m9 pull-m3">
<div class="card service-color-border border-darken" data-service="tesseract-ocr" style="border-top: 10px solid;">
<div class="card nopaque-service-color-border border-darken" data-service="tesseract-ocr" style="border-top: 10px solid;">
<div class="card-content">
<div class="row">
<div class="col s12">

View File

@ -1,12 +1,6 @@
{% set breadcrumbs %}
<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
{% if request.path == url_for('settings.index') %}
<li class="tab"><a{%if request.path == url_for('settings.index') %} class="active"{% endif %} href="{{ url_for('settings.index') }}" target="_self">Settings</a></li>
<li class="tab disabled"><i class="material-icons">navigate_next</i></li>
{% if request.path == url_for('settings.change_password') %}
<li class="tab"><a class="active" href="{{ url_for('settings.change_password') }}" target="_self">Change password</a></li>
{% elif request.path == url_for('settings.edit_general_settings') %}
<li class="tab"><a class="active" href="{{ url_for('settings.edit_general_settings') }}" target="_self">Edit general settings</a></li>
{% elif request.path == url_for('settings.edit_notification_settings') %}
<li class="tab"><a class="active" href="{{ url_for('settings.edit_notification_settings') }}" target="_self">Edit notification settings</a></li>
{% endif %}
{% endset %}

View File

@ -1,5 +0,0 @@
<div class="collection">
<a href="{{ url_for('.edit_general_settings') }}" class="collection-item{%if request.path == url_for('.edit_general_settings') %} active{% endif %}">Edit general settings</a>
<a href="{{ url_for('.change_password') }}" class="collection-item{%if request.path == url_for('.change_password') %} active{% endif %}">Change password</a>
<a href="{{ url_for('.edit_notification_settings') }}" class="collection-item{%if request.path == url_for('.edit_notification_settings') %} active{% endif %}">Edit notification settings</a>
</div>

View File

@ -1,36 +0,0 @@
{% extends "base.html.j2" %}
{% from "settings/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">{{ title }}</h1>
</div>
<div class="col s12 m4">
{% include 'settings/_menu.html.j2' %}
</div>
<div class="col s12 m8">
<div class="card">
<form enctype="multipart/form-data" method="POST">
<div class="card-content">
<span class="card-title">{{ title }}</span>
{{ form.hidden_tag() }}
{{ wtf.render_field(form.password, material_icon='vpn_key') }}
{{ wtf.render_field(form.new_password, material_icon='vpn_key') }}
{{ wtf.render_field(form.new_password2, material_icon='vpn_key') }}
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(form.submit, material_icon='send') }}
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock page_content %}

View File

@ -1,81 +0,0 @@
{% extends "base.html.j2" %}
{% from "settings/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">{{ title }}</h1>
</div>
<div class="col s12 m4">
{% include 'settings/_menu.html.j2' %}
</div>
<div class="col s12 m8">
<div class="card">
<form enctype="multipart/form-data" method="POST">
<div class="card-content">
<span class="card-title">{{ title }}</span>
{{ form.hidden_tag() }}
{{ wtf.render_field(form.username, data_length='64', material_icon='person') }}
{{ wtf.render_field(form.email, data_length='254', material_icon='email') }}
<div class="row">
<div class="col s12"><p>&nbsp;</p></div>
<div class="col s1">
<p><i class="material-icons">brightness_3</i></p>
</div>
<div class="col s8">
<p>{{ form.dark_mode.label.text }}</p>
<p class="light">Enable dark mode to ease your eyes.</p>
</div>
<div class="col s3 right-align">
<div class="switch">
<label>
{{ form.dark_mode() }}
<span class="lever"></span>
</label>
</div>
</div>
</div>
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(form.submit, material_icon='send') }}
</div>
</div>
</form>
</div>
<div class="card">
<div class="card-content">
<span class="card-title">Delete account</span>
<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>
<div class="card-action right-align">
<a href="#delete-account-modal" class="btn modal-trigger red waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</div>
</div>
</div>
{% endblock page_content %}
{% block modals %}
{{ super() }}
<div class="modal" id="delete-account-modal">
<div class="modal-content">
<h4>Confirm deletion</h4>
<p>Do you really want to delete your account and all associated data? All associated corpora, jobs and files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a href="{{ url_for('.delete') }}" class="btn red waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
{% endblock modals %}

View File

@ -1,35 +0,0 @@
{% extends "base.html.j2" %}
{% from "settings/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">{{ title }}</h1>
</div>
<div class="col s12 m4">
{% include 'settings/_menu.html.j2' %}
</div>
<div class="col s12 m8">
<div class="card">
<form enctype="multipart/form-data" method="POST">
<div class="card-content">
<span class="card-title">{{ title }}</span>
{{ form.hidden_tag() }}
{{ wtf.render_field(form.job_status_mail_notifications, material_icon='notifications') }}
{{ wtf.render_field(form.job_status_site_notifications, material_icon='feedback') }}
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(form.submit, material_icon='send') }}
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock page_content %}

View File

@ -0,0 +1,123 @@
{% extends "base.html.j2" %}
{% from "settings/_breadcrumbs.html.j2" import breadcrumbs with context %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12">
<h1 id="title">{{ title }}</h1>
</div>
<div class="col s12">
<form method="POST">
{{ edit_general_settings_form.hidden_tag() }}
<div class="card">
<div class="card-content">
<span class="card-title">General settings</span>
{{ wtf.render_field(edit_general_settings_form.username, data_length='64', material_icon='person') }}
{{ wtf.render_field(edit_general_settings_form.email, data_length='254', material_icon='email') }}
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(edit_general_settings_form.submit, material_icon='send') }}
</div>
</div>
</form>
</div>
</form>
<form method="POST">
{{ edit_interface_settings_form.hidden_tag() }}
<div class="card">
<div class="card-content">
<span class="card-title">Interface settings</span>
<div class="row">
<div class="col s12"><p>&nbsp;</p></div>
<div class="col s1">
<p><i class="material-icons">brightness_3</i></p>
</div>
<div class="col s8">
<p>{{ edit_interface_settings_form.dark_mode.label.text }}</p>
<p class="light">Enable dark mode to ease your eyes.</p>
</div>
<div class="col s3 right-align">
<div class="switch">
<label>
{{ edit_interface_settings_form.dark_mode() }}
<span class="lever"></span>
</label>
</div>
</div>
</div>
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(edit_interface_settings_form.submit, material_icon='send') }}
</div>
</div>
</div>
</form>
<form method="POST">
{{ edit_notification_settings_form.hidden_tag() }}
<div class="card">
<div class="card-content">
<span class="card-title">Notification settings</span>
{{ wtf.render_field(edit_notification_settings_form.job_status_mail_notification_level, material_icon='notifications') }}
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(edit_notification_settings_form.submit, material_icon='send') }}
</div>
</div>
</div>
</form>
<form method="POST">
{{ change_password_form.hidden_tag() }}
<div class="card">
<div class="card-content">
<span class="card-title">Change Password</span>
{{ 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_confirmation, material_icon='vpn_key') }}
</div>
<div class="card-action">
<div class="right-align">
{{ wtf.render_field(change_password_form.submit, material_icon='send') }}
</div>
</div>
</div>
</form>
<div class="card">
<div class="card-content">
<span class="card-title">Delete account</span>
<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>
<div class="card-action right-align">
<a href="#delete-account-modal" class="btn modal-trigger red waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
</div>
</div>
{% endblock page_content %}
{% block modals %}
{{ super() }}
<div class="modal" id="delete-account-modal">
<div class="modal-content">
<h4>Confirm deletion</h4>
<p>Do you really want to delete your account and all associated data? All associated corpora, jobs and files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a href="#!" class="btn modal-close waves-effect waves-light">Cancel</a>
<a href="{{ url_for('.delete') }}" class="btn red waves-effect waves-light"><i class="material-icons left">delete</i>Delete</a>
</div>
</div>
{% endblock modals %}

View File

@ -1,7 +1,7 @@
<p>Dear <b>{{ job.user.username }}</b>,</p>
<p>The status of your Job "<b>{{ job.title }}</b>" has changed!</p>
<p>It is now <b>{{ job.status }}</b>!</p>
<p>It is now <b>{{ job.status.name.lower() }}</b>!</p>
<p>You can access your Job here: <a href="{{ url_for('jobs.job', job_id=job.id) }}">{{ url_for('jobs.job', job_id=job.id) }}</a></p>

View File

@ -1,7 +1,7 @@
Dear {{ job.user.username }},
The status of your Job "{{ job.title }}" has changed!
It is now {{ job.status }}!
It is now {{ job.status.name.lower() }}!
You can access your Job here: {{ url_for('jobs.job', job_id=job.id) }}

View File

@ -1 +1 @@
Generic single-database configuration.
Single-database configuration for Flask.

View File

@ -11,7 +11,7 @@
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
@ -34,6 +34,11 @@ level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)

View File

@ -3,8 +3,7 @@ from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from flask import current_app
from alembic import context
@ -21,10 +20,10 @@ logger = logging.getLogger('alembic.env')
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option(
'sqlalchemy.url', current_app.config.get(
'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.get_engine().url).replace(
'%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
@ -72,11 +71,7 @@ def run_migrations_online():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
connectable = current_app.extensions['migrate'].db.get_engine()
with connectable.connect() as connection:
context.configure(

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: da9fd175af8c
Revision ID: 097aae1f02d7
Revises:
Create Date: 2019-10-23 08:11:02.741683
Create Date: 2022-02-08 10:02:03.748588
"""
from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'da9fd175af8c'
revision = '097aae1f02d7'
down_revision = None
branch_labels = None
depends_on = None
@ -29,67 +29,108 @@ def upgrade():
op.create_index(op.f('ix_roles_default'), 'roles', ['default'], unique=False)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.Column('confirmed', sa.Boolean(), nullable=True),
sa.Column('email', sa.String(length=254), nullable=True),
sa.Column('last_seen', sa.DateTime(), nullable=True),
sa.Column('member_since', sa.DateTime(), nullable=True),
sa.Column('password_hash', sa.String(length=128), nullable=True),
sa.Column('registration_date', sa.DateTime(), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.Column('token', sa.String(length=32), nullable=True),
sa.Column('token_expiration', sa.DateTime(), nullable=True),
sa.Column('username', sa.String(length=64), nullable=True),
sa.Column('is_dark', sa.Boolean(), nullable=True),
sa.Column('setting_dark_mode', sa.Boolean(), nullable=True),
sa.Column('setting_job_status_mail_notification_level', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_token'), 'users', ['token'], unique=True)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
op.create_table('corpora',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('creation_date', sa.DateTime(), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('last_edited_date', sa.DateTime(), nullable=True),
sa.Column('status', sa.Integer(), nullable=True),
sa.Column('title', sa.String(length=32), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('num_analysis_sessions', sa.Integer(), nullable=True),
sa.Column('num_tokens', sa.Integer(), nullable=True),
sa.Column('archive_file', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('jobs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('creation_date', sa.DateTime(), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('end_date', sa.DateTime(), nullable=True),
sa.Column('mem_mb', sa.Integer(), nullable=True),
sa.Column('n_cores', sa.Integer(), nullable=True),
sa.Column('service', sa.String(length=64), nullable=True),
sa.Column('service_args', sa.String(length=255), nullable=True),
sa.Column('service_version', sa.String(length=16), nullable=True),
sa.Column('status', sa.String(length=16), nullable=True),
sa.Column('status', sa.Integer(), nullable=True),
sa.Column('title', sa.String(length=32), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tesseract_ocr_models',
sa.Column('creation_date', sa.DateTime(), nullable=True),
sa.Column('filename', sa.String(length=255), nullable=True),
sa.Column('last_edited_date', sa.DateTime(), nullable=True),
sa.Column('mimetype', sa.String(length=255), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('compatible_service_versions', sa.String(length=255), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('publisher', sa.String(length=128), nullable=True),
sa.Column('publishing_year', sa.Integer(), nullable=True),
sa.Column('title', sa.String(length=64), nullable=True),
sa.Column('version', sa.String(length=16), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('corpus_files',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('creation_date', sa.DateTime(), nullable=True),
sa.Column('filename', sa.String(length=255), nullable=True),
sa.Column('dir', sa.String(length=255), nullable=True),
sa.Column('last_edited_date', sa.DateTime(), nullable=True),
sa.Column('mimetype', sa.String(length=255), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('corpus_id', sa.Integer(), nullable=True),
sa.Column('address', sa.String(length=255), nullable=True),
sa.Column('author', sa.String(length=255), nullable=True),
sa.Column('booktitle', sa.String(length=255), nullable=True),
sa.Column('chapter', sa.String(length=255), nullable=True),
sa.Column('editor', sa.String(length=255), nullable=True),
sa.Column('institution', sa.String(length=255), nullable=True),
sa.Column('journal', sa.String(length=255), nullable=True),
sa.Column('pages', sa.String(length=255), nullable=True),
sa.Column('publisher', sa.String(length=255), nullable=True),
sa.Column('publishing_year', sa.Integer(), nullable=True),
sa.Column('school', sa.String(length=255), nullable=True),
sa.Column('title', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['corpus_id'], ['corpora.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('job_inputs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('creation_date', sa.DateTime(), nullable=True),
sa.Column('filename', sa.String(length=255), nullable=True),
sa.Column('dir', sa.String(length=255), nullable=True),
sa.Column('last_edited_date', sa.DateTime(), nullable=True),
sa.Column('mimetype', sa.String(length=255), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('job_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('job_results',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('creation_date', sa.DateTime(), nullable=True),
sa.Column('filename', sa.String(length=255), nullable=True),
sa.Column('dir', sa.String(length=255), nullable=True),
sa.Column('last_edited_date', sa.DateTime(), nullable=True),
sa.Column('mimetype', sa.String(length=255), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('job_id', sa.Integer(), nullable=True),
sa.Column('job_input_id', sa.Integer(), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ),
sa.ForeignKeyConstraint(['job_input_id'], ['job_inputs.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
@ -100,9 +141,11 @@ def downgrade():
op.drop_table('job_results')
op.drop_table('job_inputs')
op.drop_table('corpus_files')
op.drop_table('tesseract_ocr_models')
op.drop_table('jobs')
op.drop_table('corpora')
op.drop_index(op.f('ix_users_username'), table_name='users')
op.drop_index(op.f('ix_users_token'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')
op.drop_index(op.f('ix_roles_default'), table_name='roles')

View File

@ -1,30 +0,0 @@
"""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

@ -1,28 +0,0 @@
"""empty message
Revision ID: 0aa38a7973c5
Revises: 1210adfe1e34
Create Date: 2019-11-06 09:33:46.296653
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0aa38a7973c5'
down_revision = '1210adfe1e34'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpora', sa.Column('analysis_ip', sa.String(length=16), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('corpora', 'analysis_ip')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 0d7aed934679
Revises: b15366b25bea
Create Date: 2020-06-30 13:57:48.782173
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0d7aed934679'
down_revision = 'b15366b25bea'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('result_files', sa.Column('corpus_metadata', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('result_files', 'corpus_metadata')
# ### end Alembic commands ###

View File

@ -1,30 +0,0 @@
"""empty message
Revision ID: 10a92d8f4616
Revises: 4638e6509e13
Create Date: 2020-05-12 11:24:46.022674
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '10a92d8f4616'
down_revision = '4638e6509e13'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('notifications_data', sa.Column('notified_on', sa.String(length=16), nullable=True))
op.drop_column('notifications_data', 'notified')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('notifications_data', sa.Column('notified', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.drop_column('notifications_data', 'notified_on')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 1210adfe1e34
Revises: abf60427ff84
Create Date: 2019-11-04 12:54:39.389263
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1210adfe1e34'
down_revision = 'abf60427ff84'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpora', sa.Column('status', sa.String(length=16), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('corpora', 'status')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 318074622d14
Revises: 0d7aed934679
Create Date: 2020-06-30 14:00:18.968769
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '318074622d14'
down_revision = '0d7aed934679'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('result_files', 'corpus_metadata')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('result_files', sa.Column('corpus_metadata', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View File

@ -1,30 +0,0 @@
"""empty message
Revision ID: 33ec4d09b4ca
Revises: 4cf5e5606a83
Create Date: 2020-07-13 09:07:19.297185
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '33ec4d09b4ca'
down_revision = '4cf5e5606a83'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('query_results', sa.Column('description', sa.String(length=255), nullable=True))
op.add_column('query_results', sa.Column('title', sa.String(length=32), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('query_results', 'title')
op.drop_column('query_results', 'description')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 389bcf564726
Revises: 318074622d14
Create Date: 2020-06-30 14:03:33.384379
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '389bcf564726'
down_revision = '318074622d14'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('result_files', sa.Column('corpus_metadata', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('result_files', 'corpus_metadata')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 3d9a20b8b26c
Revises: 421ba4373e50
Create Date: 2020-05-14 09:30:56.266381
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3d9a20b8b26c'
down_revision = '421ba4373e50'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('notification_email_data', sa.Column('notify_status', sa.String(length=16), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('notification_email_data', 'notify_status')
# ### end Alembic commands ###

View File

@ -1,33 +0,0 @@
"""empty message
Revision ID: 421ba4373e50
Revises: 5ba6786a847e
Create Date: 2020-05-14 08:35:47.367125
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '421ba4373e50'
down_revision = '5ba6786a847e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('notification_email_data',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('job_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('notification_email_data')
# ### end Alembic commands ###

View File

@ -1,38 +0,0 @@
"""empty message
Revision ID: 4638e6509e13
Revises: 471aa04c1a92
Create Date: 2020-05-12 06:42:04.475585
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4638e6509e13'
down_revision = '471aa04c1a92'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('notifications_data', sa.Column('notified', sa.Boolean(), nullable=True))
op.drop_column('notifications_data', 'notified_on_queued')
op.drop_column('notifications_data', 'notified_on_running')
op.drop_column('notifications_data', 'notified_on_submitted')
op.drop_column('notifications_data', 'notified_on_complete')
op.drop_column('notifications_data', 'notified_on_canceling')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('notifications_data', sa.Column('notified_on_canceling', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('notifications_data', sa.Column('notified_on_complete', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('notifications_data', sa.Column('notified_on_submitted', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('notifications_data', sa.Column('notified_on_running', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('notifications_data', sa.Column('notified_on_queued', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.drop_column('notifications_data', 'notified')
# ### end Alembic commands ###

View File

@ -1,38 +0,0 @@
"""empty message
Revision ID: 471aa04c1a92
Revises: 62233e0cb2c7
Create Date: 2020-05-11 14:07:12.934869
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '471aa04c1a92'
down_revision = '62233e0cb2c7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('notifications_data',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('job_id', sa.Integer(), nullable=True),
sa.Column('notified_on_submitted', sa.Boolean(), nullable=True),
sa.Column('notified_on_queued', sa.Boolean(), nullable=True),
sa.Column('notified_on_running', sa.Boolean(), nullable=True),
sa.Column('notified_on_complete', sa.Boolean(), nullable=True),
sa.Column('notified_on_canceling', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('notifications_data')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 4886241e0f5d
Revises: 3d9a20b8b26c
Create Date: 2020-05-14 11:58:08.498454
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4886241e0f5d'
down_revision = '3d9a20b8b26c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('notification_email_data', sa.Column('creation_date', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('notification_email_data', 'creation_date')
# ### end Alembic commands ###

View File

@ -1,36 +0,0 @@
"""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 ###

View File

@ -1,35 +0,0 @@
"""empty message
Revision ID: 4cf5e5606a83
Revises: e256f5cac75d
Create Date: 2020-07-13 08:30:57.369850
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4cf5e5606a83'
down_revision = 'e256f5cac75d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('query_results',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('filename', sa.String(length=255), nullable=True),
sa.Column('query_metadata', sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('query_results')
# ### end Alembic commands ###

View File

@ -1,30 +0,0 @@
"""empty message
Revision ID: 55d2b1a82ba9
Revises: 8b2e0d43384a
Create Date: 2021-04-14 12:10:08.675542
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '55d2b1a82ba9'
down_revision = '8b2e0d43384a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('jobs', 'mem_mb')
op.drop_column('jobs', 'n_cores')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('jobs', sa.Column('n_cores', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('jobs', sa.Column('mem_mb', sa.INTEGER(), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View File

@ -1,42 +0,0 @@
"""empty message
Revision ID: 5ba6786a847e
Revises: 10a92d8f4616
Create Date: 2020-05-12 13:15:56.265610
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5ba6786a847e'
down_revision = '10a92d8f4616'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('notification_data',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('job_id', sa.Integer(), nullable=True),
sa.Column('notified_on', sa.String(length=16), nullable=True),
sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.drop_table('notifications_data')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('notifications_data',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('job_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('notified_on', sa.VARCHAR(length=16), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], name='notifications_data_job_id_fkey'),
sa.PrimaryKeyConstraint('id', name='notifications_data_pkey')
)
op.drop_table('notification_data')
# ### end Alembic commands ###

View File

@ -1,34 +0,0 @@
"""empty message
Revision ID: 62233e0cb2c7
Revises: 68772b6560c3
Create Date: 2020-05-04 09:42:25.408403
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '62233e0cb2c7'
down_revision = '68772b6560c3'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpora', sa.Column('current_nr_of_tokens', sa.BigInteger(), nullable=True))
op.add_column('corpora', sa.Column('max_nr_of_tokens', sa.BigInteger(), nullable=True))
op.drop_column('corpora', 'analysis_container_name')
op.drop_column('corpora', 'analysis_container_ip')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpora', sa.Column('analysis_container_ip', sa.VARCHAR(length=16), autoincrement=False, nullable=True))
op.add_column('corpora', sa.Column('analysis_container_name', sa.VARCHAR(length=32), autoincrement=False, nullable=True))
op.drop_column('corpora', 'max_nr_of_tokens')
op.drop_column('corpora', 'current_nr_of_tokens')
# ### end Alembic commands ###

View File

@ -1,30 +0,0 @@
"""empty message
Revision ID: 6227310c2112
Revises: ded5a37f8a7b
Create Date: 2020-01-30 09:28:06.770159
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6227310c2112'
down_revision = 'ded5a37f8a7b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('job_results_job_input_id_fkey', 'job_results', type_='foreignkey')
op.drop_column('job_results', 'job_input_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('job_results', sa.Column('job_input_id', sa.INTEGER(), autoincrement=False, nullable=True))
op.create_foreign_key('job_results_job_input_id_fkey', 'job_results', 'job_inputs', ['job_input_id'], ['id'])
# ### end Alembic commands ###

View File

@ -1,30 +0,0 @@
"""empty message
Revision ID: 66253783654f
Revises: 7378391345fa
Create Date: 2020-04-27 08:26:19.772088
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '66253783654f'
down_revision = '7378391345fa'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('setting_dark_mode', sa.Boolean(), nullable=True))
op.drop_column('users', 'is_dark')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('is_dark', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.drop_column('users', 'setting_dark_mode')
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 68772b6560c3
Revises: 49a42c69e523
Create Date: 2020-04-28 07:47:40.495698
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '68772b6560c3'
down_revision = '49a42c69e523'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpora', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('corpora', 'last_edited_date')
# ### end Alembic commands ###

View File

@ -1,50 +0,0 @@
"""empty message
Revision ID: 68ed092ffe5e
Revises: be010d5d708d
Create Date: 2021-11-24 15:33:16.258600
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '68ed092ffe5e'
down_revision = 'be010d5d708d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpus_files', sa.Column('creation_date', sa.DateTime(), nullable=True))
op.add_column('corpus_files', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
op.add_column('corpus_files', sa.Column('mimetype', sa.String(length=255), nullable=True))
op.add_column('job_inputs', sa.Column('creation_date', sa.DateTime(), nullable=True))
op.add_column('job_inputs', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
op.add_column('job_inputs', sa.Column('mimetype', sa.String(length=255), nullable=True))
op.add_column('job_results', sa.Column('creation_date', sa.DateTime(), nullable=True))
op.add_column('job_results', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
op.add_column('job_results', sa.Column('mimetype', sa.String(length=255), nullable=True))
op.add_column('query_results', sa.Column('creation_date', sa.DateTime(), nullable=True))
op.add_column('query_results', sa.Column('last_edited_date', sa.DateTime(), nullable=True))
op.add_column('query_results', sa.Column('mimetype', sa.String(length=255), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('query_results', 'mimetype')
op.drop_column('query_results', 'last_edited_date')
op.drop_column('query_results', 'creation_date')
op.drop_column('job_results', 'mimetype')
op.drop_column('job_results', 'last_edited_date')
op.drop_column('job_results', 'creation_date')
op.drop_column('job_inputs', 'mimetype')
op.drop_column('job_inputs', 'last_edited_date')
op.drop_column('job_inputs', 'creation_date')
op.drop_column('corpus_files', 'mimetype')
op.drop_column('corpus_files', 'last_edited_date')
op.drop_column('corpus_files', 'creation_date')
# ### end Alembic commands ###

View File

@ -1,59 +0,0 @@
"""empty message
Revision ID: 6c2227f1cc77
Revises: befe5326787e
Create Date: 2020-12-02 08:50:45.880062
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '6c2227f1cc77'
down_revision = 'befe5326787e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('notification_data')
op.drop_table('notification_email_data')
op.drop_column('corpus_files', 'dir')
op.drop_column('job_inputs', 'dir')
op.drop_column('job_results', 'dir')
op.drop_column('jobs', 'secure_filename')
op.alter_column('roles', 'permissions',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('roles', 'permissions',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
op.add_column('jobs', sa.Column('secure_filename', sa.VARCHAR(length=32), autoincrement=False, nullable=True))
op.add_column('job_results', sa.Column('dir', sa.VARCHAR(length=255), autoincrement=False, nullable=True))
op.add_column('job_inputs', sa.Column('dir', sa.VARCHAR(length=255), autoincrement=False, nullable=True))
op.add_column('corpus_files', sa.Column('dir', sa.VARCHAR(length=255), autoincrement=False, nullable=True))
op.create_table('notification_email_data',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('job_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('notify_status', sa.VARCHAR(length=16), autoincrement=False, nullable=True),
sa.Column('creation_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], name='notification_email_data_job_id_fkey'),
sa.PrimaryKeyConstraint('id', name='notification_email_data_pkey')
)
op.create_table('notification_data',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('job_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('notified_on', sa.VARCHAR(length=16), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], name='notification_data_job_id_fkey'),
sa.PrimaryKeyConstraint('id', name='notification_data_pkey')
)
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 7378391345fa
Revises: 6227310c2112
Create Date: 2020-02-17 12:29:17.851954
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7378391345fa'
down_revision = '6227310c2112'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('jobs', sa.Column('secure_filename', sa.String(length=32), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('jobs', 'secure_filename')
# ### end Alembic commands ###

View File

@ -1,32 +0,0 @@
"""empty message
Revision ID: 776761fb7466
Revises: 0aa38a7973c5
Create Date: 2019-11-07 08:34:01.676055
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '776761fb7466'
down_revision = '0aa38a7973c5'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpora', sa.Column('analysis_container_ip', sa.String(length=16), nullable=True))
op.add_column('corpora', sa.Column('analysis_container_name', sa.String(length=32), nullable=True))
op.drop_column('corpora', 'analysis_ip')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpora', sa.Column('analysis_ip', sa.VARCHAR(length=16), autoincrement=False, nullable=True))
op.drop_column('corpora', 'analysis_container_name')
op.drop_column('corpora', 'analysis_container_ip')
# ### end Alembic commands ###

View File

@ -1,42 +0,0 @@
"""empty message
Revision ID: 790ce9512e75
Revises: 6c2227f1cc77
Create Date: 2021-01-25 11:13:36.953269
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '790ce9512e75'
down_revision = '6c2227f1cc77'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('corpora', 'current_nr_of_tokens',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
op.alter_column('corpora', 'max_nr_of_tokens',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('corpora', 'max_nr_of_tokens',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
op.alter_column('corpora', 'current_nr_of_tokens',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
# ### end Alembic commands ###

View File

@ -1,28 +0,0 @@
"""empty message
Revision ID: 8b2e0d43384a
Revises: 790ce9512e75
Create Date: 2021-01-25 11:18:45.256537
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8b2e0d43384a'
down_revision = '790ce9512e75'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('corpora', 'max_nr_of_tokens')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpora', sa.Column('max_nr_of_tokens', sa.INTEGER(), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View File

@ -1,50 +0,0 @@
"""empty message
Revision ID: 9d21b228d353
Revises: 33ec4d09b4ca
Create Date: 2020-07-15 08:58:59.062442
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9d21b228d353'
down_revision = '33ec4d09b4ca'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('corpus_files', 'author',
existing_type=sa.VARCHAR(length=64),
type_=sa.String(length=255),
existing_nullable=True)
op.alter_column('corpus_files', 'title',
existing_type=sa.VARCHAR(length=64),
type_=sa.String(length=255),
existing_nullable=True)
op.alter_column('roles', 'permissions',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('roles', 'permissions',
existing_type=sa.BigInteger(),
type_=sa.INTEGER(),
existing_nullable=True)
op.alter_column('corpus_files', 'title',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=64),
existing_nullable=True)
op.alter_column('corpus_files', 'author',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=64),
existing_nullable=True)
# ### end Alembic commands ###

View File

@ -1,30 +0,0 @@
"""empty message
Revision ID: a4b3cf4ab098
Revises: c384d7b3268a
Create Date: 2021-09-23 13:14:16.227784
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a4b3cf4ab098'
down_revision = 'c384d7b3268a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpora', sa.Column('num_tokens', sa.Integer(), nullable=True))
op.drop_column('corpora', 'current_nr_of_tokens')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpora', sa.Column('current_nr_of_tokens', sa.INTEGER(), autoincrement=False, nullable=True))
op.drop_column('corpora', 'num_tokens')
# ### end Alembic commands ###

View File

@ -1,32 +0,0 @@
"""empty message
Revision ID: abf60427ff84
Revises: da9fd175af8c
Create Date: 2019-10-28 14:43:39.691313
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'abf60427ff84'
down_revision = 'da9fd175af8c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('corpus_files', sa.Column('author', sa.String(length=64), nullable=True))
op.add_column('corpus_files', sa.Column('publishing_year', sa.Integer(), nullable=True))
op.add_column('corpus_files', sa.Column('title', sa.String(length=64), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('corpus_files', 'title')
op.drop_column('corpus_files', 'publishing_year')
op.drop_column('corpus_files', 'author')
# ### end Alembic commands ###

View File

@ -1,45 +0,0 @@
"""empty message
Revision ID: ad0d835fe5b1
Revises: 68ed092ffe5e
Create Date: 2022-01-18 16:23:45.673993
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ad0d835fe5b1'
down_revision = '68ed092ffe5e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('tesseract_ocr_models',
sa.Column('creation_date', sa.DateTime(), nullable=True),
sa.Column('filename', sa.String(length=255), nullable=True),
sa.Column('last_edited_date', sa.DateTime(), nullable=True),
sa.Column('mimetype', sa.String(length=255), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('compatible_service_versions', sa.String(length=255), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('publisher', sa.String(length=128), nullable=True),
sa.Column('publishing_year', sa.Integer(), nullable=True),
sa.Column('title', sa.String(length=64), nullable=True),
sa.Column('version', sa.String(length=16), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.add_column('job_results', sa.Column('description', sa.String(length=255), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('job_results', 'description')
op.drop_table('tesseract_ocr_models')
# ### end Alembic commands ###

Some files were not shown because too many files have changed in this diff Show More