mirror of
https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
synced 2024-11-15 01:05:42 +00:00
More exception handling. Remove unused database models. New common view structure!
This commit is contained in:
parent
cb9da5c7dd
commit
5a06a6b241
135
.env.tpl
135
.env.tpl
@ -9,128 +9,116 @@
|
|||||||
# NOTE: Use `.` as <project-root-dir>
|
# NOTE: Use `.` as <project-root-dir>
|
||||||
# HOST_MQ_DIR=
|
# HOST_MQ_DIR=
|
||||||
|
|
||||||
# Example: 999
|
# Example: 1000
|
||||||
# HINT: Use this bash command `getent group docker | cut -d: -f3`
|
# HINT: Use this bash command `id -u`
|
||||||
HOST_DOCKER_GID=
|
HOST_UID=
|
||||||
|
|
||||||
# Example: 1000
|
# Example: 1000
|
||||||
# HINT: Use this bash command `id -g`
|
# HINT: Use this bash command `id -g`
|
||||||
HOST_GID=
|
HOST_GID=
|
||||||
|
|
||||||
# DEFAULT: ./nopaqued.log
|
# Example: 999
|
||||||
# NOTES: Use `.` as <project-root-dir>,
|
# HINT: Use this bash command `getent group docker | cut -d: -f3`
|
||||||
# This file must be present on container startup
|
HOST_DOCKER_GID=
|
||||||
# HOST_NOPAQUE_DAEMON_LOG_FILE=
|
|
||||||
|
|
||||||
# DEFAULT: ./nopaque.log
|
# DEFAULT: ./nopaque.log
|
||||||
# NOTES: Use `.` as <project-root-dir>,
|
# NOTES: Use `.` as <project-root-dir>,
|
||||||
# This file must be present on container startup
|
# This file must be present on container startup
|
||||||
# HOST_NOPAQUE_LOG_FILE=
|
# HOST_LOG_FILE=
|
||||||
|
|
||||||
# Example: 1000
|
|
||||||
# HINT: Use this bash command `id -u`
|
|
||||||
HOST_UID=
|
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Cookies #
|
# Flask #
|
||||||
|
# https://flask.palletsprojects.com/en/1.1.x/config/ #
|
||||||
################################################################################
|
################################################################################
|
||||||
# CHOOSE ONE: False, True
|
# DEFAULT: hard to guess string
|
||||||
# DEFAULT: False
|
# HINT: Use this bash command `python -c "import uuid; print(uuid.uuid4().hex)"`
|
||||||
# HINT: Set to true if you redirect http to https
|
# SECRET_KEY=
|
||||||
# NOPAQUE_REMEMBER_COOKIE_SECURE=
|
|
||||||
|
|
||||||
# CHOOSE ONE: False, True
|
# CHOOSE ONE: False, True
|
||||||
# DEFAULT: False
|
# DEFAULT: False
|
||||||
# HINT: Set to true if you redirect http to https
|
# HINT: Set to true if you redirect http to https
|
||||||
# NOPAQUE_SESSION_COOKIE_SECURE=
|
# SESSION_COOKIE_SECURE=
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Database #
|
# Flask-Login #
|
||||||
# DATABASE_URI blueprint: #
|
# https://flask-login.readthedocs.io/en/latest/ #
|
||||||
# - dialect[+driver]://username:password@host[:port]/database #
|
|
||||||
# - sqlite is not supported #
|
|
||||||
# - values in square brackets are optional #
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# DEFAULT: postgresql://nopaque:nopaque@db/nopaque
|
# CHOOSE ONE: False, True
|
||||||
# NOPAQUE_DATABASE_URL=
|
# DEFAULT: False
|
||||||
|
# HINT: Set to true if you redirect http to https
|
||||||
# DEFAULT: postgresql://nopaque:nopaque@db/nopaque_dev
|
# REMEMBER_COOKIE_SECURE=
|
||||||
# NOPAQUE_DEV_DATABASE_URL=
|
|
||||||
|
|
||||||
# DEFAULT: postgresql://nopaque:nopaque@db/nopaque_test
|
|
||||||
# NOPAQUE_TEST_DATABASE_URL=
|
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Email #
|
# Flask-Mail #
|
||||||
|
# https://pythonhosted.org/Flask-Mail/ #
|
||||||
################################################################################
|
################################################################################
|
||||||
# EXAMPLE: nopaque Admin <nopaque@example.com>
|
# EXAMPLE: nopaque Admin <nopaque@example.com>
|
||||||
NOPAQUE_SMTP_DEFAULT_SENDER=
|
MAIL_DEFAULT_SENDER=
|
||||||
|
|
||||||
NOPAQUE_SMTP_PASSWORD=
|
MAIL_PASSWORD=
|
||||||
|
|
||||||
# EXAMPLE: smtp.example.com
|
# EXAMPLE: smtp.example.com
|
||||||
NOPAQUE_SMTP_SERVER=
|
MAIL_SERVER=
|
||||||
|
|
||||||
# EXAMPLE: 587
|
# EXAMPLE: 587
|
||||||
NOPAQUE_SMTP_PORT=
|
MAIL_PORT=
|
||||||
|
|
||||||
# CHOOSE ONE: False, True
|
# CHOOSE ONE: False, True
|
||||||
# DEFAULT: False
|
# DEFAULT: False
|
||||||
# NOPAQUE_SMTP_USE_SSL=
|
# MAIL_USE_SSL=
|
||||||
|
|
||||||
# CHOOSE ONE: False, True
|
# CHOOSE ONE: False, True
|
||||||
# DEFAULT: False
|
# DEFAULT: False
|
||||||
# NOPAQUE_SMTP_USE_TLS=
|
# MAIL_USE_TLS=
|
||||||
|
|
||||||
# EXAMPLE: nopaque@example.com
|
# EXAMPLE: nopaque@example.com
|
||||||
NOPAQUE_SMTP_USERNAME=
|
MAIL_USERNAME=
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# General #
|
# Flask-SQLAlchemy #
|
||||||
|
# https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/ #
|
||||||
################################################################################
|
################################################################################
|
||||||
|
# DEFAULT with development config: postgresql://nopaque:nopaque@db/nopaque_dev
|
||||||
|
# DEFAULT with production config: postgresql://nopaque:nopaque@db/nopaque
|
||||||
|
# DEFAULT with testing config: postgresql://nopaque:nopaque@db/nopaque_test
|
||||||
|
# SQLALCHEMY_DATABASE_URI=
|
||||||
|
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# nopaque #
|
||||||
|
################################################################################
|
||||||
|
# If an account is registered with this email adress gets automatically
|
||||||
|
# assigned the administrator role.
|
||||||
# EXAMPLE: admin.nopaque@example.com
|
# EXAMPLE: admin.nopaque@example.com
|
||||||
NOPAQUE_ADMIN_EMAIL_ADRESS=
|
NOPAQUE_ADMIN=
|
||||||
|
|
||||||
# DEFAULT: development
|
# DEFAULT: development
|
||||||
# CHOOSE ONE: development, production, testing
|
# CHOOSE ONE: development, production, testing
|
||||||
# NOPAQUE_CONFIG=
|
# NOPAQUE_CONFIG=
|
||||||
|
|
||||||
|
# This email adress is used for the contact button in the nopaque footer. If
|
||||||
|
# not set, no contact button is displayed.
|
||||||
# DEFAULT: None
|
# DEFAULT: None
|
||||||
# EXAMPLE: contact.nopaque@example.com
|
# EXAMPLE: contact.nopaque@example.com
|
||||||
# NOPAQUE_CONTACT_EMAIL_ADRESS=
|
# NOPAQUE_CONTACT=
|
||||||
|
|
||||||
# DEFAULT: /mnt/nopaque
|
# DEFAULT: /mnt/nopaque
|
||||||
# NOTE: This must be a network share and it must be available on all Docker Swarm nodes
|
# NOTE: This must be a network share and it must be available on all Docker
|
||||||
|
# Swarm nodes
|
||||||
# NOPAQUE_DATA_DIR=
|
# NOPAQUE_DATA_DIR=
|
||||||
|
|
||||||
# DEFAULT: localhost
|
|
||||||
# NOPAQUE_DOMAIN=
|
|
||||||
|
|
||||||
# DEFAULT: 0.0.0.0
|
# DEFAULT: 0.0.0.0
|
||||||
# NOPAQUE_HOST=
|
# NOPAQUE_HOST=
|
||||||
|
|
||||||
# DEFAULT: 5000
|
# DEFAULT: 5000
|
||||||
# NOPAQUE_PORT=
|
# NOPAQUE_PORT=
|
||||||
|
|
||||||
# CHOOSE ONE: http, https
|
# transport://[userid:password]@hostname[:port]/[virtual_host]
|
||||||
# DEFAULT: http
|
NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI=
|
||||||
# NOPAQUE_PROTOCOL=
|
|
||||||
|
|
||||||
# DEFAULT: hard to guess string
|
|
||||||
# HINT: Use this bash command `python -c "import uuid; print(uuid.uuid4().hex)"`
|
|
||||||
# NOPAQUE_SECRET_KEY=
|
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Logging #
|
|
||||||
################################################################################
|
|
||||||
# DEFAULT: /home/nopaqued/nopaqued.log ~ /home/nopaqued/nopaqued.log
|
|
||||||
# NOTE: Use `.` as <nopaqued-root-dir>
|
|
||||||
# NOPAQUE_DAEMON_LOG_FILE=
|
|
||||||
|
|
||||||
# DEFAULT: %Y-%m-%d %H:%M:%S
|
# DEFAULT: %Y-%m-%d %H:%M:%S
|
||||||
# NOPAQUE_LOG_DATE_FORMAT=
|
# NOPAQUE_LOG_DATE_FORMAT=
|
||||||
@ -146,37 +134,22 @@ NOPAQUE_ADMIN_EMAIL_ADRESS=
|
|||||||
# CHOOSE ONE: CRITICAL, ERROR, WARNING, INFO, DEBUG
|
# CHOOSE ONE: CRITICAL, ERROR, WARNING, INFO, DEBUG
|
||||||
# NOPAQUE_LOG_LEVEL=
|
# NOPAQUE_LOG_LEVEL=
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Message queue #
|
|
||||||
# MESSAGE_QUEUE_URI blueprint: #
|
|
||||||
# - transport://[userid:password]@hostname[:port]/[virtual_host] #
|
|
||||||
# - values in square brackets are optional #
|
|
||||||
################################################################################
|
|
||||||
# DEFAULT: None
|
|
||||||
# HINT: A message queue is not required when using a single server process
|
|
||||||
# NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI=
|
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Proxy fix #
|
|
||||||
################################################################################
|
|
||||||
# DEFAULT: 0
|
# DEFAULT: 0
|
||||||
# Number of values to trust for X-Forwarded-For
|
# Number of values to trust for X-Forwarded-For
|
||||||
# NOPAQUE_NUM_PROXIES_X_FOR=
|
# NOPAQUE_PROXY_FIX_X_FOR=
|
||||||
|
|
||||||
# DEFAULT: 0
|
# DEFAULT: 0
|
||||||
# Number of values to trust for X-Forwarded-Host
|
# Number of values to trust for X-Forwarded-Host
|
||||||
# NOPAQUE_NUM_PROXIES_X_HOST=
|
# NOPAQUE_PROXY_FIX_X_HOST=
|
||||||
|
|
||||||
# DEFAULT: 0
|
# DEFAULT: 0
|
||||||
# Number of values to trust for X-Forwarded-Port
|
# Number of values to trust for X-Forwarded-Port
|
||||||
# NOPAQUE_NUM_PROXIES_X_PORT=
|
# NOPAQUE_PROXY_FIX_X_PORT=
|
||||||
|
|
||||||
# DEFAULT: 0
|
# DEFAULT: 0
|
||||||
# Number of values to trust for X-Forwarded-Prefix
|
# Number of values to trust for X-Forwarded-Prefix
|
||||||
# NOPAQUE_NUM_PROXIES_X_PREFIX=
|
# NOPAQUE_PROXY_FIX_X_PREFIX=
|
||||||
|
|
||||||
# DEFAULT: 0
|
# DEFAULT: 0
|
||||||
# Number of values to trust for X-Forwarded-Proto
|
# Number of values to trust for X-Forwarded-Proto
|
||||||
# NOPAQUE_NUM_PROXIES_X_PROTO=
|
# NOPAQUE_PROXY_FIX_X_PROTO=
|
||||||
|
@ -28,5 +28,6 @@ services:
|
|||||||
image: nopaque:development
|
image: nopaque:development
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
- "${NOPAQUE_DATA_DIR:-/mnt/nopaque}:${NOPAQUE_DATA_DIR:-/mnt/nopaque}"
|
- "${NOPAQUE_DATA_DIR:-/mnt/nopaque}:${NOPAQUE_DATA_DIR:-/mnt/nopaque}"
|
||||||
- "${HOST_NOPAQUE_LOG_FILE-./nopaque.log}:${NOPAQUE_LOG_FILE:-/home/nopaque/nopaque.log}"
|
- "${HOST_NOPAQUE_LOG_FILE-./nopaque.log}:${NOPAQUE_LOG_FILE:-/home/nopaque/nopaque.log}"
|
||||||
|
@ -21,8 +21,9 @@ RUN apt-get update \
|
|||||||
&& rm -r /var/lib/apt/lists/*
|
&& rm -r /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
RUN groupadd --gid ${GID} --system nopaque \
|
RUN groupadd --gid ${DOCKER_GID} --system docker \
|
||||||
&& useradd --create-home --gid ${GID} --no-log-init --system --uid ${UID} nopaque
|
&& groupadd --gid ${GID} --system nopaque \
|
||||||
|
&& useradd --create-home --gid ${GID} --groups ${DOCKER_GID} --no-log-init --system --uid ${UID} nopaque
|
||||||
USER nopaque
|
USER nopaque
|
||||||
WORKDIR /home/nopaque
|
WORKDIR /home/nopaque
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ def create_app(config_name):
|
|||||||
mail.init_app(app)
|
mail.init_app(app)
|
||||||
paranoid.init_app(app)
|
paranoid.init_app(app)
|
||||||
socketio.init_app(
|
socketio.init_app(
|
||||||
app, message_queue=config[config_name].SOCKETIO_MESSAGE_QUEUE_URI)
|
app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI'])
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
from . import events
|
from . import events
|
||||||
|
@ -2,4 +2,4 @@ from flask import Blueprint
|
|||||||
|
|
||||||
|
|
||||||
admin = Blueprint('admin', __name__)
|
admin = Blueprint('admin', __name__)
|
||||||
from . import views # noqa
|
from . import views
|
||||||
|
@ -12,4 +12,3 @@ class EditGeneralSettingsAdminForm(EditGeneralSettingsForm):
|
|||||||
super().__init__(*args, user=user, **kwargs)
|
super().__init__(*args, user=user, **kwargs)
|
||||||
self.role.choices = [(role.id, role.name)
|
self.role.choices = [(role.id, role.name)
|
||||||
for role in Role.query.order_by(Role.name).all()]
|
for role in Role.query.order_by(Role.name).all()]
|
||||||
self.user = user
|
|
||||||
|
@ -29,12 +29,11 @@ def user(user_id):
|
|||||||
@admin_required
|
@admin_required
|
||||||
def delete_user(user_id):
|
def delete_user(user_id):
|
||||||
settings_tasks.delete_user(user_id)
|
settings_tasks.delete_user(user_id)
|
||||||
flash('User has been deleted!')
|
flash('User has been marked for deletion!')
|
||||||
return redirect(url_for('.users'))
|
return redirect(url_for('.users'))
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/users/<int:user_id>/edit_general_settings',
|
@admin.route('/users/<int:user_id>/edit_general_settings', methods=['GET', 'POST']) # noqa
|
||||||
methods=['GET', 'POST'])
|
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_general_settings(user_id):
|
def edit_general_settings(user_id):
|
||||||
@ -46,16 +45,13 @@ def edit_general_settings(user_id):
|
|||||||
user.username = form.username.data
|
user.username = form.username.data
|
||||||
user.confirmed = form.confirmed.data
|
user.confirmed = form.confirmed.data
|
||||||
user.role = Role.query.get(form.role.data)
|
user.role = Role.query.get(form.role.data)
|
||||||
db.session.add(user)
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('The profile has been updated.')
|
flash('Settings have been updated.')
|
||||||
return redirect(url_for('admin.edit_general_settings', user_id=user.id))
|
return redirect(url_for('.edit_general_settings', user_id=user.id))
|
||||||
form.confirmed.data = user.confirmed
|
form.confirmed.data = user.confirmed
|
||||||
form.dark_mode.data = user.setting_dark_mode
|
form.dark_mode.data = user.setting_dark_mode
|
||||||
form.email.data = user.email
|
form.email.data = user.email
|
||||||
form.role.data = user.role_id
|
form.role.data = user.role_id
|
||||||
form.username.data = user.username
|
form.username.data = user.username
|
||||||
return render_template('admin/edit_general_settings.html.j2',
|
return render_template('admin/edit_general_settings.html.j2',
|
||||||
form=form,
|
form=form, title='General settings', user=user)
|
||||||
title='General settings',
|
|
||||||
user=user)
|
|
||||||
|
@ -2,4 +2,4 @@ from flask import Blueprint
|
|||||||
|
|
||||||
|
|
||||||
auth = Blueprint('auth', __name__)
|
auth = Blueprint('auth', __name__)
|
||||||
from . import views # noqa
|
from . import views
|
||||||
|
@ -18,7 +18,7 @@ class RegistrationForm(FlaskForm):
|
|||||||
username = StringField(
|
username = StringField(
|
||||||
'Username',
|
'Username',
|
||||||
validators=[DataRequired(), Length(1, 64),
|
validators=[DataRequired(), Length(1, 64),
|
||||||
Regexp(current_app.config['ALLOWED_USERNAME_REGEX'],
|
Regexp(current_app.config['NOPAQUE_USERNAME_REGEX'],
|
||||||
message='Usernames must have only letters, numbers,'
|
message='Usernames must have only letters, numbers,'
|
||||||
' dots or underscores')]
|
' dots or underscores')]
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from flask import (current_app, flash, redirect, render_template, request,
|
from datetime import datetime
|
||||||
url_for)
|
from flask import abort, flash, redirect, render_template, request, url_for
|
||||||
from flask_login import current_user, login_user, login_required, logout_user
|
from flask_login import current_user, login_user, login_required, logout_user
|
||||||
from . import auth
|
from . import auth
|
||||||
from .forms import (LoginForm, ResetPasswordForm, ResetPasswordRequestForm,
|
from .forms import (LoginForm, ResetPasswordForm, ResetPasswordRequestForm,
|
||||||
@ -7,8 +7,8 @@ from .forms import (LoginForm, ResetPasswordForm, ResetPasswordRequestForm,
|
|||||||
from .. import db
|
from .. import db
|
||||||
from ..email import create_message, send
|
from ..email import create_message, send
|
||||||
from ..models import User
|
from ..models import User
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
|
|
||||||
|
|
||||||
@auth.before_app_request
|
@auth.before_app_request
|
||||||
@ -18,11 +18,12 @@ def before_request():
|
|||||||
unconfirmed view if user is unconfirmed.
|
unconfirmed view if user is unconfirmed.
|
||||||
"""
|
"""
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
current_user.ping()
|
current_user.last_seen = datetime.utcnow()
|
||||||
if not current_user.confirmed \
|
db.session.commit()
|
||||||
and request.endpoint \
|
if (not current_user.confirmed
|
||||||
and request.blueprint != 'auth' \
|
and request.endpoint
|
||||||
and request.endpoint != 'static':
|
and request.blueprint != 'auth'
|
||||||
|
and request.endpoint != 'static'):
|
||||||
return redirect(url_for('auth.unconfirmed'))
|
return redirect(url_for('auth.unconfirmed'))
|
||||||
|
|
||||||
|
|
||||||
@ -30,20 +31,19 @@ def before_request():
|
|||||||
def login():
|
def login():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
login_form = LoginForm(prefix='login-form')
|
form = LoginForm(prefix='login-form')
|
||||||
if login_form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User.query.filter_by(username=login_form.user.data).first()
|
user = User.query.filter_by(username=form.user.data).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
user = User.query.filter_by(email=login_form.user.data).first()
|
user = User.query.filter_by(email=form.user.data.lower()).first()
|
||||||
if user is not None and user.verify_password(login_form.password.data):
|
if user is not None and user.verify_password(form.password.data):
|
||||||
login_user(user, login_form.remember_me.data)
|
login_user(user, form.remember_me.data)
|
||||||
next = request.args.get('next')
|
next = request.args.get('next')
|
||||||
if next is None or not next.startswith('/'):
|
if next is None or not next.startswith('/'):
|
||||||
next = url_for('main.dashboard')
|
next = url_for('main.dashboard')
|
||||||
return redirect(next)
|
return redirect(next)
|
||||||
flash('Invalid email/username or password.')
|
flash('Invalid email/username or password.')
|
||||||
return render_template('auth/login.html.j2', login_form=login_form,
|
return render_template('auth/login.html.j2', form=form, title='Log in')
|
||||||
title='Log in')
|
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/logout')
|
@auth.route('/logout')
|
||||||
@ -58,26 +58,28 @@ def logout():
|
|||||||
def register():
|
def register():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
registration_form = RegistrationForm(prefix='registration-form')
|
form = RegistrationForm(prefix='registration-form')
|
||||||
if registration_form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User(email=registration_form.email.data.lower(),
|
user = User(email=form.email.data.lower(),
|
||||||
password=registration_form.password.data,
|
password=form.password.data,
|
||||||
username=registration_form.username.data)
|
username=form.username.data)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
user_dir = os.path.join(current_app.config['DATA_DIR'],
|
try:
|
||||||
str(user.id))
|
os.makedirs(user.path)
|
||||||
if os.path.exists(user_dir):
|
except OSError:
|
||||||
shutil.rmtree(user_dir)
|
logging.error('Make dir {} led to an OSError!'.format(user.path))
|
||||||
os.mkdir(user_dir)
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
abort(500)
|
||||||
|
else:
|
||||||
token = user.generate_confirmation_token()
|
token = user.generate_confirmation_token()
|
||||||
msg = create_message(user.email, 'Confirm Your Account',
|
msg = create_message(user.email, 'Confirm Your Account',
|
||||||
'auth/email/confirm', token=token, user=user)
|
'auth/email/confirm', token=token, user=user)
|
||||||
send(msg)
|
send(msg)
|
||||||
flash('A confirmation email has been sent to you by email.')
|
flash('A confirmation email has been sent to you by email.')
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('.login'))
|
||||||
return render_template('auth/register.html.j2',
|
return render_template('auth/register.html.j2', form=form,
|
||||||
registration_form=registration_form,
|
|
||||||
title='Register')
|
title='Register')
|
||||||
|
|
||||||
|
|
||||||
@ -92,7 +94,7 @@ def confirm(token):
|
|||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
else:
|
else:
|
||||||
flash('The confirmation link is invalid or has expired.')
|
flash('The confirmation link is invalid or has expired.')
|
||||||
return redirect(url_for('auth.unconfirmed'))
|
return redirect(url_for('.unconfirmed'))
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/unconfirmed')
|
@auth.route('/unconfirmed')
|
||||||
@ -119,23 +121,18 @@ def resend_confirmation():
|
|||||||
def reset_password_request():
|
def reset_password_request():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
reset_password_request_form = ResetPasswordRequestForm(
|
form = ResetPasswordRequestForm(prefix='reset-password-request-form')
|
||||||
prefix='reset-password-request-form')
|
if form.validate_on_submit():
|
||||||
if reset_password_request_form.validate_on_submit():
|
user = User.query.filter_by(email=form.email.data.lower()).first()
|
||||||
submitted_email = reset_password_request_form.email.data
|
if user is not None:
|
||||||
user = User.query.filter_by(email=submitted_email.lower()).first()
|
|
||||||
if user:
|
|
||||||
token = user.generate_reset_token()
|
token = user.generate_reset_token()
|
||||||
msg = create_message(user.email, 'Reset Your Password',
|
msg = create_message(user.email, 'Reset Your Password',
|
||||||
'auth/email/reset_password', token=token,
|
'auth/email/reset_password', token=token,
|
||||||
user=user)
|
user=user)
|
||||||
send(msg)
|
send(msg)
|
||||||
flash('An email with instructions to reset your password has been '
|
flash('An email with instructions to reset your password has been sent to you.') # noqa
|
||||||
'sent to you.')
|
return redirect(url_for('.login'))
|
||||||
return redirect(url_for('auth.login'))
|
return render_template('auth/reset_password_request.html.j2', form=form,
|
||||||
return render_template(
|
|
||||||
'auth/reset_password_request.html.j2',
|
|
||||||
reset_password_request_form=reset_password_request_form,
|
|
||||||
title='Password Reset')
|
title='Password Reset')
|
||||||
|
|
||||||
|
|
||||||
@ -143,15 +140,13 @@ def reset_password_request():
|
|||||||
def reset_password(token):
|
def reset_password(token):
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
reset_password_form = ResetPasswordForm(prefix='reset-password-form')
|
form = ResetPasswordForm(prefix='reset-password-form')
|
||||||
if reset_password_form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
if User.reset_password(token, reset_password_form.password.data):
|
if User.reset_password(token, form.password.data):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Your password has been updated.')
|
flash('Your password has been updated.')
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('.login'))
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
return render_template('auth/reset_password.html.j2',
|
return render_template('auth/reset_password.html.j2', form=form,
|
||||||
reset_password_form=reset_password_form,
|
title='Password Reset', token=token)
|
||||||
title='Password Reset',
|
|
||||||
token=token)
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from flask import (abort, current_app, flash, make_response, redirect, request,
|
from flask import (abort, flash, make_response, redirect, request,
|
||||||
render_template, url_for, send_from_directory)
|
render_template, url_for, send_from_directory)
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from . import corpora
|
from . import corpora
|
||||||
@ -11,6 +11,7 @@ from jsonschema import validate
|
|||||||
from .. import db
|
from .. import db
|
||||||
from ..models import Corpus, CorpusFile, QueryResult
|
from ..models import Corpus, CorpusFile, QueryResult
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import glob
|
import glob
|
||||||
@ -22,64 +23,58 @@ from .import_corpus import check_zip_contents
|
|||||||
@corpora.route('/add', methods=['GET', 'POST'])
|
@corpora.route('/add', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def add_corpus():
|
def add_corpus():
|
||||||
add_corpus_form = AddCorpusForm()
|
form = AddCorpusForm()
|
||||||
if add_corpus_form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
corpus = Corpus(creator=current_user,
|
corpus = Corpus(creator=current_user,
|
||||||
description=add_corpus_form.description.data,
|
description=form.description.data,
|
||||||
status='unprepared', title=add_corpus_form.title.data)
|
title=form.title.data)
|
||||||
db.session.add(corpus)
|
db.session.add(corpus)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
dir = os.path.join(current_app.config['DATA_DIR'],
|
|
||||||
str(corpus.user_id), 'corpora', str(corpus.id))
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(dir)
|
os.makedirs(corpus.path)
|
||||||
except OSError:
|
except OSError:
|
||||||
flash('[ERROR]: Could not add corpus!', 'corpus')
|
logging.error('Make dir {} led to an OSError!'.format(corpus.path))
|
||||||
corpus.delete()
|
db.session.delete(corpus)
|
||||||
else:
|
db.session.commit()
|
||||||
url = url_for('corpora.corpus', corpus_id=corpus.id)
|
abort(500)
|
||||||
flash('[<a href="{}">{}</a>] added'.format(url, corpus.title),
|
flash('Corpus "{}" added!'.format(corpus.title), 'corpus')
|
||||||
'corpus')
|
return redirect(url_for('.corpus', corpus_id=corpus.id))
|
||||||
return redirect(url_for('corpora.corpus', corpus_id=corpus.id))
|
return render_template('corpora/add_corpus.html.j2', form=form,
|
||||||
return render_template('corpora/add_corpus.html.j2',
|
|
||||||
add_corpus_form=add_corpus_form,
|
|
||||||
title='Add corpus')
|
title='Add corpus')
|
||||||
|
|
||||||
|
|
||||||
@corpora.route('/import', methods=['GET', 'POST'])
|
@corpora.route('/import', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def import_corpus():
|
def import_corpus():
|
||||||
import_corpus_form = ImportCorpusForm()
|
form = ImportCorpusForm()
|
||||||
if import_corpus_form.is_submitted():
|
if form.is_submitted():
|
||||||
if not import_corpus_form.validate():
|
if not form.validate():
|
||||||
return make_response(import_corpus_form.errors, 400)
|
return make_response(form.errors, 400)
|
||||||
corpus = Corpus(creator=current_user,
|
corpus = Corpus(creator=current_user,
|
||||||
description=import_corpus_form.description.data,
|
description=form.description.data,
|
||||||
status='unprepared',
|
title=form.title.data)
|
||||||
title=import_corpus_form.title.data)
|
|
||||||
db.session.add(corpus)
|
db.session.add(corpus)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
dir = os.path.join(current_app.config['DATA_DIR'],
|
|
||||||
str(corpus.user_id), 'corpora', str(corpus.id))
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(dir)
|
os.makedirs(corpus.path)
|
||||||
except OSError:
|
except OSError:
|
||||||
flash('[ERROR]: Could not import corpus!', 'corpus')
|
logging.error('Make dir {} led to an OSError!'.format(corpus.path))
|
||||||
corpus.delete()
|
db.session.delete(corpus)
|
||||||
else:
|
db.session.commit()
|
||||||
|
flash('Internal Server Error', 'error')
|
||||||
|
return make_response(
|
||||||
|
{'redirect_url': url_for('.import_corpus')}, 500)
|
||||||
# Upload zip
|
# Upload zip
|
||||||
archive_file = os.path.join(current_app.config['DATA_DIR'], dir,
|
archive_file = os.path.join(corpus.path, form.file.data.filename)
|
||||||
import_corpus_form.file.data.filename)
|
form.file.data.save(archive_file)
|
||||||
corpus_dir = os.path.dirname(archive_file)
|
|
||||||
import_corpus_form.file.data.save(archive_file)
|
|
||||||
# Some checks to verify it is a valid exported corpus
|
# Some checks to verify it is a valid exported corpus
|
||||||
with ZipFile(archive_file, 'r') as zip:
|
with ZipFile(archive_file, 'r') as zip:
|
||||||
contents = zip.namelist()
|
contents = zip.namelist()
|
||||||
if set(check_zip_contents).issubset(contents):
|
if set(check_zip_contents).issubset(contents):
|
||||||
# Unzip
|
# Unzip
|
||||||
shutil.unpack_archive(archive_file, corpus_dir)
|
shutil.unpack_archive(archive_file, corpus.path)
|
||||||
# Register vrt files to corpus
|
# Register vrt files to corpus
|
||||||
vrts = glob.glob(corpus_dir + '/*.vrt')
|
vrts = glob.glob(corpus.path + '/*.vrt')
|
||||||
for file in vrts:
|
for file in vrts:
|
||||||
element_tree = ET.parse(file)
|
element_tree = ET.parse(file)
|
||||||
text_node = element_tree.find('text')
|
text_node = element_tree.find('text')
|
||||||
@ -89,7 +84,6 @@ def import_corpus():
|
|||||||
booktitle=text_node.get('booktitle', 'NULL'),
|
booktitle=text_node.get('booktitle', 'NULL'),
|
||||||
chapter=text_node.get('chapter', 'NULL'),
|
chapter=text_node.get('chapter', 'NULL'),
|
||||||
corpus=corpus,
|
corpus=corpus,
|
||||||
dir=dir,
|
|
||||||
editor=text_node.get('editor', 'NULL'),
|
editor=text_node.get('editor', 'NULL'),
|
||||||
filename=os.path.basename(file),
|
filename=os.path.basename(file),
|
||||||
institution=text_node.get('institution', 'NULL'),
|
institution=text_node.get('institution', 'NULL'),
|
||||||
@ -98,30 +92,23 @@ def import_corpus():
|
|||||||
publisher=text_node.get('publisher', 'NULL'),
|
publisher=text_node.get('publisher', 'NULL'),
|
||||||
publishing_year=text_node.get('publishing_year', ''),
|
publishing_year=text_node.get('publishing_year', ''),
|
||||||
school=text_node.get('school', 'NULL'),
|
school=text_node.get('school', 'NULL'),
|
||||||
title=text_node.get('title', 'NULL'))
|
title=text_node.get('title', 'NULL')
|
||||||
|
)
|
||||||
db.session.add(corpus_file)
|
db.session.add(corpus_file)
|
||||||
# finish import and got to imported corpus
|
# finish import and redirect to imported corpus
|
||||||
url = url_for('corpora.corpus', corpus_id=corpus.id)
|
|
||||||
corpus.status = 'prepared'
|
corpus.status = 'prepared'
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
os.remove(archive_file)
|
os.remove(archive_file)
|
||||||
flash('[<a href="{}">{}</a>] imported'.format(url,
|
flash('Corpus "{}" imported!'.format(corpus.title), 'corpus')
|
||||||
corpus.title),
|
|
||||||
'corpus')
|
|
||||||
return make_response(
|
return make_response(
|
||||||
{'redirect_url': url_for('corpora.corpus',
|
{'redirect_url': url_for('.corpus', corpus_id=corpus.id)}, 201)
|
||||||
corpus_id=corpus.id)},
|
|
||||||
201)
|
|
||||||
else:
|
else:
|
||||||
# If imported zip is not valid delete corpus and give feedback
|
# If imported zip is not valid delete corpus and give feedback
|
||||||
corpus.delete()
|
flash('Can not import corpus "{}" not imported: Invalid archive file!', 'error') # noqa
|
||||||
db.session.commit()
|
tasks.delete_corpus(corpus.id)
|
||||||
flash('Imported corpus is not valid.', 'error')
|
|
||||||
return make_response(
|
return make_response(
|
||||||
{'redirect_url': url_for('corpora.import_corpus')},
|
{'redirect_url': url_for('.import_corpus')}, 201)
|
||||||
201)
|
return render_template('corpora/import_corpus.html.j2', form=form,
|
||||||
return render_template('corpora/import_corpus.html.j2',
|
|
||||||
import_corpus_form=import_corpus_form,
|
|
||||||
title='Import Corpus')
|
title='Import Corpus')
|
||||||
|
|
||||||
|
|
||||||
@ -131,17 +118,9 @@ def corpus(corpus_id):
|
|||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
corpus_files = [dict(filename=corpus_file.filename,
|
corpus_files = [corpus_file.to_dict() for corpus_file in corpus.files]
|
||||||
author=corpus_file.author,
|
return render_template('corpora/corpus.html.j2', corpus=corpus,
|
||||||
title=corpus_file.title,
|
corpus_files=corpus_files, title='Corpus')
|
||||||
publishing_year=corpus_file.publishing_year,
|
|
||||||
corpus_id=corpus.id,
|
|
||||||
id=corpus_file.id)
|
|
||||||
for corpus_file in corpus.files]
|
|
||||||
return render_template('corpora/corpus.html.j2',
|
|
||||||
corpus=corpus,
|
|
||||||
corpus_files=corpus_files,
|
|
||||||
title='Corpus')
|
|
||||||
|
|
||||||
|
|
||||||
@corpora.route('/<int:corpus_id>/export')
|
@corpora.route('/<int:corpus_id>/export')
|
||||||
@ -150,12 +129,11 @@ def export_corpus(corpus_id):
|
|||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
# TODO: Check what happens here
|
||||||
dir = os.path.dirname(corpus.archive_file)
|
dir = os.path.dirname(corpus.archive_file)
|
||||||
filename = os.path.basename(corpus.archive_file)
|
filename = os.path.basename(corpus.archive_file)
|
||||||
return send_from_directory(directory=dir,
|
return send_from_directory(as_attachment=True, directory=dir,
|
||||||
filename=filename,
|
filename=filename, mimetype='zip')
|
||||||
mimetype='zip',
|
|
||||||
as_attachment=True)
|
|
||||||
|
|
||||||
|
|
||||||
@corpora.route('/<int:corpus_id>/analyse')
|
@corpora.route('/<int:corpus_id>/analyse')
|
||||||
@ -168,7 +146,8 @@ def analyse_corpus(corpus_id):
|
|||||||
display_options_form = DisplayOptionsForm(
|
display_options_form = DisplayOptionsForm(
|
||||||
prefix='display-options-form',
|
prefix='display-options-form',
|
||||||
result_context=request.args.get('context', 20),
|
result_context=request.args.get('context', 20),
|
||||||
results_per_page=request.args.get('results_per_page', 30))
|
results_per_page=request.args.get('results_per_page', 30)
|
||||||
|
)
|
||||||
query_form = QueryForm(prefix='query-form',
|
query_form = QueryForm(prefix='query-form',
|
||||||
query=request.args.get('query'))
|
query=request.args.get('query'))
|
||||||
query_download_form = QueryDownloadForm(prefix='query-download-form')
|
query_download_form = QueryDownloadForm(prefix='query-download-form')
|
||||||
@ -177,12 +156,12 @@ def analyse_corpus(corpus_id):
|
|||||||
return render_template(
|
return render_template(
|
||||||
'corpora/analyse_corpus.html.j2',
|
'corpora/analyse_corpus.html.j2',
|
||||||
corpus=corpus,
|
corpus=corpus,
|
||||||
corpus_id=corpus_id,
|
|
||||||
display_options_form=display_options_form,
|
display_options_form=display_options_form,
|
||||||
|
inspect_display_options_form=inspect_display_options_form,
|
||||||
query_form=query_form,
|
query_form=query_form,
|
||||||
query_download_form=query_download_form,
|
query_download_form=query_download_form,
|
||||||
inspect_display_options_form=inspect_display_options_form,
|
title='Corpus analysis'
|
||||||
title='Corpus analysis')
|
)
|
||||||
|
|
||||||
|
|
||||||
@corpora.route('/<int:corpus_id>/delete')
|
@corpora.route('/<int:corpus_id>/delete')
|
||||||
@ -191,8 +170,8 @@ def delete_corpus(corpus_id):
|
|||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
flash('Corpus "{}" marked for deletion!'.format(corpus.title), 'corpus')
|
||||||
tasks.delete_corpus(corpus_id)
|
tasks.delete_corpus(corpus_id)
|
||||||
flash('Corpus deleted!', 'corpus')
|
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
|
||||||
@ -202,43 +181,33 @@ def add_corpus_file(corpus_id):
|
|||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
corpus = Corpus.query.get_or_404(corpus_id)
|
||||||
if not (corpus.creator == current_user or current_user.is_administrator()):
|
if not (corpus.creator == current_user or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
add_corpus_file_form = AddCorpusFileForm(corpus,
|
form = AddCorpusFileForm(corpus, prefix='add-corpus-file-form')
|
||||||
prefix='add-corpus-file-form')
|
if form.is_submitted():
|
||||||
if add_corpus_file_form.is_submitted():
|
if not form.validate():
|
||||||
if not add_corpus_file_form.validate():
|
return make_response(form.errors, 400)
|
||||||
return make_response(add_corpus_file_form.errors, 400)
|
|
||||||
# Save the file
|
# Save the file
|
||||||
dir = os.path.join(str(corpus.user_id), 'corpora', str(corpus.id))
|
form.file.data.save(os.path.join(corpus.path, form.file.data.filename))
|
||||||
add_corpus_file_form.file.data.save(
|
corpus_file = CorpusFile(address=form.address.data,
|
||||||
os.path.join(current_app.config['DATA_DIR'], dir,
|
author=form.author.data,
|
||||||
add_corpus_file_form.file.data.filename))
|
booktitle=form.booktitle.data,
|
||||||
corpus_file = CorpusFile(
|
chapter=form.chapter.data,
|
||||||
address=add_corpus_file_form.address.data,
|
|
||||||
author=add_corpus_file_form.author.data,
|
|
||||||
booktitle=add_corpus_file_form.booktitle.data,
|
|
||||||
chapter=add_corpus_file_form.chapter.data,
|
|
||||||
corpus=corpus,
|
corpus=corpus,
|
||||||
dir=dir,
|
editor=form.editor.data,
|
||||||
editor=add_corpus_file_form.editor.data,
|
filename=form.file.data.filename,
|
||||||
filename=add_corpus_file_form.file.data.filename,
|
institution=form.institution.data,
|
||||||
institution=add_corpus_file_form.institution.data,
|
journal=form.journal.data,
|
||||||
journal=add_corpus_file_form.journal.data,
|
pages=form.pages.data,
|
||||||
pages=add_corpus_file_form.pages.data,
|
publisher=form.publisher.data,
|
||||||
publisher=add_corpus_file_form.publisher.data,
|
publishing_year=form.publishing_year.data,
|
||||||
publishing_year=add_corpus_file_form.publishing_year.data,
|
school=form.school.data,
|
||||||
school=add_corpus_file_form.school.data,
|
title=form.title.data)
|
||||||
title=add_corpus_file_form.title.data)
|
|
||||||
db.session.add(corpus_file)
|
db.session.add(corpus_file)
|
||||||
corpus.status = 'unprepared'
|
corpus.status = 'unprepared'
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Corpus file added!', 'corpus')
|
flash('Corpus file "{}" added!'.format(corpus_file.filename), 'corpus')
|
||||||
return make_response(
|
return make_response({'redirect_url': url_for('.corpus', corpus_id=corpus.id)}, 201) # noqa
|
||||||
{'redirect_url': url_for('corpora.corpus', corpus_id=corpus.id)},
|
return render_template('corpora/add_corpus_file.html.j2', corpus=corpus,
|
||||||
201)
|
form=form, title='Add corpus file')
|
||||||
return render_template('corpora/add_corpus_file.html.j2',
|
|
||||||
corpus=corpus,
|
|
||||||
add_corpus_file_form=add_corpus_file_form,
|
|
||||||
title='Add corpus file')
|
|
||||||
|
|
||||||
|
|
||||||
@corpora.route('/<int:corpus_id>/files/<int:corpus_file_id>/delete')
|
@corpora.route('/<int:corpus_id>/files/<int:corpus_file_id>/delete')
|
||||||
@ -250,9 +219,9 @@ def delete_corpus_file(corpus_id, corpus_file_id):
|
|||||||
if not (corpus_file.corpus.creator == current_user
|
if not (corpus_file.corpus.creator == current_user
|
||||||
or current_user.is_administrator()):
|
or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
flash('Corpus file "{}" marked for deletion!'.format(corpus_file.filename), 'corpus') # noqa
|
||||||
tasks.delete_corpus_file(corpus_file_id)
|
tasks.delete_corpus_file(corpus_file_id)
|
||||||
flash('Corpus file deleted!', 'corpus')
|
return redirect(url_for('.corpus', corpus_id=corpus_id))
|
||||||
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
|
|
||||||
|
|
||||||
|
|
||||||
@corpora.route('/<int:corpus_id>/files/<int:corpus_file_id>/download')
|
@corpora.route('/<int:corpus_id>/files/<int:corpus_file_id>/download')
|
||||||
@ -264,9 +233,8 @@ def download_corpus_file(corpus_id, corpus_file_id):
|
|||||||
if not (corpus_file.corpus.creator == current_user
|
if not (corpus_file.corpus.creator == current_user
|
||||||
or current_user.is_administrator()):
|
or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
dir = os.path.join(current_app.config['DATA_DIR'],
|
return send_from_directory(as_attachment=True,
|
||||||
corpus_file.dir)
|
directory=corpus_file.corpus.path,
|
||||||
return send_from_directory(as_attachment=True, directory=dir,
|
|
||||||
filename=corpus_file.filename)
|
filename=corpus_file.filename)
|
||||||
|
|
||||||
|
|
||||||
@ -274,48 +242,45 @@ def download_corpus_file(corpus_id, corpus_file_id):
|
|||||||
methods=['GET', 'POST'])
|
methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def corpus_file(corpus_id, corpus_file_id):
|
def corpus_file(corpus_id, corpus_file_id):
|
||||||
corpus = Corpus.query.get_or_404(corpus_id)
|
|
||||||
corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
|
corpus_file = CorpusFile.query.get_or_404(corpus_file_id)
|
||||||
if not corpus_file.corpus_id == corpus_id:
|
if corpus_file.corpus_id != corpus_id:
|
||||||
abort(404)
|
abort(404)
|
||||||
if not (corpus_file.corpus.creator == current_user
|
if not (corpus_file.corpus.creator == current_user
|
||||||
or current_user.is_administrator()):
|
or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
edit_corpus_file_form = EditCorpusFileForm(prefix='edit-corpus-file-form')
|
form = EditCorpusFileForm(prefix='edit-corpus-file-form')
|
||||||
if edit_corpus_file_form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
corpus_file.address = edit_corpus_file_form.address.data
|
corpus_file.address = form.address.data
|
||||||
corpus_file.author = edit_corpus_file_form.author.data
|
corpus_file.author = form.author.data
|
||||||
corpus_file.booktitle = edit_corpus_file_form.booktitle.data
|
corpus_file.booktitle = form.booktitle.data
|
||||||
corpus_file.chapter = edit_corpus_file_form.chapter.data
|
corpus_file.chapter = form.chapter.data
|
||||||
corpus_file.editor = edit_corpus_file_form.editor.data
|
corpus_file.editor = form.editor.data
|
||||||
corpus_file.institution = edit_corpus_file_form.institution.data
|
corpus_file.institution = form.institution.data
|
||||||
corpus_file.journal = edit_corpus_file_form.journal.data
|
corpus_file.journal = form.journal.data
|
||||||
corpus_file.pages = edit_corpus_file_form.pages.data
|
corpus_file.pages = form.pages.data
|
||||||
corpus_file.publisher = edit_corpus_file_form.publisher.data
|
corpus_file.publisher = form.publisher.data
|
||||||
corpus_file.publishing_year = \
|
corpus_file.publishing_year = form.publishing_year.data
|
||||||
edit_corpus_file_form.publishing_year.data
|
corpus_file.school = form.school.data
|
||||||
corpus_file.school = edit_corpus_file_form.school.data
|
corpus_file.title = form.title.data
|
||||||
corpus_file.title = edit_corpus_file_form.title.data
|
|
||||||
corpus.status = 'unprepared'
|
corpus.status = 'unprepared'
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Corpus file edited!', 'corpus')
|
flash('Corpus file "{}" edited!'.format(corpus_file.filename), 'corpus') # noqa
|
||||||
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
|
return redirect(url_for('.corpus', corpus_id=corpus_id))
|
||||||
# If no form is submitted or valid, fill out fields with current values
|
# If no form is submitted or valid, fill out fields with current values
|
||||||
edit_corpus_file_form.address.data = corpus_file.address
|
form.address.data = corpus_file.address
|
||||||
edit_corpus_file_form.author.data = corpus_file.author
|
form.author.data = corpus_file.author
|
||||||
edit_corpus_file_form.booktitle.data = corpus_file.booktitle
|
form.booktitle.data = corpus_file.booktitle
|
||||||
edit_corpus_file_form.chapter.data = corpus_file.chapter
|
form.chapter.data = corpus_file.chapter
|
||||||
edit_corpus_file_form.editor.data = corpus_file.editor
|
form.editor.data = corpus_file.editor
|
||||||
edit_corpus_file_form.institution.data = corpus_file.institution
|
form.institution.data = corpus_file.institution
|
||||||
edit_corpus_file_form.journal.data = corpus_file.journal
|
form.journal.data = corpus_file.journal
|
||||||
edit_corpus_file_form.pages.data = corpus_file.pages
|
form.pages.data = corpus_file.pages
|
||||||
edit_corpus_file_form.publisher.data = corpus_file.publisher
|
form.publisher.data = corpus_file.publisher
|
||||||
edit_corpus_file_form.publishing_year.data = corpus_file.publishing_year
|
form.publishing_year.data = corpus_file.publishing_year
|
||||||
edit_corpus_file_form.school.data = corpus_file.school
|
form.school.data = corpus_file.school
|
||||||
edit_corpus_file_form.title.data = corpus_file.title
|
form.title.data = corpus_file.title
|
||||||
return render_template('corpora/corpus_file.html.j2',
|
return render_template('corpora/corpus_file.html.j2', corpus=corpus,
|
||||||
corpus_file=corpus_file, corpus=corpus,
|
corpus_file=corpus_file, form=form,
|
||||||
edit_corpus_file_form=edit_corpus_file_form,
|
|
||||||
title='Edit corpus file')
|
title='Edit corpus file')
|
||||||
|
|
||||||
|
|
||||||
@ -327,10 +292,10 @@ def prepare_corpus(corpus_id):
|
|||||||
abort(403)
|
abort(403)
|
||||||
if corpus.files.all():
|
if corpus.files.all():
|
||||||
tasks.build_corpus(corpus_id)
|
tasks.build_corpus(corpus_id)
|
||||||
flash('Building Corpus...', 'corpus')
|
flash('Corpus "{}" has been marked to get build!', 'corpus')
|
||||||
else:
|
else:
|
||||||
flash('Can not build corpus, please add corpus file(s).', 'corpus')
|
flash('Can not build corpus "{}": No corpus file(s)!', 'error')
|
||||||
return redirect(url_for('corpora.corpus', corpus_id=corpus_id))
|
return redirect(url_for('.corpus', corpus_id=corpus_id))
|
||||||
|
|
||||||
|
|
||||||
# Following are view functions to add, view etc. exported results.
|
# Following are view functions to add, view etc. exported results.
|
||||||
@ -340,35 +305,29 @@ def add_query_result():
|
|||||||
'''
|
'''
|
||||||
View to import a result as a json file.
|
View to import a result as a json file.
|
||||||
'''
|
'''
|
||||||
add_query_result_form = AddQueryResultForm(prefix='add-query-result-form')
|
form = AddQueryResultForm(prefix='add-query-result-form')
|
||||||
if add_query_result_form.is_submitted():
|
if form.is_submitted():
|
||||||
if not add_query_result_form.validate():
|
if not form.validate():
|
||||||
return make_response(add_query_result_form.errors, 400)
|
return make_response(form.errors, 400)
|
||||||
query_result = QueryResult(
|
query_result = QueryResult(creator=current_user,
|
||||||
creator=current_user,
|
description=form.description.data,
|
||||||
description=add_query_result_form.description.data,
|
filename=form.file.data.filename,
|
||||||
filename=add_query_result_form.file.data.filename,
|
title=form.title.data)
|
||||||
title=add_query_result_form.title.data
|
|
||||||
)
|
|
||||||
db.session.add(query_result)
|
db.session.add(query_result)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# create paths to save the uploaded json file
|
|
||||||
query_result_dir = os.path.join(current_app.config['DATA_DIR'],
|
|
||||||
str(current_user.id),
|
|
||||||
'query_results',
|
|
||||||
str(query_result.id))
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(query_result_dir)
|
os.makedirs(query_result.path)
|
||||||
except Exception:
|
except OSError:
|
||||||
|
logging.error('Make dir {} led to an OSError!'.format(query_result.path)) # noqa
|
||||||
db.session.delete(query_result)
|
db.session.delete(query_result)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Internal Server Error', 'error')
|
flash('Internal Server Error', 'error')
|
||||||
redirect_url = url_for('corpora.add_query_result')
|
return make_response(
|
||||||
return make_response({'redirect_url': redirect_url}, 500)
|
{'redirect_url': url_for('.add_query_result')}, 500)
|
||||||
# save the uploaded file
|
# save the uploaded file
|
||||||
query_result_file_path = os.path.join(query_result_dir,
|
query_result_file_path = os.path.join(query_result.path,
|
||||||
query_result.filename)
|
query_result.filename)
|
||||||
add_query_result_form.file.data.save(query_result_file_path)
|
form.file.data.save(query_result_file_path)
|
||||||
# parse json from file
|
# parse json from file
|
||||||
with open(query_result_file_path, 'r') as file:
|
with open(query_result_file_path, 'r') as file:
|
||||||
query_result_file_content = json.load(file)
|
query_result_file_content = json.load(file)
|
||||||
@ -381,19 +340,16 @@ def add_query_result():
|
|||||||
except Exception:
|
except Exception:
|
||||||
tasks.delete_query_result(query_result.id)
|
tasks.delete_query_result(query_result.id)
|
||||||
flash('Uploaded file is invalid', 'result')
|
flash('Uploaded file is invalid', 'result')
|
||||||
redirect_url = url_for('corpora.add_query_result')
|
return make_response(
|
||||||
return make_response({'redirect_url': redirect_url}, 201)
|
{'redirect_url': url_for('.add_query_result')}, 201)
|
||||||
query_result_file_content.pop('matches')
|
query_result_file_content.pop('matches')
|
||||||
query_result_file_content.pop('cpos_lookup')
|
query_result_file_content.pop('cpos_lookup')
|
||||||
query_result.query_metadata = query_result_file_content
|
query_result.query_metadata = query_result_file_content
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Query result added!', 'result')
|
flash('Query result added!', 'result')
|
||||||
redirect_url = url_for('corpora.query_result',
|
return make_response({'redirect_url': url_for('.query_result', query_result_id=query_result.id)}, 201) # noqa
|
||||||
query_result_id=query_result.id)
|
|
||||||
return make_response({'redirect_url': redirect_url}, 201)
|
|
||||||
return render_template('corpora/query_results/add_query_result.html.j2',
|
return render_template('corpora/query_results/add_query_result.html.j2',
|
||||||
add_query_result_form=add_query_result_form,
|
form=form, title='Add query result')
|
||||||
title='Add query result')
|
|
||||||
|
|
||||||
|
|
||||||
@corpora.route('/result/<int:query_result_id>')
|
@corpora.route('/result/<int:query_result_id>')
|
||||||
@ -404,8 +360,7 @@ def query_result(query_result_id):
|
|||||||
or current_user.is_administrator()):
|
or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
return render_template('corpora/query_results/query_result.html.j2',
|
return render_template('corpora/query_results/query_result.html.j2',
|
||||||
query_result=query_result,
|
query_result=query_result, title='Query result')
|
||||||
title='Query result')
|
|
||||||
|
|
||||||
|
|
||||||
@corpora.route('/result/<int:query_result_id>/inspect')
|
@corpora.route('/result/<int:query_result_id>/inspect')
|
||||||
@ -427,13 +382,7 @@ def inspect_query_result(query_result_id):
|
|||||||
inspect_display_options_form = InspectDisplayOptionsForm(
|
inspect_display_options_form = InspectDisplayOptionsForm(
|
||||||
prefix='inspect-display-options-form'
|
prefix='inspect-display-options-form'
|
||||||
)
|
)
|
||||||
query_result_file_path = os.path.join(
|
query_result_file_path = os.path.join(query_result.path, query_result.filename) # noqa
|
||||||
current_app.config['DATA_DIR'],
|
|
||||||
str(current_user.id),
|
|
||||||
'query_results',
|
|
||||||
str(query_result.id),
|
|
||||||
query_result.filename
|
|
||||||
)
|
|
||||||
with open(query_result_file_path, 'r') as query_result_file:
|
with open(query_result_file_path, 'r') as query_result_file:
|
||||||
query_result_file_content = json.load(query_result_file)
|
query_result_file_content = json.load(query_result_file)
|
||||||
return render_template('corpora/query_results/inspect.html.j2',
|
return render_template('corpora/query_results/inspect.html.j2',
|
||||||
@ -452,8 +401,8 @@ def delete_query_result(query_result_id):
|
|||||||
if not (query_result.creator == current_user
|
if not (query_result.creator == current_user
|
||||||
or current_user.is_administrator()):
|
or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
flash('Query result "{}" has been marked for deletion!'.format(query_result), 'result') # noqa
|
||||||
tasks.delete_query_result(query_result_id)
|
tasks.delete_query_result(query_result_id)
|
||||||
flash('Query result deleted!', 'result')
|
|
||||||
return redirect(url_for('services.service', service="corpus_analysis"))
|
return redirect(url_for('services.service', service="corpus_analysis"))
|
||||||
|
|
||||||
|
|
||||||
@ -464,10 +413,5 @@ def download_query_result(query_result_id):
|
|||||||
if not (query_result.creator == current_user
|
if not (query_result.creator == current_user
|
||||||
or current_user.is_administrator()):
|
or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
query_result_dir = os.path.join(current_app.config['DATA_DIR'],
|
return send_from_directory(as_attachment=True, directory=query_result.path,
|
||||||
str(current_user.id),
|
|
||||||
'query_results',
|
|
||||||
str(query_result.id))
|
|
||||||
return send_from_directory(as_attachment=True,
|
|
||||||
directory=query_result_dir,
|
|
||||||
filename=query_result.filename)
|
filename=query_result.filename)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from flask import render_template
|
from flask import current_app, render_template
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from . import mail
|
from . import mail
|
||||||
from .decorators import background
|
from .decorators import background
|
||||||
|
|
||||||
|
|
||||||
def create_message(recipient, subject, template, **kwargs):
|
def create_message(recipient, subject, template, **kwargs):
|
||||||
msg = Message('[nopaque] {}'.format(subject), recipients=[recipient])
|
msg = Message('{} {}'.format(current_app.config['NOPAQUE_MAIL_SUBJECT_PREFIX'], subject), recipients=[recipient]) # noqa
|
||||||
msg.body = render_template('{}.txt.j2'.format(template), **kwargs)
|
msg.body = render_template('{}.txt.j2'.format(template), **kwargs)
|
||||||
msg.html = render_template('{}.html.j2'.format(template), **kwargs)
|
msg.html = render_template('{}.html.j2'.format(template), **kwargs)
|
||||||
return msg
|
return msg
|
||||||
|
@ -2,4 +2,4 @@ from flask import Blueprint
|
|||||||
|
|
||||||
|
|
||||||
jobs = Blueprint('jobs', __name__)
|
jobs = Blueprint('jobs', __name__)
|
||||||
from . import views # noqa
|
from . import views
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
from flask import (abort, current_app, flash, redirect, render_template,
|
from flask import (abort, flash, redirect, render_template,
|
||||||
send_from_directory, url_for)
|
send_from_directory, url_for)
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from . import jobs
|
from . import jobs
|
||||||
from . import tasks
|
from . import tasks
|
||||||
from ..decorators import admin_required
|
from ..decorators import admin_required
|
||||||
from ..models import Job, JobInput, JobResult
|
from ..models import Job, JobInput, JobResult
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
@jobs.route('/<int:job_id>')
|
@jobs.route('/<int:job_id>')
|
||||||
@ -14,13 +13,8 @@ def job(job_id):
|
|||||||
job = Job.query.get_or_404(job_id)
|
job = Job.query.get_or_404(job_id)
|
||||||
if not (job.creator == current_user or current_user.is_administrator()):
|
if not (job.creator == current_user or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
job_inputs = [dict(filename=input.filename,
|
job_inputs = [job_input.to_dict() for job_input in job.inputs]
|
||||||
id=input.id,
|
return render_template('jobs/job.html.j2', job=job, job_inputs=job_inputs,
|
||||||
job_id=job.id)
|
|
||||||
for input in job.inputs]
|
|
||||||
return render_template('jobs/job.html.j2',
|
|
||||||
job=job,
|
|
||||||
job_inputs=job_inputs,
|
|
||||||
title='Job')
|
title='Job')
|
||||||
|
|
||||||
|
|
||||||
@ -31,7 +25,7 @@ def delete_job(job_id):
|
|||||||
if not (job.creator == current_user or current_user.is_administrator()):
|
if not (job.creator == current_user or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
tasks.delete_job(job_id)
|
tasks.delete_job(job_id)
|
||||||
flash('Job has been deleted!', 'job')
|
flash('Job has been marked for deletion!', 'job')
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
|
||||||
@ -44,9 +38,8 @@ def download_job_input(job_id, job_input_id):
|
|||||||
if not (job_input.job.creator == current_user
|
if not (job_input.job.creator == current_user
|
||||||
or current_user.is_administrator()):
|
or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
dir = os.path.join(current_app.config['DATA_DIR'],
|
return send_from_directory(as_attachment=True,
|
||||||
job_input.dir)
|
directory=job_input.job.path,
|
||||||
return send_from_directory(as_attachment=True, directory=dir,
|
|
||||||
filename=job_input.filename)
|
filename=job_input.filename)
|
||||||
|
|
||||||
|
|
||||||
@ -56,11 +49,11 @@ def download_job_input(job_id, job_input_id):
|
|||||||
def restart(job_id):
|
def restart(job_id):
|
||||||
job = Job.query.get_or_404(job_id)
|
job = Job.query.get_or_404(job_id)
|
||||||
if job.status != 'failed':
|
if job.status != 'failed':
|
||||||
flash('Could not restart job: status is not "failed"', 'error')
|
flash('Can not restart job "{}": Status is not "failed"'.format(job.title), 'error') # noqa
|
||||||
else:
|
else:
|
||||||
tasks.restart_job(job_id)
|
tasks.restart_job(job_id)
|
||||||
flash('Job has been restarted!', 'job')
|
flash('Job "{}" has been marked to get restarted!'.format(job.title), 'job') # noqa
|
||||||
return redirect(url_for('jobs.job', job_id=job_id))
|
return redirect(url_for('.job', job_id=job_id))
|
||||||
|
|
||||||
|
|
||||||
@jobs.route('/<int:job_id>/results/<int:job_result_id>/download')
|
@jobs.route('/<int:job_id>/results/<int:job_result_id>/download')
|
||||||
@ -72,7 +65,6 @@ def download_job_result(job_id, job_result_id):
|
|||||||
if not (job_result.job.creator == current_user
|
if not (job_result.job.creator == current_user
|
||||||
or current_user.is_administrator()):
|
or current_user.is_administrator()):
|
||||||
abort(403)
|
abort(403)
|
||||||
dir = os.path.join(current_app.config['DATA_DIR'],
|
return send_from_directory(as_attachment=True,
|
||||||
job_result.dir)
|
directory=job_result.job.path,
|
||||||
return send_from_directory(as_attachment=True, directory=dir,
|
|
||||||
filename=job_result.filename)
|
filename=job_result.filename)
|
||||||
|
@ -2,4 +2,4 @@ from flask import Blueprint
|
|||||||
|
|
||||||
|
|
||||||
main = Blueprint('main', __name__)
|
main = Blueprint('main', __name__)
|
||||||
from . import views # noqa
|
from . import views
|
||||||
|
@ -7,17 +7,16 @@ from ..models import User
|
|||||||
|
|
||||||
@main.route('/', methods=['GET', 'POST'])
|
@main.route('/', methods=['GET', 'POST'])
|
||||||
def index():
|
def index():
|
||||||
login_form = LoginForm(prefix='login-form')
|
form = LoginForm(prefix='login-form')
|
||||||
if login_form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User.query.filter_by(username=login_form.user.data).first()
|
user = User.query.filter_by(username=form.user.data).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
user = User.query.filter_by(email=login_form.user.data).first()
|
user = User.query.filter_by(email=form.user.data.lower()).first()
|
||||||
if user is not None and user.verify_password(login_form.password.data):
|
if user is not None and user.verify_password(form.password.data):
|
||||||
login_user(user, login_form.remember_me.data)
|
login_user(user, form.remember_me.data)
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('.dashboard'))
|
||||||
flash('Invalid email/username or password.')
|
flash('Invalid email/username or password.')
|
||||||
return render_template('main/index.html.j2', login_form=login_form,
|
return render_template('main/index.html.j2', form=form, title='nopaque')
|
||||||
title='nopaque')
|
|
||||||
|
|
||||||
|
|
||||||
@main.route('/about_and_faq')
|
@main.route('/about_and_faq')
|
||||||
@ -31,7 +30,6 @@ def dashboard():
|
|||||||
return render_template('main/dashboard.html.j2', title='Dashboard')
|
return render_template('main/dashboard.html.j2', title='Dashboard')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@main.route('/news')
|
@main.route('/news')
|
||||||
def news():
|
def news():
|
||||||
return render_template('main/news.html.j2', title='News')
|
return render_template('main/news.html.j2', title='News')
|
||||||
@ -40,12 +38,9 @@ def news():
|
|||||||
@main.route('/privacy_policy')
|
@main.route('/privacy_policy')
|
||||||
def privacy_policy():
|
def privacy_policy():
|
||||||
return render_template('main/privacy_policy.html.j2',
|
return render_template('main/privacy_policy.html.j2',
|
||||||
title=('Information on the processing of personal'
|
title='Privacy statement (GDPR)')
|
||||||
' data for the nopaque platform (GDPR)'))
|
|
||||||
|
|
||||||
|
|
||||||
@main.route('/terms_of_use')
|
@main.route('/terms_of_use')
|
||||||
def terms_of_use():
|
def terms_of_use():
|
||||||
return render_template('main/terms_of_use.html.j2',
|
return render_template('main/terms_of_use.html.j2', title='Terms of Use')
|
||||||
title='General Terms of Use of the platform '
|
|
||||||
'nopaque')
|
|
||||||
|
@ -7,6 +7,7 @@ from werkzeug.security import generate_password_hash, check_password_hash
|
|||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from . import db, login_manager
|
from . import db, login_manager
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ class Role(db.Model):
|
|||||||
'''
|
'''
|
||||||
String representation of the Role. For human readability.
|
String representation of the Role. For human readability.
|
||||||
'''
|
'''
|
||||||
return '<Role {role_name}>'.format(role_name=self.name)
|
return '<Role {}>'.format(self.name)
|
||||||
|
|
||||||
def add_permission(self, perm):
|
def add_permission(self, perm):
|
||||||
'''
|
'''
|
||||||
@ -138,6 +139,18 @@ class User(UserMixin, db.Model):
|
|||||||
cascade='save-update, merge, delete',
|
cascade='save-update, merge, delete',
|
||||||
lazy='dynamic')
|
lazy='dynamic')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return os.path.join(current_app.config['NOPAQUE_DATA_DIR'], str(self.id))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def password(self):
|
||||||
|
raise AttributeError('password is not a readable attribute')
|
||||||
|
|
||||||
|
@password.setter
|
||||||
|
def password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {'id': self.id,
|
return {'id': self.id,
|
||||||
'role_id': self.role_id,
|
'role_id': self.role_id,
|
||||||
@ -162,7 +175,7 @@ class User(UserMixin, db.Model):
|
|||||||
'''
|
'''
|
||||||
String representation of the User. For human readability.
|
String representation of the User. For human readability.
|
||||||
'''
|
'''
|
||||||
return '<User {username}>'.format(username=self.username)
|
return '<User {}>'.format(self.username)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(User, self).__init__(**kwargs)
|
super(User, self).__init__(**kwargs)
|
||||||
@ -220,14 +233,6 @@ class User(UserMixin, db.Model):
|
|||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def password(self):
|
|
||||||
raise AttributeError('password is not a readable attribute')
|
|
||||||
|
|
||||||
@password.setter
|
|
||||||
def password(self, password):
|
|
||||||
self.password_hash = generate_password_hash(password)
|
|
||||||
|
|
||||||
def verify_password(self, password):
|
def verify_password(self, password):
|
||||||
return check_password_hash(self.password_hash, password)
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
@ -244,17 +249,11 @@ class User(UserMixin, db.Model):
|
|||||||
'''
|
'''
|
||||||
return self.can(Permission.ADMIN)
|
return self.can(Permission.ADMIN)
|
||||||
|
|
||||||
def ping(self):
|
|
||||||
self.last_seen = datetime.utcnow()
|
|
||||||
db.session.add(self)
|
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
'''
|
'''
|
||||||
Delete the user and its corpora and jobs from database and filesystem.
|
Delete the user and its corpora and jobs from database and filesystem.
|
||||||
'''
|
'''
|
||||||
user_dir = os.path.join(current_app.config['DATA_DIR'],
|
shutil.rmtree(self.path, ignore_errors=True)
|
||||||
str(self.id))
|
|
||||||
shutil.rmtree(user_dir, ignore_errors=True)
|
|
||||||
db.session.delete(self)
|
db.session.delete(self)
|
||||||
|
|
||||||
|
|
||||||
@ -280,14 +279,17 @@ class JobInput(db.Model):
|
|||||||
# Foreign keys
|
# Foreign keys
|
||||||
job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
|
job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
|
||||||
# Fields
|
# Fields
|
||||||
dir = db.Column(db.String(255))
|
|
||||||
filename = db.Column(db.String(255))
|
filename = db.Column(db.String(255))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return os.path.join(self.job.path, self.filename)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
'''
|
'''
|
||||||
String representation of the JobInput. For human readability.
|
String representation of the JobInput. For human readability.
|
||||||
'''
|
'''
|
||||||
return '<JobInput {filename}>'.format(filename=self.filename)
|
return '<JobInput {}>'.format(self.filename)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {'id': self.id,
|
return {'id': self.id,
|
||||||
@ -305,14 +307,17 @@ class JobResult(db.Model):
|
|||||||
# Foreign keys
|
# Foreign keys
|
||||||
job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
|
job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
|
||||||
# Fields
|
# Fields
|
||||||
dir = db.Column(db.String(255))
|
|
||||||
filename = db.Column(db.String(255))
|
filename = db.Column(db.String(255))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return os.path.join(self.job.path, self.filename)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
'''
|
'''
|
||||||
String representation of the JobResult. For human readability.
|
String representation of the JobResult. For human readability.
|
||||||
'''
|
'''
|
||||||
return '<JobResult {filename}>'.format(filename=self.filename)
|
return '<JobResult {}>'.format(self.filename)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {'id': self.id,
|
return {'id': self.id,
|
||||||
@ -351,19 +356,16 @@ class Job(db.Model):
|
|||||||
cascade='save-update, merge, delete')
|
cascade='save-update, merge, delete')
|
||||||
results = db.relationship('JobResult', backref='job', lazy='dynamic',
|
results = db.relationship('JobResult', backref='job', lazy='dynamic',
|
||||||
cascade='save-update, merge, delete')
|
cascade='save-update, merge, delete')
|
||||||
notification_data = db.relationship('NotificationData',
|
|
||||||
cascade='save-update, merge, delete',
|
@property
|
||||||
uselist=False,
|
def path(self):
|
||||||
back_populates='job') # One-to-One relationship
|
return os.path.join(self.creator.path, 'jobs', str(self.id))
|
||||||
notification_email_data = db.relationship('NotificationEmailData',
|
|
||||||
cascade='save-update, merge, delete',
|
|
||||||
back_populates='job')
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
'''
|
'''
|
||||||
String representation of the Job. For human readability.
|
String representation of the Job. For human readability.
|
||||||
'''
|
'''
|
||||||
return '<Job {job_title}>'.format(job_title=self.title)
|
return '<Job {}>'.format(self.title)
|
||||||
|
|
||||||
def create_secure_filename(self):
|
def create_secure_filename(self):
|
||||||
'''
|
'''
|
||||||
@ -385,11 +387,7 @@ class Job(db.Model):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
sleep(1)
|
sleep(1)
|
||||||
db.session.refresh(self)
|
db.session.refresh(self)
|
||||||
job_dir = os.path.join(current_app.config['DATA_DIR'],
|
shutil.rmtree(self.path, ignore_errors=True)
|
||||||
str(self.user_id),
|
|
||||||
'jobs',
|
|
||||||
str(self.id))
|
|
||||||
shutil.rmtree(job_dir, ignore_errors=True)
|
|
||||||
db.session.delete(self)
|
db.session.delete(self)
|
||||||
|
|
||||||
def restart(self):
|
def restart(self):
|
||||||
@ -399,12 +397,8 @@ class Job(db.Model):
|
|||||||
|
|
||||||
if self.status != 'failed':
|
if self.status != 'failed':
|
||||||
raise Exception('Could not restart job: status is not "failed"')
|
raise Exception('Could not restart job: status is not "failed"')
|
||||||
job_dir = os.path.join(current_app.config['DATA_DIR'],
|
shutil.rmtree(os.path.join(self.path, 'output'), ignore_errors=True)
|
||||||
str(self.user_id),
|
shutil.rmtree(os.path.join(self.path, 'pyflow.data'), ignore_errors=True) # noqa
|
||||||
'jobs',
|
|
||||||
str(self.id))
|
|
||||||
shutil.rmtree(os.path.join(job_dir, 'output'), ignore_errors=True)
|
|
||||||
shutil.rmtree(os.path.join(job_dir, 'pyflow.data'), ignore_errors=True)
|
|
||||||
self.end_date = None
|
self.end_date = None
|
||||||
self.status = 'submitted'
|
self.status = 'submitted'
|
||||||
|
|
||||||
@ -425,63 +419,6 @@ class Job(db.Model):
|
|||||||
for result in self.results}}
|
for result in self.results}}
|
||||||
|
|
||||||
|
|
||||||
class NotificationData(db.Model):
|
|
||||||
'''
|
|
||||||
Class to define notification data used for sending a notification mail with
|
|
||||||
nopaque_notify.
|
|
||||||
'''
|
|
||||||
__tablename__ = 'notification_data'
|
|
||||||
# Primary key
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
# Foreign Key
|
|
||||||
job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
|
|
||||||
# relationships
|
|
||||||
job = db.relationship('Job', back_populates='notification_data')
|
|
||||||
# Fields
|
|
||||||
notified_on = db.Column(db.String(16), default=None)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
'''
|
|
||||||
String representation of the NotificationData. For human readability.
|
|
||||||
'''
|
|
||||||
return '<NotificationData {id}>'.format(id=self.id)
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {'id': self.id,
|
|
||||||
'job_id': self.job_id,
|
|
||||||
'job': self.job,
|
|
||||||
'notified': self.notified}
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationEmailData(db.Model):
|
|
||||||
'''
|
|
||||||
Class to define data that will be used to send a corresponding Notification
|
|
||||||
via email.
|
|
||||||
'''
|
|
||||||
__tablename__ = 'notification_email_data'
|
|
||||||
# Primary Key
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
# Foreign Key
|
|
||||||
job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
|
|
||||||
# relationships
|
|
||||||
job = db.relationship('Job', back_populates='notification_email_data')
|
|
||||||
notify_status = db.Column(db.String(16), default=None)
|
|
||||||
creation_date = db.Column(db.DateTime(), default=datetime.utcnow)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
'''
|
|
||||||
String representation of the NotificationEmailData. For human readability.
|
|
||||||
'''
|
|
||||||
return '<NotificationData {id}>'.format(id=self.id)
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {'id': self.id,
|
|
||||||
'job_id': self.job_id,
|
|
||||||
'job': self.job,
|
|
||||||
'notify_status': self.notify_status,
|
|
||||||
'creation_date': self.creation_date}
|
|
||||||
|
|
||||||
|
|
||||||
class CorpusFile(db.Model):
|
class CorpusFile(db.Model):
|
||||||
'''
|
'''
|
||||||
Class to define Files.
|
Class to define Files.
|
||||||
@ -496,7 +433,6 @@ class CorpusFile(db.Model):
|
|||||||
author = db.Column(db.String(255))
|
author = db.Column(db.String(255))
|
||||||
booktitle = db.Column(db.String(255))
|
booktitle = db.Column(db.String(255))
|
||||||
chapter = db.Column(db.String(255))
|
chapter = db.Column(db.String(255))
|
||||||
dir = db.Column(db.String(255))
|
|
||||||
editor = db.Column(db.String(255))
|
editor = db.Column(db.String(255))
|
||||||
filename = db.Column(db.String(255))
|
filename = db.Column(db.String(255))
|
||||||
institution = db.Column(db.String(255))
|
institution = db.Column(db.String(255))
|
||||||
@ -507,15 +443,15 @@ class CorpusFile(db.Model):
|
|||||||
school = db.Column(db.String(255))
|
school = db.Column(db.String(255))
|
||||||
title = db.Column(db.String(255))
|
title = db.Column(db.String(255))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return os.path.join(self.corpus.path, self.filename)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
corpus_file_path = os.path.join(current_app.config['DATA_DIR'],
|
|
||||||
str(self.corpus.user_id),
|
|
||||||
'corpora',
|
|
||||||
str(self.corpus_id),
|
|
||||||
self.filename)
|
|
||||||
try:
|
try:
|
||||||
os.remove(corpus_file_path)
|
os.remove(self.path)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
logging.error('Removing {} led to an OSError!'.format(self.path))
|
||||||
pass
|
pass
|
||||||
db.session.delete(self)
|
db.session.delete(self)
|
||||||
self.corpus.status = 'unprepared'
|
self.corpus.status = 'unprepared'
|
||||||
@ -553,13 +489,17 @@ class Corpus(db.Model):
|
|||||||
description = db.Column(db.String(255))
|
description = db.Column(db.String(255))
|
||||||
last_edited_date = db.Column(db.DateTime(), default=datetime.utcnow)
|
last_edited_date = db.Column(db.DateTime(), default=datetime.utcnow)
|
||||||
max_nr_of_tokens = db.Column(db.BigInteger, default=2147483647)
|
max_nr_of_tokens = db.Column(db.BigInteger, default=2147483647)
|
||||||
status = db.Column(db.String(16))
|
status = db.Column(db.String(16), default='unprepared')
|
||||||
title = db.Column(db.String(32))
|
title = db.Column(db.String(32))
|
||||||
archive_file = db.Column(db.String(255))
|
archive_file = db.Column(db.String(255))
|
||||||
# Relationships
|
# Relationships
|
||||||
files = db.relationship('CorpusFile', backref='corpus', lazy='dynamic',
|
files = db.relationship('CorpusFile', backref='corpus', lazy='dynamic',
|
||||||
cascade='save-update, merge, delete')
|
cascade='save-update, merge, delete')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return os.path.join(self.creator.path, 'corpora', str(self.id))
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {'id': self.id,
|
return {'id': self.id,
|
||||||
'user_id': self.user_id,
|
'user_id': self.user_id,
|
||||||
@ -571,19 +511,14 @@ class Corpus(db.Model):
|
|||||||
'files': {file.id: file.to_dict() for file in self.files}}
|
'files': {file.id: file.to_dict() for file in self.files}}
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
corpus_dir = os.path.join(current_app.config['DATA_DIR'],
|
output_dir = os.path.join(self.path, 'merged')
|
||||||
str(self.user_id),
|
|
||||||
'corpora',
|
|
||||||
str(self.id))
|
|
||||||
output_dir = os.path.join(corpus_dir, 'merged')
|
|
||||||
shutil.rmtree(output_dir, ignore_errors=True)
|
shutil.rmtree(output_dir, ignore_errors=True)
|
||||||
os.mkdir(output_dir)
|
os.mkdir(output_dir)
|
||||||
master_element_tree = ET.ElementTree(
|
master_element_tree = ET.ElementTree(
|
||||||
ET.fromstring('<corpus>\n</corpus>')
|
ET.fromstring('<corpus>\n</corpus>')
|
||||||
)
|
)
|
||||||
for corpus_file in self.files:
|
for corpus_file in self.files:
|
||||||
corpus_file_path = os.path.join(corpus_dir, corpus_file.filename)
|
element_tree = ET.parse(corpus_file.path)
|
||||||
element_tree = ET.parse(corpus_file_path)
|
|
||||||
text_node = element_tree.find('text')
|
text_node = element_tree.find('text')
|
||||||
text_node.set('address', corpus_file.address or "NULL")
|
text_node.set('address', corpus_file.address or "NULL")
|
||||||
text_node.set('author', corpus_file.author)
|
text_node.set('author', corpus_file.author)
|
||||||
@ -597,7 +532,7 @@ class Corpus(db.Model):
|
|||||||
text_node.set('publishing_year', str(corpus_file.publishing_year))
|
text_node.set('publishing_year', str(corpus_file.publishing_year))
|
||||||
text_node.set('school', corpus_file.school or "NULL")
|
text_node.set('school', corpus_file.school or "NULL")
|
||||||
text_node.set('title', corpus_file.title)
|
text_node.set('title', corpus_file.title)
|
||||||
element_tree.write(corpus_file_path)
|
element_tree.write(corpus_file.path)
|
||||||
master_element_tree.getroot().insert(1, text_node)
|
master_element_tree.getroot().insert(1, text_node)
|
||||||
output_file = os.path.join(output_dir, 'corpus.vrt')
|
output_file = os.path.join(output_dir, 'corpus.vrt')
|
||||||
master_element_tree.write(output_file,
|
master_element_tree.write(output_file,
|
||||||
@ -607,18 +542,14 @@ class Corpus(db.Model):
|
|||||||
self.status = 'submitted'
|
self.status = 'submitted'
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
corpus_dir = os.path.join(current_app.config['DATA_DIR'],
|
shutil.rmtree(self.path, ignore_errors=True)
|
||||||
str(self.user_id),
|
|
||||||
'corpora',
|
|
||||||
str(self.id))
|
|
||||||
shutil.rmtree(corpus_dir, ignore_errors=True)
|
|
||||||
db.session.delete(self)
|
db.session.delete(self)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
'''
|
'''
|
||||||
String representation of the corpus. For human readability.
|
String representation of the corpus. For human readability.
|
||||||
'''
|
'''
|
||||||
return '<Corpus {corpus_title}>'.format(corpus_title=self.title)
|
return '<Corpus {}>'.format(self.title)
|
||||||
|
|
||||||
|
|
||||||
class QueryResult(db.Model):
|
class QueryResult(db.Model):
|
||||||
@ -636,12 +567,12 @@ class QueryResult(db.Model):
|
|||||||
query_metadata = db.Column(db.JSON())
|
query_metadata = db.Column(db.JSON())
|
||||||
title = db.Column(db.String(32))
|
title = db.Column(db.String(32))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return os.path.join(self.creator.path, 'query_results', str(self.id))
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
query_result_dir = os.path.join(current_app.config['DATA_DIR'],
|
shutil.rmtree(self.path, ignore_errors=True)
|
||||||
str(self.user_id),
|
|
||||||
'query_results',
|
|
||||||
str(self.id))
|
|
||||||
shutil.rmtree(query_result_dir, ignore_errors=True)
|
|
||||||
db.session.delete(self)
|
db.session.delete(self)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
@ -654,7 +585,7 @@ class QueryResult(db.Model):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
'''
|
'''
|
||||||
String representation of the CorpusAnalysisResult. For human readability.
|
String representation of the QueryResult. For human readability.
|
||||||
'''
|
'''
|
||||||
return '<QueryResult {}>'.format(self.title)
|
return '<QueryResult {}>'.format(self.title)
|
||||||
|
|
||||||
|
@ -1,150 +0,0 @@
|
|||||||
from . import query_results
|
|
||||||
from . import tasks
|
|
||||||
from .. import db
|
|
||||||
from ..corpora.forms import DisplayOptionsForm, InspectDisplayOptionsForm
|
|
||||||
from ..models import QueryResult
|
|
||||||
from .forms import AddQueryResultForm
|
|
||||||
from flask import (abort, current_app, flash, make_response, redirect,
|
|
||||||
render_template, request, send_from_directory, url_for)
|
|
||||||
from flask_login import current_user, login_required
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from jsonschema import validate
|
|
||||||
|
|
||||||
|
|
||||||
@query_results.route('/add', methods=['GET', 'POST'])
|
|
||||||
@login_required
|
|
||||||
def add_query_result():
|
|
||||||
'''
|
|
||||||
View to import a result as a json file.
|
|
||||||
'''
|
|
||||||
add_query_result_form = AddQueryResultForm(prefix='add-query-result-form')
|
|
||||||
if add_query_result_form.is_submitted():
|
|
||||||
if not add_query_result_form.validate():
|
|
||||||
return make_response(add_query_result_form.errors, 400)
|
|
||||||
query_result = QueryResult(
|
|
||||||
creator=current_user,
|
|
||||||
description=add_query_result_form.description.data,
|
|
||||||
filename=add_query_result_form.file.data.filename,
|
|
||||||
title=add_query_result_form.title.data
|
|
||||||
)
|
|
||||||
db.session.add(query_result)
|
|
||||||
db.session.commit()
|
|
||||||
# create paths to save the uploaded json file
|
|
||||||
query_result_dir = os.path.join(current_app.config['DATA_DIR'],
|
|
||||||
str(current_user.id),
|
|
||||||
'query_results',
|
|
||||||
str(query_result.id))
|
|
||||||
try:
|
|
||||||
os.makedirs(query_result_dir)
|
|
||||||
except Exception:
|
|
||||||
db.session.delete(query_result)
|
|
||||||
db.session.commit()
|
|
||||||
flash('Internal Server Error', 'error')
|
|
||||||
redirect_url = url_for('query_results.add_query_result')
|
|
||||||
return make_response({'redirect_url': redirect_url}, 500)
|
|
||||||
# save the uploaded file
|
|
||||||
query_result_file_path = os.path.join(query_result_dir,
|
|
||||||
query_result.filename)
|
|
||||||
add_query_result_form.file.data.save(query_result_file_path)
|
|
||||||
# parse json from file
|
|
||||||
with open(query_result_file_path, 'r') as file:
|
|
||||||
query_result_file_content = json.load(file)
|
|
||||||
# parse json schema
|
|
||||||
with open('app/static/json_schema/nopaque_cqi_py_results_schema.json', 'r') as file: # noqa
|
|
||||||
schema = json.load(file)
|
|
||||||
try:
|
|
||||||
# validate imported json file
|
|
||||||
validate(instance=query_result_file_content, schema=schema)
|
|
||||||
except Exception:
|
|
||||||
tasks.delete_query_result(query_result.id)
|
|
||||||
flash('Uploaded file is invalid', 'result')
|
|
||||||
redirect_url = url_for('query_results.add_query_result')
|
|
||||||
return make_response({'redirect_url': redirect_url}, 201)
|
|
||||||
query_result_file_content.pop('matches')
|
|
||||||
query_result_file_content.pop('cpos_lookup')
|
|
||||||
query_result.query_metadata = query_result_file_content
|
|
||||||
db.session.commit()
|
|
||||||
flash('Query result added!', 'result')
|
|
||||||
redirect_url = url_for('query_results.query_result',
|
|
||||||
query_result_id=query_result.id)
|
|
||||||
return make_response({'redirect_url': redirect_url}, 201)
|
|
||||||
return render_template('corpora/query_results/add_query_result.html.j2',
|
|
||||||
add_query_result_form=add_query_result_form,
|
|
||||||
title='Add query result')
|
|
||||||
|
|
||||||
|
|
||||||
@query_results.route('/<int:query_result_id>')
|
|
||||||
@login_required
|
|
||||||
def query_result(query_result_id):
|
|
||||||
query_result = QueryResult.query.get_or_404(query_result_id)
|
|
||||||
if not (query_result.creator == current_user
|
|
||||||
or current_user.is_administrator()):
|
|
||||||
abort(403)
|
|
||||||
return render_template('corpora/query_results/query_result.html.j2',
|
|
||||||
query_result=query_result,
|
|
||||||
title='Query result')
|
|
||||||
|
|
||||||
|
|
||||||
@query_results.route('/<int:query_result_id>/inspect')
|
|
||||||
@login_required
|
|
||||||
def inspect_query_result(query_result_id):
|
|
||||||
'''
|
|
||||||
View to inspect imported result file in a corpus analysis like interface
|
|
||||||
'''
|
|
||||||
query_result = QueryResult.query.get_or_404(query_result_id)
|
|
||||||
query_metadata = query_result.query_metadata
|
|
||||||
if not (query_result.creator == current_user
|
|
||||||
or current_user.is_administrator()):
|
|
||||||
abort(403)
|
|
||||||
display_options_form = DisplayOptionsForm(
|
|
||||||
prefix='display-options-form',
|
|
||||||
results_per_page=request.args.get('results_per_page', 30),
|
|
||||||
result_context=request.args.get('context', 20)
|
|
||||||
)
|
|
||||||
inspect_display_options_form = InspectDisplayOptionsForm(
|
|
||||||
prefix='inspect-display-options-form'
|
|
||||||
)
|
|
||||||
query_result_file_path = os.path.join(
|
|
||||||
current_app.config['DATA_DIR'],
|
|
||||||
str(current_user.id),
|
|
||||||
'query_results',
|
|
||||||
str(query_result.id),
|
|
||||||
query_result.filename
|
|
||||||
)
|
|
||||||
with open(query_result_file_path, 'r') as query_result_file:
|
|
||||||
query_result_file_content = json.load(query_result_file)
|
|
||||||
return render_template('corpora/query_results/inspect.html.j2',
|
|
||||||
display_options_form=display_options_form,
|
|
||||||
inspect_display_options_form=inspect_display_options_form,
|
|
||||||
query_result_file_content=query_result_file_content,
|
|
||||||
query_metadata=query_metadata,
|
|
||||||
title='Inspect query result')
|
|
||||||
|
|
||||||
|
|
||||||
@query_results.route('/<int:query_result_id>/delete')
|
|
||||||
@login_required
|
|
||||||
def delete_query_result(query_result_id):
|
|
||||||
query_result = QueryResult.query.get_or_404(query_result_id)
|
|
||||||
if not (query_result.creator == current_user
|
|
||||||
or current_user.is_administrator()):
|
|
||||||
abort(403)
|
|
||||||
tasks.delete_query_result(query_result_id)
|
|
||||||
flash('Query result deleted!', 'result')
|
|
||||||
return redirect(url_for('services.service', service="corpus_analysis"))
|
|
||||||
|
|
||||||
|
|
||||||
@query_results.route('/<int:query_result_id>/download')
|
|
||||||
@login_required
|
|
||||||
def download_query_result(query_result_id):
|
|
||||||
query_result = QueryResult.query.get_or_404(query_result_id)
|
|
||||||
if not (query_result.creator == current_user
|
|
||||||
or current_user.is_administrator()):
|
|
||||||
abort(403)
|
|
||||||
query_result_dir = os.path.join(current_app.config['DATA_DIR'],
|
|
||||||
str(current_user.id),
|
|
||||||
'query_results',
|
|
||||||
str(query_result.id))
|
|
||||||
return send_from_directory(as_attachment=True,
|
|
||||||
directory=query_result_dir,
|
|
||||||
filename=query_result.filename)
|
|
@ -2,4 +2,4 @@ from flask import Blueprint
|
|||||||
|
|
||||||
|
|
||||||
services = Blueprint('services', __name__)
|
services = Blueprint('services', __name__)
|
||||||
from . import views # noqa
|
from . import views
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from flask import (abort, current_app, flash, make_response, render_template,
|
from flask import abort, flash, make_response, render_template, url_for
|
||||||
url_for)
|
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from . import services
|
from . import services
|
||||||
@ -7,19 +6,20 @@ from .. import db
|
|||||||
from ..jobs.forms import AddFileSetupJobForm, AddNLPJobForm, AddOCRJobForm
|
from ..jobs.forms import AddFileSetupJobForm, AddNLPJobForm, AddOCRJobForm
|
||||||
from ..models import Job, JobInput
|
from ..models import Job, JobInput
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
SERVICES = {'corpus_analysis': {'name': 'Corpus analysis'},
|
SERVICES = {'corpus_analysis': {'name': 'Corpus analysis'},
|
||||||
'file-setup': {'name': 'File setup',
|
'file-setup': {'name': 'File setup',
|
||||||
'resources': {'mem_mb': 4096, 'n_cores': 4},
|
'resources': {'mem_mb': 4096, 'n_cores': 4},
|
||||||
'add_job_form': AddFileSetupJobForm},
|
'form': AddFileSetupJobForm},
|
||||||
'nlp': {'name': 'Natural Language Processing',
|
'nlp': {'name': 'Natural Language Processing',
|
||||||
'resources': {'mem_mb': 4096, 'n_cores': 2},
|
'resources': {'mem_mb': 4096, 'n_cores': 2},
|
||||||
'add_job_form': AddNLPJobForm},
|
'form': AddNLPJobForm},
|
||||||
'ocr': {'name': 'Optical Character Recognition',
|
'ocr': {'name': 'Optical Character Recognition',
|
||||||
'resources': {'mem_mb': 8192, 'n_cores': 4},
|
'resources': {'mem_mb': 8192, 'n_cores': 4},
|
||||||
'add_job_form': AddOCRJobForm}}
|
'form': AddOCRJobForm}}
|
||||||
|
|
||||||
|
|
||||||
@services.route('/<service>', methods=['GET', 'POST'])
|
@services.route('/<service>', methods=['GET', 'POST'])
|
||||||
@ -30,54 +30,49 @@ def service(service):
|
|||||||
if service == 'corpus_analysis':
|
if service == 'corpus_analysis':
|
||||||
return render_template('services/{}.html.j2'.format(service),
|
return render_template('services/{}.html.j2'.format(service),
|
||||||
title=SERVICES[service]['name'])
|
title=SERVICES[service]['name'])
|
||||||
add_job_form = SERVICES[service]['add_job_form'](prefix='add-job-form')
|
form = SERVICES[service]['form'](prefix='add-job-form')
|
||||||
if add_job_form.is_submitted():
|
if form.is_submitted():
|
||||||
if not add_job_form.validate():
|
if not form.validate():
|
||||||
return make_response(add_job_form.errors, 400)
|
return make_response(form.errors, 400)
|
||||||
service_args = []
|
service_args = []
|
||||||
if service == 'nlp':
|
if service == 'nlp':
|
||||||
service_args.append('-l {}'.format(add_job_form.language.data))
|
service_args.append('-l {}'.format(form.language.data))
|
||||||
if add_job_form.check_encoding.data:
|
if form.check_encoding.data:
|
||||||
service_args.append('--check-encoding')
|
service_args.append('--check-encoding')
|
||||||
if service == 'ocr':
|
if service == 'ocr':
|
||||||
service_args.append('-l {}'.format(add_job_form.language.data))
|
service_args.append('-l {}'.format(form.language.data))
|
||||||
if add_job_form.binarization.data:
|
if form.binarization.data:
|
||||||
service_args.append('--binarize')
|
service_args.append('--binarize')
|
||||||
job = Job(creator=current_user,
|
job = Job(creator=current_user,
|
||||||
description=add_job_form.description.data,
|
description=form.description.data,
|
||||||
mem_mb=SERVICES[service]['resources']['mem_mb'],
|
mem_mb=SERVICES[service]['resources']['mem_mb'],
|
||||||
n_cores=SERVICES[service]['resources']['n_cores'],
|
n_cores=SERVICES[service]['resources']['n_cores'],
|
||||||
service=service, service_args=json.dumps(service_args),
|
service=service, service_args=json.dumps(service_args),
|
||||||
service_version=add_job_form.version.data,
|
service_version=form.version.data,
|
||||||
status='preparing', title=add_job_form.title.data)
|
status='preparing', title=form.title.data)
|
||||||
if job.service != 'corpus_analysis':
|
if job.service != 'corpus_analysis':
|
||||||
job.create_secure_filename()
|
job.create_secure_filename()
|
||||||
db.session.add(job)
|
db.session.add(job)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
relative_dir = os.path.join(str(job.user_id), 'jobs', str(job.id))
|
|
||||||
absolut_dir = os.path.join(current_app.config['DATA_DIR'],
|
|
||||||
relative_dir)
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(absolut_dir)
|
os.makedirs(job.path)
|
||||||
except OSError:
|
except OSError:
|
||||||
job.delete()
|
logging.error('Make dir {} led to an OSError!'.format(job.path))
|
||||||
flash('Internal Server Error', 'job')
|
db.session.delete(job)
|
||||||
return make_response({'redirect_url': url_for('services.service',
|
db.session.commit()
|
||||||
service=service)},
|
flash('Internal Server Error', 'error')
|
||||||
500)
|
return make_response(
|
||||||
|
{'redirect_url': url_for('.service', service=service)}, 500)
|
||||||
else:
|
else:
|
||||||
for file in add_job_form.files.data:
|
for file in form.files.data:
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
file.save(os.path.join(absolut_dir, filename))
|
job_input = JobInput(dir=job.path, filename=filename, job=job)
|
||||||
job_input = JobInput(dir=relative_dir, filename=filename,
|
file.save(job_input.path)
|
||||||
job=job)
|
|
||||||
db.session.add(job_input)
|
db.session.add(job_input)
|
||||||
job.status = 'submitted'
|
job.status = 'submitted'
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
url = url_for('jobs.job', job_id=job.id)
|
flash('Job "{}" added'.format(job.title), 'job')
|
||||||
flash('[<a href="{}">{}</a>] added'.format(url, job.title), 'job')
|
|
||||||
return make_response(
|
return make_response(
|
||||||
{'redirect_url': url_for('jobs.job', job_id=job.id)}, 201)
|
{'redirect_url': url_for('jobs.job', job_id=job.id)}, 201)
|
||||||
return render_template('services/{}.html.j2'.format(service),
|
return render_template('services/{}.html.j2'.format(service),
|
||||||
title=SERVICES[service]['name'],
|
form=form, title=SERVICES[service]['name'])
|
||||||
add_job_form=add_job_form)
|
|
||||||
|
@ -35,7 +35,7 @@ class EditGeneralSettingsForm(FlaskForm):
|
|||||||
'Benutzername',
|
'Benutzername',
|
||||||
validators=[DataRequired(),
|
validators=[DataRequired(),
|
||||||
Length(1, 64),
|
Length(1, 64),
|
||||||
Regexp(current_app.config['ALLOWED_USERNAME_REGEX'],
|
Regexp(current_app.config['NOPAQUE_USERNAME_REGEX'],
|
||||||
message='Usernames must have only letters, numbers,'
|
message='Usernames must have only letters, numbers,'
|
||||||
' dots or underscores')]
|
' dots or underscores')]
|
||||||
)
|
)
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
from flask import current_app, flash, redirect, render_template, url_for
|
from flask import flash, redirect, render_template, url_for
|
||||||
from flask_login import current_user, login_required, logout_user
|
from flask_login import current_user, login_required, logout_user
|
||||||
from . import settings, tasks
|
from . import settings, tasks
|
||||||
from .forms import (ChangePasswordForm, EditGeneralSettingsForm,
|
from .forms import (ChangePasswordForm, EditGeneralSettingsForm,
|
||||||
EditNotificationSettingsForm)
|
EditNotificationSettingsForm)
|
||||||
from .. import db
|
from .. import db
|
||||||
from ..decorators import admin_required
|
|
||||||
from ..models import Role, User
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
@settings.route('/')
|
@settings.route('/')
|
||||||
@ -26,8 +22,7 @@ def change_password():
|
|||||||
flash('Your password has been updated.')
|
flash('Your password has been updated.')
|
||||||
return redirect(url_for('.change_password'))
|
return redirect(url_for('.change_password'))
|
||||||
return render_template('settings/change_password.html.j2',
|
return render_template('settings/change_password.html.j2',
|
||||||
form=form,
|
form=form, title='Change password')
|
||||||
title='Change password')
|
|
||||||
|
|
||||||
|
|
||||||
@settings.route('/edit_general_settings', methods=['GET', 'POST'])
|
@settings.route('/edit_general_settings', methods=['GET', 'POST'])
|
||||||
@ -40,12 +35,12 @@ def edit_general_settings():
|
|||||||
current_user.username = form.username.data
|
current_user.username = form.username.data
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Your changes have been saved.')
|
flash('Your changes have been saved.')
|
||||||
|
return redirect(url_for('.edit_general_settings'))
|
||||||
form.dark_mode.data = current_user.setting_dark_mode
|
form.dark_mode.data = current_user.setting_dark_mode
|
||||||
form.email.data = current_user.email
|
form.email.data = current_user.email
|
||||||
form.username.data = current_user.username
|
form.username.data = current_user.username
|
||||||
return render_template('settings/edit_general_settings.html.j2',
|
return render_template('settings/edit_general_settings.html.j2',
|
||||||
form=form,
|
form=form, title='General settings')
|
||||||
title='General settings')
|
|
||||||
|
|
||||||
|
|
||||||
@settings.route('/edit_notification_settings', methods=['GET', 'POST'])
|
@settings.route('/edit_notification_settings', methods=['GET', 'POST'])
|
||||||
@ -59,13 +54,13 @@ def edit_notification_settings():
|
|||||||
form.job_status_site_notifications.data
|
form.job_status_site_notifications.data
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Your changes have been saved.')
|
flash('Your changes have been saved.')
|
||||||
|
return redirect(url_for('.edit_notification_settings'))
|
||||||
form.job_status_mail_notifications.data = \
|
form.job_status_mail_notifications.data = \
|
||||||
current_user.setting_job_status_mail_notifications
|
current_user.setting_job_status_mail_notifications
|
||||||
form.job_status_site_notifications.data = \
|
form.job_status_site_notifications.data = \
|
||||||
current_user.setting_job_status_site_notifications
|
current_user.setting_job_status_site_notifications
|
||||||
return render_template('settings/edit_notification_settings.html.j2',
|
return render_template('settings/edit_notification_settings.html.j2',
|
||||||
form=form,
|
form=form, title='Notification settings')
|
||||||
title='Notification settings')
|
|
||||||
|
|
||||||
|
|
||||||
@settings.route('/delete')
|
@settings.route('/delete')
|
||||||
@ -76,5 +71,5 @@ def delete():
|
|||||||
"""
|
"""
|
||||||
tasks.delete_user(current_user.id)
|
tasks.delete_user(current_user.id)
|
||||||
logout_user()
|
logout_user()
|
||||||
flash('Your account has been deleted!')
|
flash('Your account has been marked for deletion!')
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
|
@ -11,15 +11,11 @@ def check_corpora():
|
|||||||
corpora = Corpus.query.all()
|
corpora = Corpus.query.all()
|
||||||
for corpus in filter(lambda corpus: corpus.status == 'submitted', corpora):
|
for corpus in filter(lambda corpus: corpus.status == 'submitted', corpora):
|
||||||
corpus_utils.create_build_corpus_service(corpus)
|
corpus_utils.create_build_corpus_service(corpus)
|
||||||
for corpus in filter(lambda corpus: (corpus.status == 'queued'
|
for corpus in filter(lambda corpus: corpus.status in ['queued', 'running'], corpora): # noqa
|
||||||
or corpus.status == 'running'),
|
|
||||||
corpora):
|
|
||||||
corpus_utils.checkout_build_corpus_service(corpus)
|
corpus_utils.checkout_build_corpus_service(corpus)
|
||||||
for corpus in filter(lambda corpus: corpus.status == 'start analysis',
|
for corpus in filter(lambda corpus: corpus.status == 'start analysis', corpora): # noqa
|
||||||
corpora):
|
|
||||||
corpus_utils.create_cqpserver_container(corpus)
|
corpus_utils.create_cqpserver_container(corpus)
|
||||||
for corpus in filter(lambda corpus: corpus.status == 'stop analysis',
|
for corpus in filter(lambda corpus: corpus.status == 'stop analysis', corpora): # noqa
|
||||||
corpora):
|
|
||||||
corpus_utils.remove_cqpserver_container(corpus)
|
corpus_utils.remove_cqpserver_container(corpus)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@ -28,8 +24,6 @@ def check_jobs():
|
|||||||
jobs = Job.query.all()
|
jobs = Job.query.all()
|
||||||
for job in filter(lambda job: job.status == 'submitted', jobs):
|
for job in filter(lambda job: job.status == 'submitted', jobs):
|
||||||
job_utils.create_job_service(job)
|
job_utils.create_job_service(job)
|
||||||
for job in filter(lambda job: job.status == 'queued', jobs):
|
for job in filter(lambda job: job.status in ['queued', 'running'], jobs):
|
||||||
job_utils.checkout_job_service(job)
|
|
||||||
for job in filter(lambda job: job.status == 'running', jobs):
|
|
||||||
job_utils.checkout_job_service(job)
|
job_utils.checkout_job_service(job)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from flask import current_app
|
|
||||||
from . import docker_client
|
from . import docker_client
|
||||||
import docker
|
import docker
|
||||||
import logging
|
import logging
|
||||||
@ -7,20 +6,14 @@ import shutil
|
|||||||
|
|
||||||
|
|
||||||
def create_build_corpus_service(corpus):
|
def create_build_corpus_service(corpus):
|
||||||
corpus_dir = os.path.join(current_app.config['DATA_DIR'],
|
corpus_data_dir = os.path.join(corpus.path, 'data')
|
||||||
str(corpus.user_id),
|
shutil.rmtree(corpus_data_dir, ignore_errors=True)
|
||||||
'corpora',
|
|
||||||
str(corpus.id))
|
|
||||||
corpus_data_dir = os.path.join(corpus_dir, 'data')
|
|
||||||
corpus_file = os.path.join(corpus_dir, 'merged', 'corpus.vrt')
|
|
||||||
corpus_registry_dir = os.path.join(corpus_dir, 'registry')
|
|
||||||
if os.path.exists(corpus_data_dir):
|
|
||||||
shutil.rmtree(corpus_data_dir)
|
|
||||||
if os.path.exists(corpus_registry_dir):
|
|
||||||
shutil.rmtree(corpus_registry_dir)
|
|
||||||
os.mkdir(corpus_data_dir)
|
os.mkdir(corpus_data_dir)
|
||||||
|
corpus_registry_dir = os.path.join(corpus.path, 'registry')
|
||||||
|
shutil.rmtree(corpus_registry_dir, ignore_errors=True)
|
||||||
os.mkdir(corpus_registry_dir)
|
os.mkdir(corpus_registry_dir)
|
||||||
service_args = {
|
corpus_file = os.path.join(corpus.path, 'merged', 'corpus.vrt')
|
||||||
|
service_kwargs = {
|
||||||
'command': 'docker-entrypoint.sh build-corpus',
|
'command': 'docker-entrypoint.sh build-corpus',
|
||||||
'constraints': ['node.role==worker'],
|
'constraints': ['node.role==worker'],
|
||||||
'labels': {'origin': 'nopaque',
|
'labels': {'origin': 'nopaque',
|
||||||
@ -32,30 +25,34 @@ def create_build_corpus_service(corpus):
|
|||||||
'name': 'build-corpus_{}'.format(corpus.id),
|
'name': 'build-corpus_{}'.format(corpus.id),
|
||||||
'restart_policy': docker.types.RestartPolicy()
|
'restart_policy': docker.types.RestartPolicy()
|
||||||
}
|
}
|
||||||
service_image = \
|
service_image = 'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/cqpserver:latest' # noqa
|
||||||
'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/cqpserver:latest'
|
|
||||||
try:
|
try:
|
||||||
docker_client.services.create(service_image, **service_args)
|
docker_client.services.create(service_image, **service_kwargs)
|
||||||
except docker.errors.APIError as e:
|
except docker.errors.APIError as e:
|
||||||
logging.error('create_build_corpus_service({}): '.format(corpus.id)
|
logging.error('Create "{}" service raised '.format(service_kwargs['name']) # noqa
|
||||||
+ '{} (status: {} -> failed)'.format(e, corpus.status))
|
+ '[docker-APIError] The server returned an error. '
|
||||||
corpus.status = 'failed'
|
+ 'Details: {}'.format(e))
|
||||||
else:
|
else:
|
||||||
corpus.status = 'queued'
|
corpus.status = 'queued'
|
||||||
finally:
|
|
||||||
# TODO: send email
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def checkout_build_corpus_service(corpus):
|
def checkout_build_corpus_service(corpus):
|
||||||
service_name = 'build-corpus_{}'.format(corpus.id)
|
service_name = 'build-corpus_{}'.format(corpus.id)
|
||||||
try:
|
try:
|
||||||
service = docker_client.services.get(service_name)
|
service = docker_client.services.get(service_name)
|
||||||
except docker.errors.NotFound as e:
|
except docker.errors.NotFound:
|
||||||
logging.error('checkout_build_corpus_service({}):'.format(corpus.id)
|
logging.error('Get "{}" service raised '.format(service_name)
|
||||||
+ ' {} (stauts: {} -> failed)'.format(e, corpus.status))
|
+ '[docker-NotFound] The service does not exist. '
|
||||||
|
+ '(corpus.status: {} -> failed)'.format(corpus.status))
|
||||||
corpus.status = 'failed'
|
corpus.status = 'failed'
|
||||||
# TODO: handle docker.errors.APIError and docker.errors.InvalidVersion
|
except docker.errors.APIError as e:
|
||||||
|
logging.error('Get "{}" service raised '.format(service_name)
|
||||||
|
+ '[docker-APIError] The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e))
|
||||||
|
except docker.errors.InvalidVersion:
|
||||||
|
logging.error('Get "{}" service raised '.format(service_name)
|
||||||
|
+ '[docker-InvalidVersion] One of the arguments is '
|
||||||
|
+ 'not supported with the current API version.')
|
||||||
else:
|
else:
|
||||||
service_tasks = service.tasks()
|
service_tasks = service.tasks()
|
||||||
if not service_tasks:
|
if not service_tasks:
|
||||||
@ -63,25 +60,23 @@ def checkout_build_corpus_service(corpus):
|
|||||||
task_state = service_tasks[0].get('Status').get('State')
|
task_state = service_tasks[0].get('Status').get('State')
|
||||||
if corpus.status == 'queued' and task_state != 'pending':
|
if corpus.status == 'queued' and task_state != 'pending':
|
||||||
corpus.status = 'running'
|
corpus.status = 'running'
|
||||||
elif corpus.status == 'running' and task_state == 'complete':
|
elif corpus.status == 'running' and task_state in ['complete', 'failed']: # noqa
|
||||||
|
try:
|
||||||
service.remove()
|
service.remove()
|
||||||
corpus.status = 'prepared'
|
except docker.errors.APIError as e:
|
||||||
elif corpus.status == 'running' and task_state == 'failed':
|
logging.error('Remove "{}" service raised '.format(service_name) # noqa
|
||||||
service.remove()
|
+ '[docker-APIError] The server returned an error. ' # noqa
|
||||||
corpus.status = task_state
|
+ 'Details: {}'.format(e))
|
||||||
finally:
|
return
|
||||||
# TODO: send email
|
else:
|
||||||
pass
|
corpus.status = 'prepared' if task_state == 'complete' \
|
||||||
|
else 'failed'
|
||||||
|
|
||||||
|
|
||||||
def create_cqpserver_container(corpus):
|
def create_cqpserver_container(corpus):
|
||||||
corpus_dir = os.path.join(current_app.config['DATA_DIR'],
|
corpus_data_dir = os.path.join(corpus.path, 'data')
|
||||||
str(corpus.user_id),
|
corpus_registry_dir = os.path.join(corpus.path, 'registry')
|
||||||
'corpora',
|
container_kwargs = {
|
||||||
str(corpus.id))
|
|
||||||
corpus_data_dir = os.path.join(corpus_dir, 'data')
|
|
||||||
corpus_registry_dir = os.path.join(corpus_dir, 'registry')
|
|
||||||
container_args = {
|
|
||||||
'command': 'cqpserver',
|
'command': 'cqpserver',
|
||||||
'detach': True,
|
'detach': True,
|
||||||
'volumes': [corpus_data_dir + ':/corpora/data:rw',
|
'volumes': [corpus_data_dir + ':/corpora/data:rw',
|
||||||
@ -89,20 +84,43 @@ def create_cqpserver_container(corpus):
|
|||||||
'name': 'cqpserver_{}'.format(corpus.id),
|
'name': 'cqpserver_{}'.format(corpus.id),
|
||||||
'network': 'nopaque_default'
|
'network': 'nopaque_default'
|
||||||
}
|
}
|
||||||
container_image = \
|
container_image = 'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/cqpserver:latest' # noqa
|
||||||
'gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/cqpserver:latest'
|
# Check if a cqpserver container already exists. If this is the case,
|
||||||
|
# remove it and create a new one
|
||||||
try:
|
try:
|
||||||
container = docker_client.containers.get(container_args['name'])
|
container = docker_client.containers.get(container_kwargs['name'])
|
||||||
except docker.errors.NotFound:
|
except docker.errors.NotFound:
|
||||||
pass
|
pass
|
||||||
except docker.errors.DockerException:
|
except docker.errors.APIError as e:
|
||||||
|
logging.error('Get "{}" container raised '.format(container_kwargs['name'])
|
||||||
|
+ '[docker-APIError] The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e))
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
container.remove(force=True)
|
|
||||||
try:
|
try:
|
||||||
docker_client.containers.run(container_image, **container_args)
|
container.remove(force=True)
|
||||||
except docker.errors.DockerException:
|
except docker.errors.APIError as e:
|
||||||
|
logging.error('Remove "{}" container raised '.format(container_kwargs['name'])
|
||||||
|
+ '[docker-APIError] The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e))
|
||||||
return
|
return
|
||||||
|
try:
|
||||||
|
docker_client.containers.run(container_image, **container_kwargs)
|
||||||
|
except docker.errors.ContainerError:
|
||||||
|
# This case should not occur, because detach is True.
|
||||||
|
logging.error('Run "{}" container raised '.format(container_kwargs['name'])
|
||||||
|
+ '[docker-ContainerError] The container exits with a '
|
||||||
|
+ 'non-zero exit code and detach is False.')
|
||||||
|
corpus.status = 'failed'
|
||||||
|
except docker.errors.ImageNotFound:
|
||||||
|
logging.error('Run "{}" container raised '.format(container_kwargs['name'])
|
||||||
|
+ '[docker-ImageNotFound] The specified image does not '
|
||||||
|
+ 'exist.')
|
||||||
|
corpus.status = 'failed'
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error('Run "{}" container raised '.format(container_kwargs['name'])
|
||||||
|
+ '[docker-APIError] The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e))
|
||||||
else:
|
else:
|
||||||
corpus.status = 'analysing'
|
corpus.status = 'analysing'
|
||||||
|
|
||||||
@ -113,8 +131,17 @@ def remove_cqpserver_container(corpus):
|
|||||||
container = docker_client.containers.get(container_name)
|
container = docker_client.containers.get(container_name)
|
||||||
except docker.errors.NotFound:
|
except docker.errors.NotFound:
|
||||||
pass
|
pass
|
||||||
except docker.errors.DockerException:
|
except docker.errors.APIError as e:
|
||||||
|
logging.error('Get "{}" container raised '.format(container_name)
|
||||||
|
+ '[docker-APIError] The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e))
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
container.remove(force=True)
|
container.remove(force=True)
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error('Remove "{}" container raised '.format(container_name)
|
||||||
|
+ '[docker-APIError] The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e))
|
||||||
|
return
|
||||||
corpus.status = 'prepared'
|
corpus.status = 'prepared'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import current_app
|
|
||||||
from . import docker_client
|
from . import docker_client
|
||||||
from .. import db
|
from .. import db
|
||||||
|
from ..email import create_message, send
|
||||||
from ..models import JobResult
|
from ..models import JobResult
|
||||||
import docker
|
import docker
|
||||||
import logging
|
import logging
|
||||||
@ -10,51 +10,60 @@ import os
|
|||||||
|
|
||||||
|
|
||||||
def create_job_service(job):
|
def create_job_service(job):
|
||||||
job_dir = os.path.join(current_app.config['DATA_DIR'],
|
|
||||||
str(job.user_id),
|
|
||||||
'jobs',
|
|
||||||
str(job.id))
|
|
||||||
cmd = '{} -i /files -o /files/output'.format(job.service)
|
cmd = '{} -i /files -o /files/output'.format(job.service)
|
||||||
if job.service == 'file-setup':
|
if job.service == 'file-setup':
|
||||||
cmd += ' -f {}'.format(job.secure_filename)
|
cmd += ' -f {}'.format(job.secure_filename)
|
||||||
cmd += ' --log-dir /files'
|
cmd += ' --log-dir /files'
|
||||||
cmd += ' --zip [{}]_{}'.format(job.service, job.secure_filename)
|
cmd += ' --zip [{}]_{}'.format(job.service, job.secure_filename)
|
||||||
cmd += ' ' + ' '.join(json.loads(job.service_args))
|
cmd += ' ' + ' '.join(json.loads(job.service_args))
|
||||||
service_args = {'command': cmd,
|
service_kwargs = {'command': cmd,
|
||||||
'constraints': ['node.role==worker'],
|
'constraints': ['node.role==worker'],
|
||||||
'labels': {'origin': 'nopaque',
|
'labels': {'origin': 'nopaque',
|
||||||
'type': 'service.{}'.format(job.service),
|
'type': 'service.{}'.format(job.service),
|
||||||
'job_id': str(job.id)},
|
'job_id': str(job.id)},
|
||||||
'mounts': [job_dir + ':/files:rw'],
|
'mounts': [job.path + ':/files:rw'],
|
||||||
'name': 'job_{}'.format(job.id),
|
'name': 'job_{}'.format(job.id),
|
||||||
'resources': docker.types.Resources(
|
'resources': docker.types.Resources(
|
||||||
cpu_reservation=job.n_cores * (10 ** 9),
|
cpu_reservation=job.n_cores * (10 ** 9),
|
||||||
mem_reservation=job.mem_mb * (10 ** 6)),
|
mem_reservation=job.mem_mb * (10 ** 6)
|
||||||
|
),
|
||||||
'restart_policy': docker.types.RestartPolicy()}
|
'restart_policy': docker.types.RestartPolicy()}
|
||||||
service_image = ('gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/'
|
service_image = ('gitlab.ub.uni-bielefeld.de:4567/sfb1288inf/'
|
||||||
+ job.service + ':' + job.service_version)
|
+ job.service + ':' + job.service_version)
|
||||||
try:
|
try:
|
||||||
docker_client.services.create(service_image, **service_args)
|
docker_client.services.create(service_image, **service_kwargs)
|
||||||
except docker.errors.APIError as e:
|
except docker.errors.APIError as e:
|
||||||
logging.error('create_job_service({}): {} '.format(job.id, e)
|
logging.error('Create "{}" service raised '.format(service_kwargs['name']) # noqa
|
||||||
+ '(status: {} -> failed)'.format(job.status))
|
+ '[docker-APIError] The server returned an error. '
|
||||||
job.status = 'failed'
|
+ 'Details: {}'.format(e))
|
||||||
else:
|
else:
|
||||||
job.status = 'queued'
|
job.status = 'queued'
|
||||||
finally:
|
msg = create_message(
|
||||||
# TODO: send email
|
job.creator.email,
|
||||||
pass
|
'Status update for your Job "{}"'.format(job.title),
|
||||||
|
'tasks/email/notification',
|
||||||
|
job=job
|
||||||
|
)
|
||||||
|
send(msg)
|
||||||
|
|
||||||
|
|
||||||
def checkout_job_service(job):
|
def checkout_job_service(job):
|
||||||
service_name = 'job_{}'.format(job.id)
|
service_name = 'job_{}'.format(job.id)
|
||||||
try:
|
try:
|
||||||
service = docker_client.services.get(service_name)
|
service = docker_client.services.get(service_name)
|
||||||
except docker.errors.NotFound as e:
|
except docker.errors.NotFound:
|
||||||
logging.error('checkout_job_service({}): {} '.format(job.id, e)
|
logging.error('Get "{}" service raised '.format(service_name)
|
||||||
+ '(status: {} -> submitted)'.format(job.status))
|
+ '[docker-NotFound] The service does not exist. '
|
||||||
job.status = 'submitted'
|
+ '(job.status: {} -> failed)'.format(job.status))
|
||||||
# TODO: handle docker.errors.APIError and docker.errors.InvalidVersion
|
job.status = 'failed'
|
||||||
|
except docker.errors.APIError as e:
|
||||||
|
logging.error('Get "{}" service raised '.format(service_name)
|
||||||
|
+ '[docker-APIError] The server returned an error. '
|
||||||
|
+ 'Details: {}'.format(e))
|
||||||
|
except docker.errors.InvalidVersion:
|
||||||
|
logging.error('Get "{}" service raised '.format(service_name)
|
||||||
|
+ '[docker-InvalidVersion] One of the arguments is '
|
||||||
|
+ 'not supported with the current API version.')
|
||||||
else:
|
else:
|
||||||
service_tasks = service.tasks()
|
service_tasks = service.tasks()
|
||||||
if not service_tasks:
|
if not service_tasks:
|
||||||
@ -62,22 +71,16 @@ def checkout_job_service(job):
|
|||||||
task_state = service_tasks[0].get('Status').get('State')
|
task_state = service_tasks[0].get('Status').get('State')
|
||||||
if job.status == 'queued' and task_state != 'pending':
|
if job.status == 'queued' and task_state != 'pending':
|
||||||
job.status = 'running'
|
job.status = 'running'
|
||||||
elif job.status == 'queued' and task_state == 'complete':
|
elif job.status == 'running' and task_state == 'complete':
|
||||||
service.remove()
|
service.remove()
|
||||||
job.end_date = datetime.utcnow()
|
job.end_date = datetime.utcnow()
|
||||||
job.status = task_state
|
job.status = task_state
|
||||||
if task_state == 'complete':
|
if task_state == 'complete':
|
||||||
results_dir = os.path.join(current_app.config['DATA_DIR'],
|
job_results_dir = os.path.join(job.path, 'output')
|
||||||
str(job.user_id),
|
job_results = filter(lambda x: x.endswith('.zip'),
|
||||||
'jobs',
|
os.listdir(job_results_dir))
|
||||||
str(job.id),
|
for job_result in job_results:
|
||||||
'output')
|
job_result = JobResult(filename=job_result, job=job)
|
||||||
results = filter(lambda x: x.endswith('.zip'),
|
|
||||||
os.listdir(results_dir))
|
|
||||||
for result in results:
|
|
||||||
job_result = JobResult(dir=results_dir,
|
|
||||||
filename=result,
|
|
||||||
job_id=job.id)
|
|
||||||
db.session.add(job_result)
|
db.session.add(job_result)
|
||||||
elif job.status == 'running' and task_state == 'failed':
|
elif job.status == 'running' and task_state == 'failed':
|
||||||
service.remove()
|
service.remove()
|
||||||
@ -85,6 +88,13 @@ def checkout_job_service(job):
|
|||||||
job.status = task_state
|
job.status = task_state
|
||||||
finally:
|
finally:
|
||||||
# TODO: send email
|
# TODO: send email
|
||||||
|
msg = create_message(
|
||||||
|
job.creator.email,
|
||||||
|
'[nopaque] Status update for your Job "{}"'.format(job.title),
|
||||||
|
'tasks/email/notification',
|
||||||
|
job=job
|
||||||
|
)
|
||||||
|
send(msg)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,20 +35,20 @@
|
|||||||
<div class="card medium">
|
<div class="card medium">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ login_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ wtf.render_field(login_form.user, material_icon='person') }}
|
{{ wtf.render_field(form.user, material_icon='person') }}
|
||||||
{{ wtf.render_field(login_form.password, material_icon='vpn_key') }}
|
{{ wtf.render_field(form.password, material_icon='vpn_key') }}
|
||||||
<div class="row" style="margin-bottom: 0;">
|
<div class="row" style="margin-bottom: 0;">
|
||||||
<div class="col s6 left-align">
|
<div class="col s6 left-align">
|
||||||
<a href="{{ url_for('.reset_password_request') }}">Forgot your password?</a>
|
<a href="{{ url_for('.reset_password_request') }}">Forgot your password?</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s6 right-align">
|
<div class="col s6 right-align">
|
||||||
{{ wtf.render_field(login_form.remember_me) }}
|
{{ wtf.render_field(form.remember_me) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(login_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,14 +34,14 @@
|
|||||||
<div class="card medium">
|
<div class="card medium">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ registration_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ wtf.render_field(registration_form.username, data_length='64', material_icon='person') }}
|
{{ wtf.render_field(form.username, data_length='64', material_icon='person') }}
|
||||||
{{ wtf.render_field(registration_form.password, data_length='128', material_icon='vpn_key') }}
|
{{ wtf.render_field(form.password, data_length='128', material_icon='vpn_key') }}
|
||||||
{{ wtf.render_field(registration_form.password_confirmation, data_length='128', material_icon='vpn_key') }}
|
{{ wtf.render_field(form.password_confirmation, data_length='128', material_icon='vpn_key') }}
|
||||||
{{ wtf.render_field(registration_form.email, class_='validate', material_icon='email', type='email') }}
|
{{ wtf.render_field(form.email, class_='validate', material_icon='email', type='email') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(registration_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,12 +20,12 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ reset_password_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ wtf.render_field(reset_password_form.password, data_length='128') }}
|
{{ wtf.render_field(form.password, data_length='128') }}
|
||||||
{{ wtf.render_field(reset_password_form.password_confirmation, data_length='128') }}
|
{{ wtf.render_field(form.password_confirmation, data_length='128') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(reset_password_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,11 +20,11 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ reset_password_request_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ wtf.render_field(reset_password_request_form.email, class_='validate', material_icon='email', type='email') }}
|
{{ wtf.render_field(form.email, class_='validate', material_icon='email', type='email') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(reset_password_request_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,18 +27,18 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ add_corpus_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12 m4">
|
<div class="col s12 m4">
|
||||||
{{ wtf.render_field(add_corpus_form.title, data_length='32', material_icon='title') }}
|
{{ wtf.render_field(form.title, data_length='32', material_icon='title') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 m8">
|
<div class="col s12 m8">
|
||||||
{{ wtf.render_field(add_corpus_form.description, data_length='255', material_icon='description') }}
|
{{ wtf.render_field(form.description, data_length='255', material_icon='description') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(add_corpus_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,24 +27,24 @@
|
|||||||
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ add_corpus_file_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12 m4">
|
<div class="col s12 m4">
|
||||||
{{ wtf.render_field(add_corpus_file_form.author, data_length='255', material_icon='person') }}
|
{{ wtf.render_field(form.author, data_length='255', material_icon='person') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 m4">
|
<div class="col s12 m4">
|
||||||
{{ wtf.render_field(add_corpus_file_form.title, data_length='255', material_icon='title') }}
|
{{ wtf.render_field(form.title, data_length='255', material_icon='title') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 m4">
|
<div class="col s12 m4">
|
||||||
{{ wtf.render_field(add_corpus_file_form.publishing_year, material_icon='access_time') }}
|
{{ wtf.render_field(form.publishing_year, material_icon='access_time') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
{{ wtf.render_field(add_corpus_file_form.file, accept='.vrt', placeholder='Choose your .vrt file') }}
|
{{ wtf.render_field(form.file, accept='.vrt', placeholder='Choose your .vrt file') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(add_corpus_file_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
@ -52,7 +52,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="collapsible-header"><i class="material-icons">add</i>Add additional metadata</div>
|
<div class="collapsible-header"><i class="material-icons">add</i>Add additional metadata</div>
|
||||||
<div class="collapsible-body">
|
<div class="collapsible-body">
|
||||||
{% for field in add_corpus_file_form
|
{% for field in form
|
||||||
if field.short_name not in ['author', 'csrf_token', 'file', 'publishing_year', 'submit', 'title'] %}
|
if field.short_name not in ['author', 'csrf_token', 'file', 'publishing_year', 'submit', 'title'] %}
|
||||||
{{ wtf.render_field(field, data_length='255', material_icon=field.label.text[0:1]) }}
|
{{ wtf.render_field(field, data_length='255', material_icon=field.label.text[0:1]) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -155,7 +155,7 @@ import {
|
|||||||
*/
|
*/
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Initialize the client for server client communication in dynamic mode
|
// Initialize the client for server client communication in dynamic mode
|
||||||
let corpusId = {{ corpus_id }}
|
let corpusId = {{ corpus.id }}
|
||||||
const client = new Client({'corpusId': corpusId,
|
const client = new Client({'corpusId': corpusId,
|
||||||
'socket': nopaque.socket,
|
'socket': nopaque.socket,
|
||||||
'logging': true,
|
'logging': true,
|
||||||
|
@ -20,23 +20,23 @@
|
|||||||
|
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
{{ edit_corpus_file_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12 m4">
|
<div class="col s12 m4">
|
||||||
{{ wtf.render_field(edit_corpus_file_form.author, data_length='255', material_icon='person') }}
|
{{ wtf.render_field(form.author, data_length='255', material_icon='person') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 m4">
|
<div class="col s12 m4">
|
||||||
{{ wtf.render_field(edit_corpus_file_form.title, data_length='255', material_icon='title') }}
|
{{ wtf.render_field(form.title, data_length='255', material_icon='title') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 m4">
|
<div class="col s12 m4">
|
||||||
{{ wtf.render_field(edit_corpus_file_form.publishing_year, material_icon='access_time') }}
|
{{ wtf.render_field(form.publishing_year, material_icon='access_time') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(edit_corpus_file_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
@ -44,7 +44,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="collapsible-header"><i class="material-icons">edit</i>Edit additional metadata</div>
|
<div class="collapsible-header"><i class="material-icons">edit</i>Edit additional metadata</div>
|
||||||
<div class="collapsible-body">
|
<div class="collapsible-body">
|
||||||
{% for field in edit_corpus_file_form
|
{% for field in form
|
||||||
if field.short_name not in ['author', 'csrf_token', 'publishing_year', 'submit', 'title'] %}
|
if field.short_name not in ['author', 'csrf_token', 'publishing_year', 'submit', 'title'] %}
|
||||||
{{ wtf.render_field(field, data_length='255', material_icon=field.label.text[0:1]) }}
|
{{ wtf.render_field(field, data_length='255', material_icon=field.label.text[0:1]) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends "nopaque.html.j2" %}
|
{% extends "nopaque.html.j2" %}
|
||||||
{% from '_colors.html.j2' import colors %}
|
{% from '_colors.html.j2' import colors %}
|
||||||
{% import 'materialize/wtf.html.j2' as wtf %}
|
{% import 'materialize/wtf.html.j2' as wtf %}
|
||||||
|
|
||||||
@ -27,23 +27,23 @@
|
|||||||
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ import_corpus_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12 m4">
|
<div class="col s12 m4">
|
||||||
{{ wtf.render_field(import_corpus_form.title, data_length='32', material_icon='title') }}
|
{{ wtf.render_field(form.title, data_length='32', material_icon='title') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 m8">
|
<div class="col s12 m8">
|
||||||
{{ wtf.render_field(import_corpus_form.description, data_length='255', material_icon='description') }}
|
{{ wtf.render_field(form.description, data_length='255', material_icon='description') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
{{ wtf.render_field(import_corpus_form.file, accept='.zip', placeholder='Choose your exported .zip file') }}
|
{{ wtf.render_field(form.file, accept='.zip', placeholder='Choose your exported .zip file') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(import_corpus_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,21 +27,21 @@
|
|||||||
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ add_query_result_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12 m4">
|
<div class="col s12 m4">
|
||||||
{{ wtf.render_field(add_query_result_form.title, data_length='32', material_icon='title') }}
|
{{ wtf.render_field(form.title, data_length='32', material_icon='title') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 m8">
|
<div class="col s12 m8">
|
||||||
{{ wtf.render_field(add_query_result_form.description, data_length='255', material_icon='description') }}
|
{{ wtf.render_field(form.description, data_length='255', material_icon='description') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
{{ wtf.render_field(add_query_result_form.file, accept='.json', placeholder='Choose your .json file') }}
|
{{ wtf.render_field(form.file, accept='.json', placeholder='Choose your .json file') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(add_query_result_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -159,20 +159,20 @@
|
|||||||
<form method="POST">
|
<form method="POST">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<span class="card-title">Log in</span>
|
<span class="card-title">Log in</span>
|
||||||
{{ login_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{{ wtf.render_field(login_form.user, material_icon='person') }}
|
{{ wtf.render_field(form.user, material_icon='person') }}
|
||||||
{{ wtf.render_field(login_form.password, material_icon='vpn_key') }}
|
{{ wtf.render_field(form.password, material_icon='vpn_key') }}
|
||||||
<div class="row" style="margin-bottom: 0;">
|
<div class="row" style="margin-bottom: 0;">
|
||||||
<div class="col s6 left-align">
|
<div class="col s6 left-align">
|
||||||
<a href="{{ url_for('auth.reset_password_request') }}">Forgot your password?</a>
|
<a href="{{ url_for('auth.reset_password_request') }}">Forgot your password?</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s6 right-align">
|
<div class="col s6 right-align">
|
||||||
{{ wtf.render_field(login_form.remember_me) }}
|
{{ wtf.render_field(form.remember_me) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(login_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -231,9 +231,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col s12 m9 right-align">
|
<div class="col s12 m9 right-align">
|
||||||
<a class="btn-small blue waves-effect waves-light" href="{{ url_for('main.about_and_faq') }}"><i class="left material-icons">info_outline</i>About and faq</a>
|
<a class="btn-small blue waves-effect waves-light" href="{{ url_for('main.about_and_faq') }}"><i class="left material-icons">info_outline</i>About and faq</a>
|
||||||
{% if config.CONTACT_EMAIL_ADRESS %}
|
{% if config.NOPAQUE_CONTACT %}
|
||||||
<a class="btn-small pink waves-effect waves-light" href="mailto:{{ config.CONTACT_EMAIL_ADRESS }}?subject=[nopaque] Contact"><i class="left material-icons">rate_review</i>Contact</a>
|
<a class="btn-small pink waves-effect waves-light" href="mailto:{{ config.NOPAQUE_CONTACT }}?subject={{ config.NOPAQUE_MAIL_SUBJECT_PREFIX }} Contact"><i class="left material-icons">rate_review</i>Contact</a>
|
||||||
<a class="btn-small green waves-effect waves-light" href="mailto:{{ config.CONTACT_EMAIL_ADRESS }}?subject=[nopaque] Feedback"><i class="left material-icons">feedback</i>Feedback</a>
|
<a class="btn-small green waves-effect waves-light" href="mailto:{{ config.NOPAQUE_CONTACT }}?subject={{ config.NOPAQUE_MAIL_SUBJECT_PREFIX }} Feedback"><i class="left material-icons">feedback</i>Feedback</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn-small orange waves-effect waves-light" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque"><i class="left material-icons">code</i>GitLab</a>
|
<a class="btn-small orange waves-effect waves-light" href="https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque"><i class="left material-icons">code</i>GitLab</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,24 +48,24 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ add_job_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12 l4">
|
<div class="col s12 l4">
|
||||||
{{ wtf.render_field(add_job_form.title, data_length='32', material_icon='title') }}
|
{{ wtf.render_field(form.title, data_length='32', material_icon='title') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l8">
|
<div class="col s12 l8">
|
||||||
{{ wtf.render_field(add_job_form.description, data_length='255', material_icon='description') }}
|
{{ wtf.render_field(form.description, data_length='255', material_icon='description') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
{{ wtf.render_field(add_job_form.files, accept='image/jpeg, image/png, image/tiff', placeholder='Choose your .jpeg, .png or .tiff files') }}
|
{{ wtf.render_field(form.files, accept='image/jpeg, image/png, image/tiff', placeholder='Choose your .jpeg, .png or .tiff files') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 hide">
|
<div class="col s12 hide">
|
||||||
{{ wtf.render_field(add_job_form.version, material_icon='apps') }}
|
{{ wtf.render_field(form.version, material_icon='apps') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(add_job_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,34 +66,34 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ add_job_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12 l4">
|
<div class="col s12 l4">
|
||||||
{{ wtf.render_field(add_job_form.title, data_length='32', material_icon='title') }}
|
{{ wtf.render_field(form.title, data_length='32', material_icon='title') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l8">
|
<div class="col s12 l8">
|
||||||
{{ wtf.render_field(add_job_form.description, data_length='255', material_icon='description') }}
|
{{ wtf.render_field(form.description, data_length='255', material_icon='description') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l5">
|
<div class="col s12 l5">
|
||||||
{{ wtf.render_field(add_job_form.files, accept='text/plain', placeholder='Choose your .txt files') }}
|
{{ wtf.render_field(form.files, accept='text/plain', placeholder='Choose your .txt files') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l4">
|
<div class="col s12 l4">
|
||||||
{{ wtf.render_field(add_job_form.language, material_icon='language') }}
|
{{ wtf.render_field(form.language, material_icon='language') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l3">
|
<div class="col s12 l3">
|
||||||
{{ wtf.render_field(add_job_form.version, material_icon='apps') }}
|
{{ wtf.render_field(form.version, material_icon='apps') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
<span class="card-title">Preprocessing</span>
|
<span class="card-title">Preprocessing</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s9">
|
<div class="col s9">
|
||||||
<p>{{ add_job_form.check_encoding.label.text }}</p>
|
<p>{{ form.check_encoding.label.text }}</p>
|
||||||
<p class="light">If the input files are not created with the nopaque OCR service or you do not know if your text files are UTF-8 encoded, check this switch. We will try to automatically determine the right encoding for your texts to process them.</p>
|
<p class="light">If the input files are not created with the nopaque OCR service or you do not know if your text files are UTF-8 encoded, check this switch. We will try to automatically determine the right encoding for your texts to process them.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s3 right-align">
|
<div class="col s3 right-align">
|
||||||
<div class="switch">
|
<div class="switch">
|
||||||
<label>
|
<label>
|
||||||
{{ add_job_form.check_encoding() }}
|
{{ form.check_encoding() }}
|
||||||
<span class="lever"></span>
|
<span class="lever"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -107,7 +107,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(add_job_form.submit, material_icon='send') }}
|
{{ wtf.render_field(form.submit, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,34 +48,34 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
<form class="nopaque-submit-form" data-progress-modal="progress-modal">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ add_job_form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s12 l4">
|
<div class="col s12 l4">
|
||||||
{{ wtf.render_field(add_job_form.title, data_length='32', material_icon='title') }}
|
{{ wtf.render_field(form.title, data_length='32', material_icon='title') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l8">
|
<div class="col s12 l8">
|
||||||
{{ wtf.render_field(add_job_form.description, data_length='255', material_icon='description') }}
|
{{ wtf.render_field(form.description, data_length='255', material_icon='description') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l5">
|
<div class="col s12 l5">
|
||||||
{{ wtf.render_field(add_job_form.files, accept='application/pdf', color=ocr_color_darken, placeholder='Choose your .pdf files') }}
|
{{ wtf.render_field(form.files, accept='application/pdf', color=ocr_color_darken, placeholder='Choose your .pdf files') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l4">
|
<div class="col s12 l4">
|
||||||
{{ wtf.render_field(add_job_form.language, material_icon='language') }}
|
{{ wtf.render_field(form.language, material_icon='language') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12 l3">
|
<div class="col s12 l3">
|
||||||
{{ wtf.render_field(add_job_form.version, material_icon='apps') }}
|
{{ wtf.render_field(form.version, material_icon='apps') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col s12">
|
<div class="col s12">
|
||||||
<span class="card-title">Preprocessing</span>
|
<span class="card-title">Preprocessing</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s9">
|
<div class="col s9">
|
||||||
<p>{{ add_job_form.binarization.label.text }}</p>
|
<p>{{ form.binarization.label.text }}</p>
|
||||||
<p class="light">Based on a brightness threshold pixels are converted into either black or white. It is useful to reduce noise in images. (<b>longer duration</b>)</p>
|
<p class="light">Based on a brightness threshold pixels are converted into either black or white. It is useful to reduce noise in images. (<b>longer duration</b>)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col s3 right-align">
|
<div class="col s3 right-align">
|
||||||
<div class="switch">
|
<div class="switch">
|
||||||
<label>
|
<label>
|
||||||
{{ add_job_form.binarization() }}
|
{{ form.binarization() }}
|
||||||
<span class="lever"></span>
|
<span class="lever"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -134,7 +134,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action right-align">
|
<div class="card-action right-align">
|
||||||
{{ wtf.render_field(add_job_form.submit, color=ocr_color_darken, material_icon='send') }}
|
{{ wtf.render_field(form.submit, color=ocr_color_darken, material_icon='send') }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
<p>Dear <b>{{ user.username }}</b>,</p>
|
<p>Dear <b>{{ job.creator.username }}</b>,</p>
|
||||||
|
|
||||||
<p>The status of your Job/Corpus({{ job.id }}) with the title <b>"{{ job.title }}"</b> has changed!</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 }}</b>!</p>
|
||||||
<p>Time of this status update was: <b>{time} UTC</b></p>
|
|
||||||
|
|
||||||
<p>You can access your Job/Corpus here: <a href="{{ url_for('jobs.job', job_id=job.id) }}">{{ url_for('jobs.job', job_id=job.id) }}</a></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>
|
||||||
|
|
||||||
<p>Kind regards!<br>Your nopaque team</p>
|
<p>Kind regards!<br>Your nopaque team</p>
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
Dear {{ user.username }},
|
Dear {{ job.creator.username }},
|
||||||
|
|
||||||
The status of your Job/Corpus({{ job.id }}) with the title "{{ job.title }}" has changed!
|
The status of your Job "{{ job.title }}" has changed!
|
||||||
It is now {{ job.status }}!
|
It is now {{ job.status }}!
|
||||||
Time of this status update was: {time} UTC
|
|
||||||
|
|
||||||
You can access your Job/Corpus here: {{ url_for('jobs.job', job_id=job.id) }}
|
You can access your Job here: {{ url_for('jobs.job', job_id=job.id) }}
|
||||||
|
|
||||||
Kind regards!
|
Kind regards!
|
||||||
Your nopaque team
|
Your nopaque team
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
flask deploy
|
flask deploy
|
||||||
if [[ "$?" == "0" ]]; then
|
if [[ "$?" == "0" ]]; then
|
||||||
|
139
web/config.py
139
web/config.py
@ -7,103 +7,96 @@ ROOT_DIR = os.path.abspath(os.path.dirname(__file__))
|
|||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
''' # Cookies # '''
|
''' # Flask # '''
|
||||||
REMEMBER_COOKIE_HTTPONLY = True
|
SECRET_KEY = os.environ.get('SECRET_KEY', 'hard to guess string')
|
||||||
REMEMBER_COOKIE_SECURE = os.environ.get(
|
SESSION_COOKIE_SECURE = \
|
||||||
'NOPAQUE_REMEMBER_COOKIE_SECURE', 'false').lower() == 'true'
|
os.environ.get('SESSION_COOKIE_SECURE', 'false').lower() == 'true'
|
||||||
SESSION_COOKIE_SECURE = os.environ.get(
|
|
||||||
'NOPAQUE_SESSION_COOKIE_SECURE', 'false').lower() == 'true'
|
|
||||||
|
|
||||||
''' # Database # '''
|
''' # Flask-Login # '''
|
||||||
|
REMEMBER_COOKIE_HTTPONLY = True
|
||||||
|
REMEMBER_COOKIE_SECURE = \
|
||||||
|
os.environ.get('REMEMBER_COOKIE_SECURE', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
''' # Flask-Mail # '''
|
||||||
|
MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER')
|
||||||
|
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||||
|
MAIL_PORT = int(os.environ.get('MAIL_PORT'))
|
||||||
|
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||||
|
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||||
|
MAIL_USE_SSL = os.environ.get('MAIL_USE_SSL', 'false').lower() == 'true'
|
||||||
|
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
''' # Flask-SQLAlchemy # '''
|
||||||
SQLALCHEMY_RECORD_QUERIES = True
|
SQLALCHEMY_RECORD_QUERIES = True
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
''' # Email # '''
|
''' # nopaque # '''
|
||||||
MAIL_DEFAULT_SENDER = os.environ.get('NOPAQUE_SMTP_DEFAULT_SENDER')
|
NOPAQUE_ADMIN = os.environ.get('NOPAQUE_ADMIN')
|
||||||
MAIL_PASSWORD = os.environ.get('NOPAQUE_SMTP_PASSWORD')
|
NOPAQUE_CONTACT = os.environ.get('NOPAQUE_CONTACT')
|
||||||
MAIL_PORT = int(os.environ.get('NOPAQUE_SMTP_PORT'))
|
NOPAQUE_DATA_DIR = os.environ.get('NOPAQUE_DATA_DIR', '/mnt/nopaque')
|
||||||
MAIL_SERVER = os.environ.get('NOPAQUE_SMTP_SERVER')
|
NOPAQUE_MAIL_SUBJECT_PREFIX = '[nopaque]'
|
||||||
MAIL_USERNAME = os.environ.get('NOPAQUE_SMTP_USERNAME')
|
NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI = \
|
||||||
MAIL_USE_SSL = os.environ.get(
|
os.environ.get('NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI')
|
||||||
'NOPAQUE_SMTP_USE_SSL', 'false').lower() == 'true'
|
NOPAQUE_USERNAME_REGEX = '^[A-Za-zÄÖÜäöüß0-9_.]*$'
|
||||||
MAIL_USE_TLS = os.environ.get(
|
|
||||||
'NOPAQUE_SMTP_USE_TLS', 'false').lower() == 'true'
|
|
||||||
|
|
||||||
''' # General # '''
|
|
||||||
ADMIN_EMAIL_ADRESS = os.environ.get('NOPAQUE_ADMIN_EMAIL_ADRESS')
|
|
||||||
ALLOWED_USERNAME_REGEX = '^[A-Za-zÄÖÜäöüß0-9_.]*$'
|
|
||||||
CONTACT_EMAIL_ADRESS = os.environ.get('NOPAQUE_CONTACT_EMAIL_ADRESS')
|
|
||||||
DATA_DIR = os.environ.get('NOPAQUE_DATA_DIR', '/mnt/nopaque')
|
|
||||||
SECRET_KEY = os.environ.get('NOPAQUE_SECRET_KEY', 'hard to guess string')
|
|
||||||
|
|
||||||
''' # Logging # '''
|
|
||||||
LOG_DATE_FORMAT = os.environ.get('NOPAQUE_LOG_DATE_FORMAT',
|
|
||||||
'%Y-%m-%d %H:%M:%S')
|
|
||||||
LOG_FILE = os.environ.get('NOPAQUE_LOG_FILE',
|
|
||||||
os.path.join(ROOT_DIR, 'nopaque.log'))
|
|
||||||
LOG_FORMAT = os.environ.get(
|
|
||||||
'NOPAQUE_LOG_FORMAT',
|
|
||||||
'[%(asctime)s] %(levelname)s in '
|
|
||||||
'%(pathname)s (function: %(funcName)s, line: %(lineno)d): %(message)s'
|
|
||||||
)
|
|
||||||
LOG_LEVEL = os.environ.get('NOPAQUE_LOG_LEVEL', 'WARNING')
|
|
||||||
|
|
||||||
''' # Message queue # '''
|
|
||||||
SOCKETIO_MESSAGE_QUEUE_URI = os.environ.get(
|
|
||||||
'NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI')
|
|
||||||
|
|
||||||
''' # Proxy fix # '''
|
|
||||||
PROXY_FIX_X_FOR = int(os.environ.get('NOPAQUE_PROXY_FIX_X_FOR', '0'))
|
|
||||||
PROXY_FIX_X_HOST = int(os.environ.get('NOPAQUE_PROXY_FIX_X_HOST', '0'))
|
|
||||||
PROXY_FIX_X_PORT = int(os.environ.get('NOPAQUE_PROXY_FIX_X_PORT', '0'))
|
|
||||||
PROXY_FIX_X_PREFIX = int(os.environ.get('NOPAQUE_PROXY_FIX_X_PREFIX', '0'))
|
|
||||||
PROXY_FIX_X_PROTO = int(os.environ.get('NOPAQUE_PROXY_FIX_X_PROTO', '0'))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def init_app(cls, app):
|
def init_app(cls, app):
|
||||||
# Set up logging according to the corresponding (LOG_*) variables
|
# Set up logging according to the corresponding (NOPAQUE_LOG_*)
|
||||||
logging.basicConfig(datefmt=cls.LOG_DATE_FORMAT,
|
# environment variables
|
||||||
filename=cls.LOG_FILE,
|
basic_config_kwargs = {
|
||||||
format=cls.LOG_FORMAT,
|
'datefmt': os.environ.get('NOPAQUE_LOG_DATE_FORMAT',
|
||||||
level=cls.LOG_LEVEL)
|
'%Y-%m-%d %H:%M:%S'),
|
||||||
|
'filename': os.environ.get('NOPAQUE_LOG_FILE',
|
||||||
|
os.path.join(ROOT_DIR, 'nopaque.log')),
|
||||||
|
'format': os.environ.get(
|
||||||
|
'NOPAQUE_LOG_FORMAT',
|
||||||
|
'[%(asctime)s] %(levelname)s in '
|
||||||
|
'%(pathname)s (function: %(funcName)s, line: %(lineno)d): '
|
||||||
|
'%(message)s'
|
||||||
|
),
|
||||||
|
'level': os.environ.get('NOPAQUE_LOG_LEVEL', 'WARNING')
|
||||||
|
}
|
||||||
|
logging.basicConfig(**basic_config_kwargs)
|
||||||
# Set up and apply the ProxyFix middleware according to the
|
# Set up and apply the ProxyFix middleware according to the
|
||||||
# corresponding (PROXY_FIX_*) variables
|
# corresponding (NOPAQUE_PROXY_FIX_*) environment variables
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app,
|
proxy_fix_kwargs = {
|
||||||
x_for=cls.PROXY_FIX_X_FOR,
|
'x_for': int(os.environ.get('NOPAQUE_PROXY_FIX_X_FOR', '0')),
|
||||||
x_host=cls.PROXY_FIX_X_HOST,
|
'x_host': int(os.environ.get('NOPAQUE_PROXY_FIX_X_HOST', '0')),
|
||||||
x_port=cls.PROXY_FIX_X_PORT,
|
'x_port': int(os.environ.get('NOPAQUE_PROXY_FIX_X_PORT', '0')),
|
||||||
x_prefix=cls.PROXY_FIX_X_PREFIX,
|
'x_prefix': int(os.environ.get('NOPAQUE_PROXY_FIX_X_PREFIX', '0')),
|
||||||
x_proto=cls.PROXY_FIX_X_PROTO)
|
'x_proto': int(os.environ.get('NOPAQUE_PROXY_FIX_X_PROTO', '0'))
|
||||||
|
}
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, **proxy_fix_kwargs)
|
||||||
|
|
||||||
|
|
||||||
class DevelopmentConfig(Config):
|
class DevelopmentConfig(Config):
|
||||||
''' # Database # '''
|
''' # Flask # '''
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
''' # Flask-SQLAlchemy # '''
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||||
'NOPAQUE_DEV_DATABASE_URL',
|
'SQLALCHEMY_DATABASE_URI',
|
||||||
'postgresql://nopaque:nopaque@db/nopaque_dev'
|
'postgresql://nopaque:nopaque@db/nopaque_dev'
|
||||||
)
|
)
|
||||||
|
|
||||||
''' # General # '''
|
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
|
|
||||||
class ProductionConfig(Config):
|
class ProductionConfig(Config):
|
||||||
''' # Database # '''
|
''' # Flask-SQLAlchemy # '''
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||||
'NOPAQUE_DATABASE_URL', 'postgresql://nopaque:nopaque@db/nopaque')
|
'SQLALCHEMY_DATABASE_URI', 'postgresql://nopaque:nopaque@db/nopaque')
|
||||||
|
|
||||||
|
|
||||||
class TestingConfig(Config):
|
class TestingConfig(Config):
|
||||||
''' # Database # '''
|
''' # Flask # '''
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
|
||||||
'NOPAQUE_TEST_DATABASE_URL',
|
|
||||||
'postgresql://nopaque:nopaque@db/nopaque_test'
|
|
||||||
)
|
|
||||||
|
|
||||||
''' # General # '''
|
|
||||||
TESTING = True
|
TESTING = True
|
||||||
WTF_CSRF_ENABLED = False
|
WTF_CSRF_ENABLED = False
|
||||||
|
|
||||||
|
''' # Flask-SQLAlchemy # '''
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||||
|
'SQLALCHEMY_DATABASE_URI',
|
||||||
|
'postgresql://nopaque:nopaque@db/nopaque_test'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
config = {'development': DevelopmentConfig,
|
config = {'development': DevelopmentConfig,
|
||||||
'production': ProductionConfig,
|
'production': ProductionConfig,
|
||||||
|
@ -17,8 +17,7 @@ if os.path.exists(DOTENV_FILE):
|
|||||||
|
|
||||||
from app import create_app, db, socketio # noqa
|
from app import create_app, db, socketio # noqa
|
||||||
from app.models import (Corpus, CorpusFile, Job, JobInput, JobResult,
|
from app.models import (Corpus, CorpusFile, Job, JobInput, JobResult,
|
||||||
NotificationData, NotificationEmailData, QueryResult,
|
QueryResult, Role, User) # noqa
|
||||||
Role, User) # noqa
|
|
||||||
from flask_migrate import Migrate, upgrade # noqa
|
from flask_migrate import Migrate, upgrade # noqa
|
||||||
|
|
||||||
|
|
||||||
@ -34,8 +33,6 @@ def make_shell_context():
|
|||||||
'Job': Job,
|
'Job': Job,
|
||||||
'JobInput': JobInput,
|
'JobInput': JobInput,
|
||||||
'JobResult': JobResult,
|
'JobResult': JobResult,
|
||||||
'NotificationData': NotificationData,
|
|
||||||
'NotificationEmailData': NotificationEmailData,
|
|
||||||
'QueryResult': QueryResult,
|
'QueryResult': QueryResult,
|
||||||
'Role': Role,
|
'Role': Role,
|
||||||
'User': User}
|
'User': User}
|
||||||
@ -53,9 +50,9 @@ def deploy():
|
|||||||
|
|
||||||
@app.cli.command()
|
@app.cli.command()
|
||||||
def tasks():
|
def tasks():
|
||||||
from app.tasks import process_corpora, process_jobs
|
from app.tasks import check_corpora, check_jobs
|
||||||
process_corpora()
|
check_corpora()
|
||||||
process_jobs()
|
check_jobs()
|
||||||
|
|
||||||
|
|
||||||
@app.cli.command()
|
@app.cli.command()
|
||||||
|
Loading…
Reference in New Issue
Block a user