diff --git a/app/admin/forms.py b/app/admin/forms.py
index eb0ed231bda9a9fb9e9b0602623a98aad965b5b4..30d994481c61e4cd38510454a828996e5766b8ac 100644
--- a/app/admin/forms.py
+++ b/app/admin/forms.py
@@ -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()
+        ]
diff --git a/app/admin/routes.py b/app/admin/routes.py
index 7979243d1d0b4db5e96bad9d5d7e28460ed3d7b0..79f05e191c6561616522c6e011e877f1f1cafb5c 100644
--- a/app/admin/routes.py
+++ b/app/admin/routes.py
@@ -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)
+    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))
+    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('Settings have been updated.')
+        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
+    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
+    )
diff --git a/app/api/auth.py b/app/api/auth.py
index fea4123b3fdc79bc4cc5d4905d7e9dd5992a6145..4c6a3dd959477e2dab3ee8adf0f939344371404d 100644
--- a/app/api/auth.py
+++ b/app/api/auth.py
@@ -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()
diff --git a/app/api/tokens.py b/app/api/tokens.py
index 2eebbfb716a387a6e4df0e37e6422d855e2cd1bf..e1b55527ddc3a41534b21a3344db52c1a5a5dcc0 100644
--- a/app/api/tokens.py
+++ b/app/api/tokens.py
@@ -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')
diff --git a/app/auth/forms.py b/app/auth/forms.py
index 8509bc448ec89c30fc7a3f758d291143e773454e..a418bc51a02b3b718699883ce0c5566793248637 100644
--- a/app/auth/forms.py
+++ b/app/auth/forms.py
@@ -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')
 
diff --git a/app/auth/routes.py b/app/auth/routes.py
index 3584225179afc684c42a4e4b0431cc1d4fbb81b7..045d0bafcc5efce3372b6a9bdb9f48b5d220d7d2 100644
--- a/app/auth/routes.py
+++ b/app/auth/routes.py
@@ -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
-                and request.endpoint
-                and request.blueprint != 'auth'
-                and request.endpoint != 'static'):
+        if (
+            not current_user.confirmed
+            and request.endpoint
+            and request.blueprint != 'auth'
+            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,19 +89,20 @@ 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,
-                'Confirm Your Account',
-                'auth/email/confirm',
-                token=token,
-                user=user
-            )
-            send(msg)
-            flash('A confirmation email has been sent to you by email.')
-            return redirect(url_for('.login'))
+        token = user.generate_confirmation_token()
+        msg = create_message(
+            user.email,
+            'Confirm Your Account',
+            'auth/email/confirm',
+            token=token,
+            user=user
+        )
+        send(msg)
+        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',
         form=form,
@@ -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
+    )
diff --git a/app/contribute/routes.py b/app/contribute/routes.py
index e0b43231c16d9634b8f2355a3481fb21b2452924..8bfd9bd84666d54b9449378030025a67944937b8 100644
--- a/app/contribute/routes.py
+++ b/app/contribute/routes.py
@@ -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
diff --git a/app/corpora/cqi_over_socketio/__init__.py b/app/corpora/cqi_over_socketio/__init__.py
index 3a358758405879116c0469fe236daf9a94864fc0..f1f5f8265007245a3a9e5ba2ba0228e48fbe620c 100644
--- a/app/corpora/cqi_over_socketio/__init__.py
+++ b/app/corpora/cqi_over_socketio/__init__.py
@@ -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()
diff --git a/app/corpora/forms.py b/app/corpora/forms.py
index c015d87ed1de852e7151293e885bb27d994e6823..9af4a5ce518c80e964c2c7a7eabce5367af2073e 100644
--- a/app/corpora/forms.py
+++ b/app/corpora/forms.py
@@ -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
 
 
diff --git a/app/corpora/query_results_routes.py b/app/corpora/query_results_routes.py
index 27402dc937f11b2e1950af173461bc7bea60656f..6c22e11efdcad32859216ee1d918f1b41f743718 100644
--- a/app/corpora/query_results_routes.py
+++ b/app/corpora/query_results_routes.py
@@ -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"))
 
