diff --git a/app/auth/views.py b/app/auth/views.py index d9111ff1a86bd94cfc8042159051ca9e2cdd6702..9470480f6ca27367331f20879a550bf26734be45 100644 --- a/app/auth/views.py +++ b/app/auth/views.py @@ -5,7 +5,7 @@ from . import auth from .forms import (LoginForm, ResetPasswordForm, ResetPasswordRequestForm, RegistrationForm) from .. import db -from ..email import create_message, send_async +from ..email import create_message, send from ..models import User import os import shutil @@ -70,7 +70,7 @@ def register(): token = user.generate_confirmation_token() msg = create_message(user.email, 'Confirm Your Account', 'auth/email/confirm', token=token, user=user) - send_async(msg) + send(msg) flash('A confirmation email has been sent to you by email.') return redirect(url_for('auth.login')) return render_template('auth/register.html.j2', @@ -107,7 +107,7 @@ def resend_confirmation(): token = current_user.generate_confirmation_token() msg = create_message(current_user.email, 'Confirm Your Account', 'auth/email/confirm', token=token, user=current_user) - send_async(msg) + send(msg) flash('A new confirmation email has been sent to you by email.') return redirect(url_for('auth.unconfirmed')) @@ -126,7 +126,7 @@ def reset_password_request(): msg = create_message(user.email, 'Reset Your Password', 'auth/email/reset_password', token=token, user=user) - send_async(msg) + send(msg) flash('An email with instructions to reset your password has been ' 'sent to you.') return redirect(url_for('auth.login')) diff --git a/app/corpora/background_functions.py b/app/corpora/background_functions.py deleted file mode 100644 index 55d9af6bdb2aee7839f578f2b2e85edaf4ac0b4c..0000000000000000000000000000000000000000 --- a/app/corpora/background_functions.py +++ /dev/null @@ -1,30 +0,0 @@ -from ..models import Corpus, CorpusFile - - -def delete_corpus_(app, corpus_id): - with app.app_context(): - corpus = Corpus.query.get(corpus_id) - if corpus is None: - # raise Exception('Corpus {} not found!'.format(corpus_id)) - pass - else: - corpus.delete() - - -def delete_corpus_file_(app, corpus_file_id): - with app.app_context(): - corpus_file = CorpusFile.query.get(corpus_file_id) - if corpus_file is None: - # raise Exception('Corpus file {} not found!'.format(corpus_file_id)) - pass - else: - corpus_file.delete() - - -def edit_corpus_file_(app, corpus_file_id): - with app.app_context(): - corpus_file = CorpusFile.query.get(corpus_file_id) - if corpus_file is None: - raise Exception('Corpus file {} not found!'.format(corpus_file_id)) - else: - corpus_file.insert_metadata() diff --git a/app/corpora/tasks.py b/app/corpora/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..4bd68ebfa963de0dab58f18e7ccf9e5b143b5f87 --- /dev/null +++ b/app/corpora/tasks.py @@ -0,0 +1,41 @@ +from ..decorators import background +from ..models import Corpus, CorpusFile +import os +import shutil + + +@background +def delete_corpus(app, corpus_id): + with app.app_context(): + corpus = Corpus.query.get(corpus_id) + if corpus is None: + return + path = os.path.join(app.config['NOPAQUE_STORAGE'], str(corpus.user_id), + 'corpora', str(corpus.id)) + shutil.rmtree(path, ignore_errors=True) + corpus.delete() + + +@background +def delete_corpus_file(app, corpus_file_id): + with app.app_context(): + corpus_file = CorpusFile.query.get(corpus_file_id) + if corpus_file is None: + return + path = os.path.join(app.config['NOPAQUE_STORAGE'], corpus_file.dir, + corpus_file.filename) + try: + os.remove(path) + except Exception: + pass + else: + corpus_file.delete() + + +@background +def edit_corpus_file(app, corpus_file_id): + with app.app_context(): + corpus_file = CorpusFile.query.get(corpus_file_id) + if corpus_file is None: + raise Exception('Corpus file {} not found!'.format(corpus_file_id)) + corpus_file.insert_metadata() diff --git a/app/corpora/views.py b/app/corpora/views.py index 1e47c9e3cd15c3dae61cafb7eb36de855ed8687b..8f4053abe42a3a78ce2f361575a8aaa149b569cd 100644 --- a/app/corpora/views.py +++ b/app/corpora/views.py @@ -1,10 +1,8 @@ from flask import (abort, current_app, flash, make_response, redirect, request, render_template, url_for, send_from_directory) from flask_login import current_user, login_required -from threading import Thread from . import corpora -from .background_functions import (delete_corpus_, delete_corpus_file_, - edit_corpus_file_) +from . import tasks from .forms import (AddCorpusFileForm, AddCorpusForm, EditCorpusFileForm, QueryDownloadForm, QueryForm, DisplayOptionsForm, InspectDisplayOptionsForm) @@ -78,9 +76,7 @@ def delete_corpus(corpus_id): corpus = Corpus.query.get_or_404(corpus_id) if not (corpus.creator == current_user or current_user.is_administrator()): abort(403) - thread = Thread(target=delete_corpus_, - args=(current_app._get_current_object(), corpus.id)) - thread.start() + tasks.delete_corpus(corpus_id) flash('Corpus deleted!') return redirect(url_for('main.dashboard')) @@ -119,10 +115,7 @@ def add_corpus_file(corpus_id): title=add_corpus_file_form.title.data) db.session.add(corpus_file) db.session.commit() - thread = Thread(target=edit_corpus_file_, - args=(current_app._get_current_object(), - corpus_file.id)) - thread.start() + tasks.edit_corpus_file(corpus_file.id) flash('Corpus file added!') return make_response( {'redirect_url': url_for('corpora.corpus', corpus_id=corpus.id)}, @@ -142,9 +135,7 @@ def delete_corpus_file(corpus_id, corpus_file_id): if not (corpus_file.corpus.creator == current_user or current_user.is_administrator()): abort(403) - thread = Thread(target=delete_corpus_file_, - args=(current_app._get_current_object(), corpus_file.id)) - thread.start() + tasks.delete_corpus_file(corpus_file_id) flash('Corpus file deleted!') return redirect(url_for('corpora.corpus', corpus_id=corpus_id)) @@ -191,10 +182,7 @@ def edit_corpus_file(corpus_id, corpus_file_id): corpus_file.school = edit_corpus_file_form.school.data corpus_file.title = edit_corpus_file_form.title.data db.session.commit() - thread = Thread(target=edit_corpus_file_, - args=(current_app._get_current_object(), - corpus_file.id)) - thread.start() + tasks.edit_corpus_file(corpus_file_id) flash('Corpus file edited!') return redirect(url_for('corpora.corpus', corpus_id=corpus_id)) # If no form is submitted or valid, fill out fields with current values diff --git a/app/decorators.py b/app/decorators.py index c218e31458958b98c6986756fe1b21d4b53ba810..fe740fefa493ce62581a4abed2c591ce4da1e107 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -1,33 +1,45 @@ -from flask import abort +from flask import abort, current_app from flask_login import current_user from flask_socketio import disconnect from functools import wraps -from .models import Permission +from threading import Thread def admin_required(f): @wraps(f) def wrapped(*args, **kwargs): - if not current_user.can(Permission.ADMIN): + if current_user.is_administrator: + return f(*args, **kwargs) + else: abort(403) - return f(*args, **kwargs) return wrapped -def socketio_login_required(f): +def background(f): + ''' This decorator executes a function in a Thread ''' @wraps(f) def wrapped(*args, **kwargs): - if not current_user.is_authenticated: - disconnect() - else: - return f(*args, **kwargs) + app = current_app._get_current_object() + thread = Thread(target=f, args=(app, *args), kwargs=kwargs) + thread.start() + return thread return wrapped def socketio_admin_required(f): @wraps(f) def wrapped(*args, **kwargs): - if not current_user.can(Permission.ADMIN): + if current_user.is_administrator: + return f(*args, **kwargs) + else: + disconnect() + return wrapped + + +def socketio_login_required(f): + @wraps(f) + def wrapped(*args, **kwargs): + if not current_user.is_authenticated: disconnect() else: return f(*args, **kwargs) diff --git a/app/email.py b/app/email.py index ab24c7642bb001426c452790d00f4b9b8e92575d..88effaf9f68640b18ea267d79a4ffb174eead0ab 100644 --- a/app/email.py +++ b/app/email.py @@ -1,7 +1,7 @@ from flask import current_app, render_template from flask_mail import Message -from threading import Thread from . import mail +from .decorators import background def create_message(recipient, subject, template, **kwargs): @@ -15,13 +15,7 @@ def create_message(recipient, subject, template, **kwargs): return msg +@background def send(app, msg): with app.app_context(): mail.send(msg) - - -def send_async(msg): - app = current_app._get_current_object() - thread = Thread(target=send, args=(app, msg)) - thread.start() - return thread diff --git a/app/jobs/background_functions.py b/app/jobs/background_functions.py deleted file mode 100644 index 6808be497ac05b0e8ed4eb37730b751577512d0d..0000000000000000000000000000000000000000 --- a/app/jobs/background_functions.py +++ /dev/null @@ -1,9 +0,0 @@ -from ..models import Job - - -def delete_job_(app, job_id): - with app.app_context(): - job = Job.query.get(job_id) - if job is None: - raise Exception('Job {} not found!'.format(job_id)) - job.delete() diff --git a/app/jobs/tasks.py b/app/jobs/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..56b4462c1bbfb8b09d22713295c421976a6519e8 --- /dev/null +++ b/app/jobs/tasks.py @@ -0,0 +1,28 @@ +from time import sleep +from .. import db +from ..decorators import background +from ..models import Job +import os +import shutil + + +@background +def delete_job(app, job_id): + with app.app_context(): + job = Job.query.get(job_id) + if job is None: + return + if job.status not in ['complete', 'failed']: + job.status = 'canceling' + db.session.commit() + while job.status != 'canceled': + # In case the daemon handled a job in any way + if job.status != 'canceling': + job.status = 'canceling' + db.session.commit() + sleep(1) + db.session.refresh(job) + path = os.path.join(app.config['NOPAQUE_STORAGE'], str(job.user_id), + 'jobs', str(job.id)) + shutil.rmtree(path, ignore_errors=True) + job.delete() diff --git a/app/jobs/views.py b/app/jobs/views.py index 4afd2cb3694edb933646de8e806b11a5d6bc824e..fe5ac9b25dc5cd986e038218aec2798435d8aeba 100644 --- a/app/jobs/views.py +++ b/app/jobs/views.py @@ -1,9 +1,8 @@ from flask import (abort, current_app, flash, redirect, render_template, send_from_directory, url_for) from flask_login import current_user, login_required -from threading import Thread from . import jobs -from .background_functions import delete_job_ +from . import tasks from ..models import Job, JobInput, JobResult import os @@ -23,9 +22,7 @@ def delete_job(job_id): job = Job.query.get_or_404(job_id) if not (job.creator == current_user or current_user.is_administrator()): abort(403) - thread = Thread(target=delete_job_, - args=(current_app._get_current_object(), job_id)) - thread.start() + tasks.delete_job(job_id) flash('Job has been deleted!') return redirect(url_for('main.dashboard')) diff --git a/app/models.py b/app/models.py index 4925be7129bb35835784e903e67e56cfd79fe667..378800dc819aef9d84c8ee97a1d34192d6126d4e 100644 --- a/app/models.py +++ b/app/models.py @@ -2,7 +2,6 @@ from datetime import datetime from flask import current_app from flask_login import UserMixin, AnonymousUserMixin from itsdangerous import BadSignature, TimedJSONWebSignatureSerializer -from time import sleep from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.utils import secure_filename from . import db, logger, login_manager @@ -326,26 +325,12 @@ class Job(db.Model): def delete(self): """ - Delete the job and its inputs and outputs from database and filesystem. - """ - if self.status != 'complete' and self.status != 'failed': - self.status = 'canceling' - db.session.commit() - while self.status != 'canceled': - # In case the daemon handled a job in any way - if self.status != 'canceling': - self.status = 'canceling' - db.session.commit() - sleep(1) - db.session.refresh(self) - path = os.path.join(current_app.config['NOPAQUE_STORAGE'], - str(self.user_id), 'jobs', str(self.id)) - try: - shutil.rmtree(path) - except Exception as e: - ''' TODO: Proper exception handling ''' - logger.warning(e) - pass + Delete the job and its inputs and results from the database. + """ + for input in self.inputs: + db.session.delete(input) + for result in self.results: + db.session.delete(result) db.session.delete(self) db.session.commit() @@ -391,14 +376,6 @@ class CorpusFile(db.Model): corpus_id = db.Column(db.Integer, db.ForeignKey('corpora.id')) def delete(self): - path = os.path.join(current_app.config['NOPAQUE_STORAGE'], - self.dir, self.filename) - try: - os.remove(path) - except Exception as e: - ''' TODO: Proper exception handling ''' - logger.warning(e) - pass self.corpus.status = 'unprepared' db.session.delete(self) db.session.commit() @@ -460,12 +437,6 @@ class Corpus(db.Model): files = db.relationship('CorpusFile', backref='corpus', lazy='dynamic', cascade='save-update, merge, delete') - def __repr__(self): - """ - String representation of the corpus. For human readability. - """ - return '<Corpus %r>' % self.title - def to_dict(self): return {'id': self.id, 'creation_date': self.creation_date.timestamp(), @@ -475,22 +446,20 @@ class Corpus(db.Model): 'title': self.title, 'user_id': self.user_id} + def build(self): + pass + def delete(self): for corpus_file in self.files: - corpus_file.delete() - path = os.path.join(current_app.config['NOPAQUE_STORAGE'], - str(self.user_id), 'corpora', str(self.id)) - try: - shutil.rmtree(path) - except Exception as e: - ''' TODO: Proper exception handling ''' - logger.warning(e) - pass + db.session.delete(corpus_file) db.session.delete(self) db.session.commit() - def prepare(self): - pass + def __repr__(self): + """ + String representation of the corpus. For human readability. + """ + return '<Corpus %r>' % self.title ''' diff --git a/app/templates/macros/materialize.html.j2 b/app/templates/macros/materialize.html.j2 index 0155402a778637fa8636d292956af5286bfd3eb8..5b063dfab875f9d59a170b6271ad95b99bd18c95 100644 --- a/app/templates/macros/materialize.html.j2 +++ b/app/templates/macros/materialize.html.j2 @@ -9,6 +9,8 @@ {% if field.type == 'BooleanField' %} {{ render_boolean_field(field, *args, **kwargs) }} + {% elif field.type == 'DecimalRangeField' %} + {{ render_decimal_range_field(field, *args, **kwargs) }} {% elif field.type == 'IntegerField' %} {% set tmp = kwargs.update({'type': 'number'}) %} {% if 'class_' in kwargs and 'validate' not in kwargs['class_'] %} @@ -42,6 +44,12 @@ </div> {% endmacro %} +{% macro render_decimal_range_field(field) %} + <p class="range-field"> + {{ field(**kwargs) }} + </p> +{% endmacro %} + {% macro render_file_field(field) %} <div class="file-field input-field"> <div class="btn"> diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 90467b92d35c981f2ea8c0bf6d670a1ad22fd7ff..e8cad4f155369c40efa31dddd69402060dc71f30 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -9,4 +9,4 @@ GUNICORN_WORKERS="${GUNICORN_WORKERS:-1}" source venv/bin/activate flask deploy -gunicorn --bind :5000 --workers "${GUNICORN_WORKERS}" --worker-class eventlet nopaque:app +gunicorn --access-logfile - --bind :5000 --error-logfile - --workers "${GUNICORN_WORKERS}" --worker-class eventlet nopaque:app