diff --git a/app/corpora/query_results_tasks.py b/app/corpora/query_results_tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..653096b3a36e5a0c17d0af749bb760092d761a89
--- /dev/null
+++ b/app/corpora/query_results_tasks.py
@@ -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()
diff --git a/app/corpora/routes.py b/app/corpora/routes.py
index f6d95b542029fbb814459830dbca789e4c7af19b..fd0085eec2ec69384a504a6944411e6e5ed21eab 100644
--- a/app/corpora/routes.py
+++ b/app/corpora/routes.py
@@ -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,
-                               directory=os.path.dirname(corpus_file.path),
-                               filename=corpus_file.filename)
+    return send_from_directory(
+        as_attachment=True,
+        directory=os.path.dirname(corpus_file.path),
+        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))
diff --git a/app/corpora/tasks.py b/app/corpora/tasks.py
index c37ebd99d2f18c40b3a91e611eb945f5081e586c..c914a25a05053d1cc1ae879fbe0bd59d066447c9 100644
--- a/app/corpora/tasks.py
+++ b/app/corpora/tasks.py
@@ -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()
diff --git a/app/daemon/corpus_utils.py b/app/daemon/corpus_utils.py
index 3962582eb0d1347cbb5fe4d1933f09dfd59a487d..78e17f295fc929bd57d720737e10526813aab738 100644
--- a/app/daemon/corpus_utils.py
+++ b/app/daemon/corpus_utils.py
@@ -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(
diff --git a/app/daemon/job_utils.py b/app/daemon/job_utils.py
index c640f35cf8a671d571da5340ef34eaf6c6617cb0..e4797e264ac28de4d303011cac9e7359e3ffc33b 100644
--- a/app/daemon/job_utils.py
+++ b/app/daemon/job_utils.py
@@ -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(
diff --git a/app/events/socketio.py b/app/events/socketio.py
index 94b4c0c7d7c74ededd8e50addd1cb0a7a5ebaaff..e892ea9dfc1a381813ad4e97154395fa8b5e1c60 100644
--- a/app/events/socketio.py
+++ b/app/events/socketio.py
@@ -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
 
 
 ###############################################################################
diff --git a/app/events/sqlalchemy.py b/app/events/sqlalchemy.py
index b3362ca00d3e271864fb23902c33f6e3dbc4e00a..dd88a0fe7ad3cefae886afa79a8dd0d7db11cbc4 100644
--- a/app/events/sqlalchemy.py
+++ b/app/events/sqlalchemy.py
@@ -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]
-        # 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'
+        if isinstance(history.added[0], datetime):
+            # In order to be JSON serializable, DateTime attributes must be
+            # converted to a string
+            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(
diff --git a/app/jobs/routes.py b/app/jobs/routes.py
index 4acd7c47675efcd78a80e68fc4cc0bd3298f597a..ac484958282075e4e49950ee45f1b30ff3fe3987 100644
--- a/app/jobs/routes.py
+++ b/app/jobs/routes.py
@@ -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,
diff --git a/app/jobs/tasks.py b/app/jobs/tasks.py
index 9ba974e09d1ab5186bf91c0f638f907177e50e59..1738b0cd77099dd7ccde8e5189911ac90b73f465 100644
--- a/app/jobs/tasks.py
+++ b/app/jobs/tasks.py
@@ -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
diff --git a/app/main/routes.py b/app/main/routes.py
index a817362f06c46333bc92b81cf1bf07dc869e3d51..79f19318848adce0ecf0513663ae3d47b4f994cf 100644
--- a/app/main/routes.py
+++ b/app/main/routes.py
@@ -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')
diff --git a/app/models.py b/app/models.py
index d02b511ca51b945db51f43f459e0062f8c5ebd8d..034edc915cca75d0ca5acee999b9b9ad84d367f3 100644
--- a/app/models.py
+++ b/app/models.py
@@ -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))
diff --git a/app/query_results_models.py b/app/query_results_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..102d2825d1aa43ea6f9e151f402708dd4627c3c6
--- /dev/null
+++ b/app/query_results_models.py
@@ -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)
diff --git a/app/services/forms.py b/app/services/forms.py
index 0bebfb02db3496593c595ebc19922eebe052f144..4a16ad4a799ed89a18088ca009050b019f65f0e9 100644
--- a/app/services/forms.py
+++ b/app/services/forms.py
@@ -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):
diff --git a/app/services/routes.py b/app/services/routes.py
index d430e61e4ab2cef5f1f0715ed956ca2e2cd66ba3..7a8e520ebcff52d1f270753b351160c11e91a0a9 100644
--- a/app/services/routes.py
+++ b/app/services/routes.py
@@ -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
diff --git a/app/settings/forms.py b/app/settings/forms.py
index d402fad2ca3171df5e6f8c7485a6226089f56f73..03d7f243f20172f47617d1f31b516d0a8ac88c32 100644
--- a/app/settings/forms.py
+++ b/app/settings/forms.py
@@ -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(),
-                    Length(1, 64),
-                    Regexp(USERNAME_REGEX,
-                           message='Usernames must have only letters, numbers,'
-                                   ' dots or underscores')]
+        'Username',
+        validators=[
+            DataRequired(),
+            Length(1, 64),
+            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
+        ]
diff --git a/app/settings/routes.py b/app/settings/routes.py
index 9cc9960af6f2222745409904814f1b5989d9cc82..c59a55f2fc00f2388eb0e572d793fcfe211456bb 100644
--- a/app/settings/routes.py
+++ b/app/settings/routes.py
@@ -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('.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('.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'))
+    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'))
diff --git a/app/settings/tasks.py b/app/settings/tasks.py
index cc5d9e3693a4e2d9fff99d32c729083fd15e3c34..2bd82ca92c36f9030fa1a3e9ef7c572ef6d27231 100644
--- a/app/settings/tasks.py
+++ b/app/settings/tasks.py
@@ -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
diff --git a/app/static/css/nopaque.css b/app/static/css/nopaque.css
index ee5377e123f5f0e1ef825a04e44271f7fd6b6201..ce122980848f394e9acec54cf3de96b45bef03b0 100644
--- a/app/static/css/nopaque.css
+++ b/app/static/css/nopaque.css
@@ -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";}
 
-.hoverable {cursor: pointer;}
-.s-attr.chip .p-attr.chip {background-color: inherit;}
+.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";}
 
-.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%;
-}
+.hoverable {cursor: pointer;}
+.chip.s-attr .chip.p-attr {background-color: inherit;}
diff --git a/app/static/css/nopaque_icons.css b/app/static/css/nopaque_icons.css
index f3e497525df30c875a73cda5f3ddd7b40c8a9110..20d1c68cb533a2fa34cfcb6d33e1d35ecc7a780f 100644
--- a/app/static/css/nopaque_icons.css
+++ b/app/static/css/nopaque_icons.css
@@ -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 */
diff --git a/app/static/js/App.js b/app/static/js/App.js
index 3761be8cf7a5503b5d5500649cf67b0d24fef7f1..6a188e6305854b3c7c58f7a95a261830d9947982 100644
--- a/app/static/js/App.js
+++ b/app/static/js/App.js
@@ -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>';
diff --git a/app/static/js/JobStatusNotifier.js b/app/static/js/JobStatusNotifier.js
index 0ae834cdfe81c877ce9039351c22c1d413c23409..fb258b04c08929000005ff2e6bd6a5fe4f37d815 100644
--- a/app/static/js/JobStatusNotifier.js
+++ b/app/static/js/JobStatusNotifier.js
@@ -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');
     }
   }
 }
diff --git a/app/static/js/RessourceDisplays/CorpusDisplay.js b/app/static/js/RessourceDisplays/CorpusDisplay.js
index b807241e9ffe1f828fc3079084a56a47faf99ed7..ad3cb34bb732bc17e0f166706e50b3a453c2234e 100644
--- a/app/static/js/RessourceDisplays/CorpusDisplay.js
+++ b/app/static/js/RessourceDisplays/CorpusDisplay.js
@@ -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');
diff --git a/app/static/js/RessourceDisplays/JobDisplay.js b/app/static/js/RessourceDisplays/JobDisplay.js
index 921029087d89fb71bcbe1b51325727c80f820920..f00242a503d0fe60636eca37e3c4c80a3cc25160 100644
--- a/app/static/js/RessourceDisplays/JobDisplay.js
+++ b/app/static/js/RessourceDisplays/JobDisplay.js
@@ -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');
diff --git a/app/static/js/RessourceLists/CorpusFileList.js b/app/static/js/RessourceLists/CorpusFileList.js
index 6aa3864ba67ab0e554c085526cc73c6b43a678b9..a4d76dadf973c84c5a0ac7a5fec50fb27a1669b7 100644
--- a/app/static/js/RessourceLists/CorpusFileList.js
+++ b/app/static/js/RessourceLists/CorpusFileList.js
@@ -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:
diff --git a/app/static/js/RessourceLists/CorpusList.js b/app/static/js/RessourceLists/CorpusList.js
index 7d4eb077abd3292484ce9b6022368630599faee2..d939d17ee71ad9ffaf0c1e53c4f7126f9a5f85bc 100644
--- a/app/static/js/RessourceLists/CorpusList.js
+++ b/app/static/js/RessourceLists/CorpusList.js
@@ -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'
     ]
diff --git a/app/static/js/RessourceLists/JobInputList.js b/app/static/js/RessourceLists/JobInputList.js
index 714f3e597fe84591105ddd380d5ea304389b990a..c1f313122903af6efd6ab7a5c5cf6eda6615319a 100644
--- a/app/static/js/RessourceLists/JobInputList.js
+++ b/app/static/js/RessourceLists/JobInputList.js
@@ -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'
+    ]
   };
 
 
diff --git a/app/static/js/RessourceLists/JobList.js b/app/static/js/RessourceLists/JobList.js
index ecb84dbdf9c8a8eb9feff34e386ca897b0f70079..58a0074ace221c08680a4813f7a6d17e7e235bf8 100644
--- a/app/static/js/RessourceLists/JobList.js
+++ b/app/static/js/RessourceLists/JobList.js
@@ -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'
     ]
diff --git a/app/static/js/RessourceLists/JobResultList.js b/app/static/js/RessourceLists/JobResultList.js
index 708b25f22504c06552213100083e3f2cfddd0489..5f9da3b59bef588bdee2d6375710058686fa3196 100644
--- a/app/static/js/RessourceLists/JobResultList.js
+++ b/app/static/js/RessourceLists/JobResultList.js
@@ -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'
     ]
diff --git a/app/static/js/RessourceLists/QueryResultList.js b/app/static/js/RessourceLists/QueryResultList.js
index 429cc70a39c9300c82a266f05daf19933ae85616..c78ea3cbf142b55894f8a02cb7b73269390c544e 100644
--- a/app/static/js/RessourceLists/QueryResultList.js
+++ b/app/static/js/RessourceLists/QueryResultList.js
@@ -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:
diff --git a/app/static/js/RessourceLists/UserList.js b/app/static/js/RessourceLists/UserList.js
index e52eac480aca379428ed823c05415c8f32975a32..5f488f47ef3e5ef897b812e7d5efcf00640cee60 100644
--- a/app/static/js/RessourceLists/UserList.js
+++ b/app/static/js/RessourceLists/UserList.js
@@ -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'
     ]
diff --git a/app/templates/_colors.html.j2 b/app/templates/_colors.html.j2
index a6ac0ed82a107e3154e6def33bc138ae2d9c6273..ee170aac0e1232da6e7031eb883ad003a7385587 100644
--- a/app/templates/_colors.html.j2
+++ b/app/templates/_colors.html.j2
@@ -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'
+  }
 } %}
diff --git a/app/templates/_sidenav.html.j2 b/app/templates/_sidenav.html.j2
index 8729f4f8a81d0dcb52b14a775c5405a521301922..10986506c49617a89e7545fdb9c2192d6d099215 100644
--- a/app/templates/_sidenav.html.j2
+++ b/app/templates/_sidenav.html.j2
@@ -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>
diff --git a/app/templates/_styles.html.j2 b/app/templates/_styles.html.j2
index d94765decb4642636621d2badc61b5d567d965fc..14a614f0c617122eeaaad0d409eee433ce037944 100644
--- a/app/templates/_styles.html.j2
+++ b/app/templates/_styles.html.j2
@@ -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>
diff --git a/app/templates/admin/edit_user.html.j2 b/app/templates/admin/edit_user.html.j2
index d824c1129ec8569b3ad7e803ebc78159b0caad69..f44ccb7c18236f46ba0582c956eea5c93906a036 100644
--- a/app/templates/admin/edit_user.html.j2
+++ b/app/templates/admin/edit_user.html.j2
@@ -6,66 +6,132 @@
 <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">
+      <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>
 
-    <div class="col s12 m8">
+    <form method="POST">
+      {{ edit_interface_settings_form.hidden_tag() }}
       <div class="card">
-        <form method="POST">
-          <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') }}
-            <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 class="col s12"><p>&nbsp;</p></div>
-              <div class="col s12 divider"></div>
-              <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 class="light">Change confirmation status manually.</p>
-              </div>
-              <div class="col s3 right-align">
-                <div class="switch">
-                  <label>
-                    {{ form.confirmed() }}
-                    <span class="lever"></span>
-                  </label>
-                </div>
+        <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 class="card-action right-align">
-            {{ wtf.render_field(form.submit, material_icon='send') }}
+        </div>
+        <div class="card-action">
+          <div class="right-align">
+            {{ wtf.render_field(edit_interface_settings_form.submit, material_icon='send') }}
           </div>
-        </form>
+        </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>{{ 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>
+                  {{ admin_edit_user_form.confirmed() }}
+                  <span class="lever"></span>
+                </label>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="card-action right-align">
+          {{ 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 %}
diff --git a/app/templates/corpora/add_corpus.html.j2 b/app/templates/corpora/add_corpus.html.j2
index 9d7f24e4b907a536f36de4acf6347c40e3f4803e..820230c85a115b194491551041d52aeef8dd5195 100644
--- a/app/templates/corpora/add_corpus.html.j2
+++ b/app/templates/corpora/add_corpus.html.j2
@@ -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">
diff --git a/app/templates/corpora/add_corpus_file.html.j2 b/app/templates/corpora/add_corpus_file.html.j2
index 8246821a76351f55dc884fedc2f19c462e4aa09e..d1d3c4fd9aad7131b1c36f23127dca425bb46c18 100644
--- a/app/templates/corpora/add_corpus_file.html.j2
+++ b/app/templates/corpora/add_corpus_file.html.j2
@@ -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">
diff --git a/app/templates/corpora/analyse_corpus.html.j2 b/app/templates/corpora/analyse_corpus.html.j2
index 11389fafc830efea67bdf1a68b0c74edc411b84c..d94ed2746577eb162462a7508d4bcfa953c3ba42 100644
--- a/app/templates/corpora/analyse_corpus.html.j2
+++ b/app/templates/corpora/analyse_corpus.html.j2
@@ -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>
diff --git a/app/templates/corpora/corpus.html.j2 b/app/templates/corpora/corpus.html.j2
index 7ca7b413af7171905ad8303d98413759c5c56eff..07f4373b118781b01b59d77d02dc474270b4a5eb 100644
--- a/app/templates/corpora/corpus.html.j2
+++ b/app/templates/corpora/corpus.html.j2
@@ -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>
diff --git a/app/templates/corpora/corpus_file.html.j2 b/app/templates/corpora/corpus_file.html.j2
index 53ac550ef5e2976f5b47e2b86a6673e7ec5234a8..e779d34f92d8ccefdeb8bf2b7fe5dfc9d5a7ed00 100644
--- a/app/templates/corpora/corpus_file.html.j2
+++ b/app/templates/corpora/corpus_file.html.j2
@@ -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">
diff --git a/app/templates/corpora/import_corpus.html.j2 b/app/templates/corpora/import_corpus.html.j2
index 865a33980b7e5a81525d25534206d89276166fcf..5d3e4107941c866b09ba1638f900f17993763bc4 100644
--- a/app/templates/corpora/import_corpus.html.j2
+++ b/app/templates/corpora/import_corpus.html.j2
@@ -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">
diff --git a/app/templates/jobs/job.html.j2 b/app/templates/jobs/job.html.j2
index f5f7076927a5fc4b0dbd68e445aca46d47be85e6..42296b226229758374d7effd77130cf3e07a53ff 100644
--- a/app/templates/jobs/job.html.j2
+++ b/app/templates/jobs/job.html.j2
@@ -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">
diff --git a/app/templates/main/dashboard.html.j2 b/app/templates/main/dashboard.html.j2
index 05f5b804fc9cbd18e90de06915ea9bb3193a4be0..fbb19b488a39b00908ad001b729d5095033ada72 100644
--- a/app/templates/main/dashboard.html.j2
+++ b/app/templates/main/dashboard.html.j2
@@ -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>
diff --git a/app/templates/main/index.html.j2 b/app/templates/main/index.html.j2
index 0bd343c34b5b090f44742358cc6d830e75f7fa85..7f41203d218a20bbff2da7292a117ca5fb761f6e 100644
--- a/app/templates/main/index.html.j2
+++ b/app/templates/main/index.html.j2
@@ -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 nopaque’s 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>
diff --git a/app/templates/services/corpus_analysis.html.j2 b/app/templates/services/corpus_analysis.html.j2
index f89b46fcb59cc581695bf91d89ee8ad36d7bfb84..d29f503574f40892a93432c5085bb07d06f9e008 100644
--- a/app/templates/services/corpus_analysis.html.j2
+++ b/app/templates/services/corpus_analysis.html.j2
@@ -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>
diff --git a/app/templates/services/file_setup.html.j2 b/app/templates/services/file_setup.html.j2
index 9e70288c4f030e5bf1b1d566c7cb577d4f16ff9d..ec2efede707817aab4d3eae0d6a0b3cc383ccfcb 100644
--- a/app/templates/services/file_setup.html.j2
+++ b/app/templates/services/file_setup.html.j2
@@ -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">
diff --git a/app/templates/services/spacy_nlp.html.j2 b/app/templates/services/spacy_nlp.html.j2
index 30fab84cff34a8a42d8d9bd78e208d4fcc6cca8c..30516fb57cf7b658b3ce29a0a08161993ffa1b89 100644
--- a/app/templates/services/spacy_nlp.html.j2
+++ b/app/templates/services/spacy_nlp.html.j2
@@ -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">
diff --git a/app/templates/services/tesseract_ocr.html.j2 b/app/templates/services/tesseract_ocr.html.j2
index 6612128105838ffb8dd9ddb7a8e4b248f9a5a121..0a529cd85168c6e1b4c953b4b881c6ab73c1833f 100644
--- a/app/templates/services/tesseract_ocr.html.j2
+++ b/app/templates/services/tesseract_ocr.html.j2
@@ -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">
diff --git a/app/templates/settings/_breadcrumbs.html.j2 b/app/templates/settings/_breadcrumbs.html.j2
index 0856463c248bc92dea988f1f969211364ddecd43..33b8984cef722fdd030696346afcbddf55c6ba89 100644
--- a/app/templates/settings/_breadcrumbs.html.j2
+++ b/app/templates/settings/_breadcrumbs.html.j2
@@ -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 %}
diff --git a/app/templates/settings/_menu.html.j2 b/app/templates/settings/_menu.html.j2
deleted file mode 100644
index 554a3f67eb49b1bcaabffda54ada154afa959bad..0000000000000000000000000000000000000000
--- a/app/templates/settings/_menu.html.j2
+++ /dev/null
@@ -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>
diff --git a/app/templates/settings/change_password.html.j2 b/app/templates/settings/change_password.html.j2
deleted file mode 100644
index da71ec3f59768598b672c60cf4b996a979dc6a0d..0000000000000000000000000000000000000000
--- a/app/templates/settings/change_password.html.j2
+++ /dev/null
@@ -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 %}
diff --git a/app/templates/settings/edit_general_settings.html.j2 b/app/templates/settings/edit_general_settings.html.j2
deleted file mode 100644
index cfae41a695dba32d487a8414bd4f1803fe7a5d48..0000000000000000000000000000000000000000
--- a/app/templates/settings/edit_general_settings.html.j2
+++ /dev/null
@@ -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 %}
diff --git a/app/templates/settings/edit_notification_settings.html.j2 b/app/templates/settings/edit_notification_settings.html.j2
deleted file mode 100644
index 74d98ba9fbfe8e03d04ab706e6638b9be0c13d85..0000000000000000000000000000000000000000
--- a/app/templates/settings/edit_notification_settings.html.j2
+++ /dev/null
@@ -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 %}
diff --git a/app/templates/settings/index.html.j2 b/app/templates/settings/index.html.j2
new file mode 100644
index 0000000000000000000000000000000000000000..1be9e284d61c58a0c1535ae1f599e8b8406829fd
--- /dev/null
+++ b/app/templates/settings/index.html.j2
@@ -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 %}
diff --git a/app/templates/tasks/email/notification.html.j2 b/app/templates/tasks/email/notification.html.j2
index 019dd45614ccf3329be1538f068df0d293719d25..6945f60d38129bfc86f640aa01077b2303783481 100644
--- a/app/templates/tasks/email/notification.html.j2
+++ b/app/templates/tasks/email/notification.html.j2
@@ -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>
 
diff --git a/app/templates/tasks/email/notification.txt.j2 b/app/templates/tasks/email/notification.txt.j2
index be746b9c2d1dc2779713ed1a3bfc0861800d2da6..3dba4927025d1c97e1fdbb6c0133bf52ce2fb7b1 100644
--- a/app/templates/tasks/email/notification.txt.j2
+++ b/app/templates/tasks/email/notification.txt.j2
@@ -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) }}
 
diff --git a/migrations/README b/migrations/README
index 98e4f9c44effe479ed38c66ba922e7bcc672916f..0e048441597444a7e2850d6d7c4ce15550f79bda 100644
--- a/migrations/README
+++ b/migrations/README
@@ -1 +1 @@
-Generic single-database configuration.
\ No newline at end of file
+Single-database configuration for Flask.
diff --git a/migrations/alembic.ini b/migrations/alembic.ini
index f8ed4801f78bcb83cc6acb589508c1b24eda297a..ec9d45c26a6bb54e833fd4e6ce2de29343894f4b 100644
--- a/migrations/alembic.ini
+++ b/migrations/alembic.ini
@@ -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,)
diff --git a/migrations/env.py b/migrations/env.py
index 79b8174be8a33bf4aaa610a4bab01dbd7bee9d80..68feded2a040005310d770ac7136b2e4ff8a6312 100644
--- a/migrations/env.py
+++ b/migrations/env.py
@@ -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(
diff --git a/migrations/versions/da9fd175af8c_.py b/migrations/versions/097aae1f02d7_.py
similarity index 53%
rename from migrations/versions/da9fd175af8c_.py
rename to migrations/versions/097aae1f02d7_.py
index 421c505900d6dbcd4015e532ff1be4dbf49bb2e6..ccac6756c2f6a8ef1f90f1fac0e279bf8b44ab66 100644
--- a/migrations/versions/da9fd175af8c_.py
+++ b/migrations/versions/097aae1f02d7_.py
@@ -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')
diff --git a/migrations/versions/099037c4aa06_.py b/migrations/versions/099037c4aa06_.py
deleted file mode 100644
index d9adf506911c4a5de16821610da734b634621cf1..0000000000000000000000000000000000000000
--- a/migrations/versions/099037c4aa06_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/0aa38a7973c5_.py b/migrations/versions/0aa38a7973c5_.py
deleted file mode 100644
index d242aaf520e18fb0d676c2d6db5c054857b8fe90..0000000000000000000000000000000000000000
--- a/migrations/versions/0aa38a7973c5_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/0d7aed934679_.py b/migrations/versions/0d7aed934679_.py
deleted file mode 100644
index 3c45d90ca8b4d94caa8f7c159043246882e58d55..0000000000000000000000000000000000000000
--- a/migrations/versions/0d7aed934679_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/10a92d8f4616_.py b/migrations/versions/10a92d8f4616_.py
deleted file mode 100644
index c750487f0878cd1e12b06af270d75c0dade115ab..0000000000000000000000000000000000000000
--- a/migrations/versions/10a92d8f4616_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/1210adfe1e34_.py b/migrations/versions/1210adfe1e34_.py
deleted file mode 100644
index edf3a2c97c7c86cbfacdb4c69aa3209fdb8d1e06..0000000000000000000000000000000000000000
--- a/migrations/versions/1210adfe1e34_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/318074622d14_.py b/migrations/versions/318074622d14_.py
deleted file mode 100644
index 84dba226e3e6ecf43136b0e576a649b3c44db452..0000000000000000000000000000000000000000
--- a/migrations/versions/318074622d14_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/33ec4d09b4ca_.py b/migrations/versions/33ec4d09b4ca_.py
deleted file mode 100644
index c5cd903ebdc5c78b7327a6bd1ad0a68c70e38098..0000000000000000000000000000000000000000
--- a/migrations/versions/33ec4d09b4ca_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/389bcf564726_.py b/migrations/versions/389bcf564726_.py
deleted file mode 100644
index 4244fcc11796c9132415c59c6b76b7c7c136c7f1..0000000000000000000000000000000000000000
--- a/migrations/versions/389bcf564726_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/3d9a20b8b26c_.py b/migrations/versions/3d9a20b8b26c_.py
deleted file mode 100644
index a384f219f26764e1b73ddada8708b168e9353729..0000000000000000000000000000000000000000
--- a/migrations/versions/3d9a20b8b26c_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/421ba4373e50_.py b/migrations/versions/421ba4373e50_.py
deleted file mode 100644
index af0c8f10096c2a0ebb7e1ff9de0eb430d4c6535e..0000000000000000000000000000000000000000
--- a/migrations/versions/421ba4373e50_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/4638e6509e13_.py b/migrations/versions/4638e6509e13_.py
deleted file mode 100644
index 66a965ca673c483e8de61a785cffdca8cb6baf8b..0000000000000000000000000000000000000000
--- a/migrations/versions/4638e6509e13_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/471aa04c1a92_.py b/migrations/versions/471aa04c1a92_.py
deleted file mode 100644
index c8cddf7e56306a3ec2c3ae5026a41f131be1e2cc..0000000000000000000000000000000000000000
--- a/migrations/versions/471aa04c1a92_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/4886241e0f5d_.py b/migrations/versions/4886241e0f5d_.py
deleted file mode 100644
index 3db8b0ac3883edc45db0c20b4c4dfe5fa0f924a6..0000000000000000000000000000000000000000
--- a/migrations/versions/4886241e0f5d_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/49a42c69e523_.py b/migrations/versions/49a42c69e523_.py
deleted file mode 100644
index 36281de5c9784ec7ce32eafe37c0b1e80d073ec8..0000000000000000000000000000000000000000
--- a/migrations/versions/49a42c69e523_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/4cf5e5606a83_.py b/migrations/versions/4cf5e5606a83_.py
deleted file mode 100644
index 350b1172f62944a1326ec1c4674e0e555f4e88d5..0000000000000000000000000000000000000000
--- a/migrations/versions/4cf5e5606a83_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/55d2b1a82ba9_.py b/migrations/versions/55d2b1a82ba9_.py
deleted file mode 100644
index e847160d16505746ae08edd2b22351ef87530c50..0000000000000000000000000000000000000000
--- a/migrations/versions/55d2b1a82ba9_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/5ba6786a847e_.py b/migrations/versions/5ba6786a847e_.py
deleted file mode 100644
index 62080605c575b6ec4cdc42d8537c2692a342f899..0000000000000000000000000000000000000000
--- a/migrations/versions/5ba6786a847e_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/62233e0cb2c7_.py b/migrations/versions/62233e0cb2c7_.py
deleted file mode 100644
index d87005039ed621d6f48bda6bc1a8bc38a7996dfa..0000000000000000000000000000000000000000
--- a/migrations/versions/62233e0cb2c7_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/6227310c2112_.py b/migrations/versions/6227310c2112_.py
deleted file mode 100644
index f4307c2e86b4dbae0b3277a2cc5b416a3ccf37bf..0000000000000000000000000000000000000000
--- a/migrations/versions/6227310c2112_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/66253783654f_.py b/migrations/versions/66253783654f_.py
deleted file mode 100644
index 1971d4dde385f4f6510dc781b28b2791583ed94e..0000000000000000000000000000000000000000
--- a/migrations/versions/66253783654f_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/68772b6560c3_.py b/migrations/versions/68772b6560c3_.py
deleted file mode 100644
index ee1228d3c8ecc5fb137312750ca487b647e19fc8..0000000000000000000000000000000000000000
--- a/migrations/versions/68772b6560c3_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/68ed092ffe5e_.py b/migrations/versions/68ed092ffe5e_.py
deleted file mode 100644
index bbd8b9678bf0e01116c1ac9b23295822f70489f5..0000000000000000000000000000000000000000
--- a/migrations/versions/68ed092ffe5e_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/6c2227f1cc77_.py b/migrations/versions/6c2227f1cc77_.py
deleted file mode 100644
index 9d58746e26c98f9ca681f1de5406183e67955161..0000000000000000000000000000000000000000
--- a/migrations/versions/6c2227f1cc77_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/7378391345fa_.py b/migrations/versions/7378391345fa_.py
deleted file mode 100644
index 133046a2d6666c5c5eca5ef84f4366aa36e97573..0000000000000000000000000000000000000000
--- a/migrations/versions/7378391345fa_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/776761fb7466_.py b/migrations/versions/776761fb7466_.py
deleted file mode 100644
index 3106d5bdded2b36f82f5145ff08782dc1d24bfaf..0000000000000000000000000000000000000000
--- a/migrations/versions/776761fb7466_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/790ce9512e75_.py b/migrations/versions/790ce9512e75_.py
deleted file mode 100644
index 033b8d3706b9d394291e3e788de830f2e654c6e8..0000000000000000000000000000000000000000
--- a/migrations/versions/790ce9512e75_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/8b2e0d43384a_.py b/migrations/versions/8b2e0d43384a_.py
deleted file mode 100644
index 9cc64a132787b675fa032df79be4c73b178a9cbc..0000000000000000000000000000000000000000
--- a/migrations/versions/8b2e0d43384a_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/9d21b228d353_.py b/migrations/versions/9d21b228d353_.py
deleted file mode 100644
index 33368bbff8a7452cd97f25e3bab2b7397447daaa..0000000000000000000000000000000000000000
--- a/migrations/versions/9d21b228d353_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/a4b3cf4ab098_.py b/migrations/versions/a4b3cf4ab098_.py
deleted file mode 100644
index 3876e3ef0ddf64b3b6ef38fc9b178c9881884b8b..0000000000000000000000000000000000000000
--- a/migrations/versions/a4b3cf4ab098_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/abf60427ff84_.py b/migrations/versions/abf60427ff84_.py
deleted file mode 100644
index 4b6cf65279ae04b2f785f9ba5a262d1e9de07e59..0000000000000000000000000000000000000000
--- a/migrations/versions/abf60427ff84_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/ad0d835fe5b1_.py b/migrations/versions/ad0d835fe5b1_.py
deleted file mode 100644
index 0248e316ccf0ce18b3e1b18bc5c058f2e5dc0c68..0000000000000000000000000000000000000000
--- a/migrations/versions/ad0d835fe5b1_.py
+++ /dev/null
@@ -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 ###
diff --git a/migrations/versions/b15366b25bea_.py b/migrations/versions/b15366b25bea_.py
deleted file mode 100644
index 2e90d9b00ffd5b17fcf825549f82472d45bca821..0000000000000000000000000000000000000000
--- a/migrations/versions/b15366b25bea_.py
+++ /dev/null
@@ -1,42 +0,0 @@
-"""empty message
-
-Revision ID: b15366b25bea
-Revises: 4886241e0f5d
-Create Date: 2020-06-29 13:41:14.394680
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = 'b15366b25bea'
-down_revision = '4886241e0f5d'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.create_table('results',
-    sa.Column('id', sa.Integer(), nullable=False),
-    sa.Column('user_id', sa.Integer(), nullable=True),
-    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
-    sa.PrimaryKeyConstraint('id')
-    )
-    op.create_table('result_files',
-    sa.Column('id', sa.Integer(), nullable=False),
-    sa.Column('result_id', sa.Integer(), nullable=True),
-    sa.Column('filename', sa.String(length=255), nullable=True),
-    sa.Column('dir', sa.String(length=255), nullable=True),
-    sa.ForeignKeyConstraint(['result_id'], ['results.id'], ),
-    sa.PrimaryKeyConstraint('id')
-    )
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_table('result_files')
-    op.drop_table('results')
-    # ### end Alembic commands ###
diff --git a/migrations/versions/be010d5d708d_.py b/migrations/versions/be010d5d708d_.py
deleted file mode 100644
index 4a2d93b8be3d6dfcc7072543a5dc8d70b174527a..0000000000000000000000000000000000000000
--- a/migrations/versions/be010d5d708d_.py
+++ /dev/null
@@ -1,28 +0,0 @@
-"""empty message
-
-Revision ID: be010d5d708d
-Revises: a4b3cf4ab098
-Create Date: 2021-09-24 09:34:54.173653
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = 'be010d5d708d'
-down_revision = 'a4b3cf4ab098'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.add_column('corpora', sa.Column('num_analysis_sessions', sa.Integer(), nullable=True))
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_column('corpora', 'num_analysis_sessions')
-    # ### end Alembic commands ###
diff --git a/migrations/versions/befe5326787e_.py b/migrations/versions/befe5326787e_.py
deleted file mode 100644
index 11839d5c15d85198831e0125e2f300f5cd160605..0000000000000000000000000000000000000000
--- a/migrations/versions/befe5326787e_.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""empty message
-
-Revision ID: befe5326787e
-Revises: ecaf75fece7b
-Create Date: 2020-10-16 13:32:09.620960
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = 'befe5326787e'
-down_revision = 'ecaf75fece7b'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.add_column('corpora', sa.Column('archive_file', sa.String(length=255), nullable=True))
-    op.drop_column('corpora', 'archive_dir')
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.add_column('corpora', sa.Column('archive_dir', sa.VARCHAR(length=255), autoincrement=False, nullable=True))
-    op.drop_column('corpora', 'archive_file')
-    # ### end Alembic commands ###
diff --git a/migrations/versions/c3827cddea6e_.py b/migrations/versions/c3827cddea6e_.py
deleted file mode 100644
index eb8f3ea18bbf1ae31451e3f024b18e334ab0eb1a..0000000000000000000000000000000000000000
--- a/migrations/versions/c3827cddea6e_.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""empty message
-
-Revision ID: c3827cddea6e
-Revises: 9d21b228d353
-Create Date: 2020-07-15 12:33:24.574579
-
-"""
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision = 'c3827cddea6e'
-down_revision = '9d21b228d353'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_table('result_files')
-    op.drop_table('results')
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.create_table('result_files',
-    sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
-    sa.Column('result_id', sa.INTEGER(), autoincrement=False, nullable=True),
-    sa.Column('filename', sa.VARCHAR(length=255), autoincrement=False, nullable=True),
-    sa.Column('dir', sa.VARCHAR(length=255), autoincrement=False, nullable=True),
-    sa.ForeignKeyConstraint(['result_id'], ['results.id'], name='result_files_result_id_fkey'),
-    sa.PrimaryKeyConstraint('id', name='result_files_pkey')
-    )
-    op.create_table('results',
-    sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
-    sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True),
-    sa.Column('corpus_metadata', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True),
-    sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='results_user_id_fkey'),
-    sa.PrimaryKeyConstraint('id', name='results_pkey')
-    )
-    # ### end Alembic commands ###
diff --git a/migrations/versions/c384d7b3268a_.py b/migrations/versions/c384d7b3268a_.py
deleted file mode 100644
index aaffd8d2bc30a84e0845ad5deb2db8fae99e6af5..0000000000000000000000000000000000000000
--- a/migrations/versions/c384d7b3268a_.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""empty message
-
-Revision ID: c384d7b3268a
-Revises: 55d2b1a82ba9
-Create Date: 2021-09-14 09:11:45.409350
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = 'c384d7b3268a'
-down_revision = '55d2b1a82ba9'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.add_column('users', sa.Column('token', sa.String(length=32), nullable=True))
-    op.add_column('users', sa.Column('token_expiration', sa.DateTime(), nullable=True))
-    op.create_index(op.f('ix_users_token'), 'users', ['token'], unique=True)
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_index(op.f('ix_users_token'), table_name='users')
-    op.drop_column('users', 'token_expiration')
-    op.drop_column('users', 'token')
-    # ### end Alembic commands ###
diff --git a/migrations/versions/ded5a37f8a7b_.py b/migrations/versions/ded5a37f8a7b_.py
deleted file mode 100644
index 141e47860ba796d9cc4cf71745d6eb2fa7f5b175..0000000000000000000000000000000000000000
--- a/migrations/versions/ded5a37f8a7b_.py
+++ /dev/null
@@ -1,44 +0,0 @@
-"""empty message
-
-Revision ID: ded5a37f8a7b
-Revises: 776761fb7466
-Create Date: 2020-01-08 14:39:32.182439
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = 'ded5a37f8a7b'
-down_revision = '776761fb7466'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.add_column('corpus_files', sa.Column('address', sa.String(length=255), nullable=True))
-    op.add_column('corpus_files', sa.Column('booktitle', sa.String(length=255), nullable=True))
-    op.add_column('corpus_files', sa.Column('chapter', sa.String(length=255), nullable=True))
-    op.add_column('corpus_files', sa.Column('editor', sa.String(length=255), nullable=True))
-    op.add_column('corpus_files', sa.Column('institution', sa.String(length=255), nullable=True))
-    op.add_column('corpus_files', sa.Column('journal', sa.String(length=255), nullable=True))
-    op.add_column('corpus_files', sa.Column('pages', sa.String(length=255), nullable=True))
-    op.add_column('corpus_files', sa.Column('publisher', sa.String(length=255), nullable=True))
-    op.add_column('corpus_files', sa.Column('school', sa.String(length=255), nullable=True))
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_column('corpus_files', 'school')
-    op.drop_column('corpus_files', 'publisher')
-    op.drop_column('corpus_files', 'pages')
-    op.drop_column('corpus_files', 'journal')
-    op.drop_column('corpus_files', 'institution')
-    op.drop_column('corpus_files', 'editor')
-    op.drop_column('corpus_files', 'chapter')
-    op.drop_column('corpus_files', 'booktitle')
-    op.drop_column('corpus_files', 'address')
-    # ### end Alembic commands ###
diff --git a/migrations/versions/e256f5cac75d_.py b/migrations/versions/e256f5cac75d_.py
deleted file mode 100644
index 3e810f2b5ffda9bf71fdf8874828eeb3f640ce99..0000000000000000000000000000000000000000
--- a/migrations/versions/e256f5cac75d_.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""empty message
-
-Revision ID: e256f5cac75d
-Revises: 389bcf564726
-Create Date: 2020-07-01 07:45:24.637861
-
-"""
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision = 'e256f5cac75d'
-down_revision = '389bcf564726'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_column('result_files', 'corpus_metadata')
-    op.add_column('results', sa.Column('corpus_metadata', sa.JSON(), nullable=True))
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_column('results', 'corpus_metadata')
-    op.add_column('result_files', sa.Column('corpus_metadata', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True))
-    # ### end Alembic commands ###
diff --git a/migrations/versions/ecaf75fece7b_.py b/migrations/versions/ecaf75fece7b_.py
deleted file mode 100644
index 5e258a2c68125b3557dd19d6260c69173d496d1f..0000000000000000000000000000000000000000
--- a/migrations/versions/ecaf75fece7b_.py
+++ /dev/null
@@ -1,28 +0,0 @@
-"""empty message
-
-Revision ID: ecaf75fece7b
-Revises: c3827cddea6e
-Create Date: 2020-10-16 13:31:30.681269
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = 'ecaf75fece7b'
-down_revision = 'c3827cddea6e'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.add_column('corpora', sa.Column('archive_dir', sa.String(length=255), nullable=True))
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_column('corpora', 'archive_dir')
-    # ### end Alembic commands ###
diff --git a/nopaque.py b/nopaque.py
index ab8db5a6febb7b2257794b73990911c6529ce926..243b915d0b253f4449727ac74d047dfff7f77b1d 100644
--- a/nopaque.py
+++ b/nopaque.py
@@ -3,9 +3,18 @@
 import eventlet
 eventlet.monkey_patch()
 
-from app.models import (Corpus, CorpusFile, Job, JobInput, JobResult,
-                        Permission, QueryResult, Role, TesseractOCRModel, User)  # noqa
 from app import db, cli, create_app  # noqa
+from app.models import (
+    Corpus,
+    CorpusFile,
+    Job,
+    JobInput,
+    JobResult,
+    Permission,
+    Role,
+    TesseractOCRModel,
+    User
+)  # noqa
 from flask import Flask  # noqa
 from typing import Any, Dict  # noqa
 
@@ -31,7 +40,6 @@ def make_shell_context() -> Dict[str, Any]:
         'JobInput': JobInput,
         'JobResult': JobResult,
         'Permission': Permission,
-        'QueryResult': QueryResult,
         'Role': Role,
         'TesseractOCRModel': TesseractOCRModel,
         'User': User
diff --git a/requirements.txt b/requirements.txt
index 52770c57c788a37ebfd80c9547504de7328a2ff1..ed85ab37ebe4f1bbd61cf22e90b9a74bad845306 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 cqi
 docker
 eventlet==0.30.2
-Flask~=1.1
+Flask==1.1.4
 Flask-Assets
 Flask-Hashids
 Flask-HTTPAuth
@@ -14,7 +14,6 @@ Flask-SocketIO~=5.1
 Flask-SQLAlchemy
 Flask-WTF
 gunicorn
-hashids
 hiredis
 jsonschema
 psycopg2