diff --git a/app/auth/views.py b/app/auth/views.py index d9111ff1..9470480f 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 55d9af6b..00000000 --- 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 00000000..4bd68ebf --- /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 1e47c9e3..8f4053ab 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 c218e314..fe740fef 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -1,16 +1,38 @@ -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 background(f): + ''' This decorator executes a function in a Thread ''' + @wraps(f) + def wrapped(*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 current_user.is_administrator: + return f(*args, **kwargs) + else: + disconnect() return wrapped @@ -22,13 +44,3 @@ def socketio_login_required(f): else: return f(*args, **kwargs) return wrapped - - -def socketio_admin_required(f): - @wraps(f) - def wrapped(*args, **kwargs): - if not current_user.can(Permission.ADMIN): - disconnect() - else: - return f(*args, **kwargs) - return wrapped diff --git a/app/email.py b/app/email.py index ab24c764..88effaf9 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 6808be49..00000000 --- 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 00000000..56b4462c --- /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 4afd2cb3..fe5ac9b2 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 4925be71..378800dc 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. + Delete the job and its inputs and results from the database. """ - 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 + 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 '' % 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 '' % self.title ''' diff --git a/app/templates/macros/materialize.html.j2 b/app/templates/macros/materialize.html.j2 index 0155402a..5b063dfa 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 @@ {% endmacro %} +{% macro render_decimal_range_field(field) %} +

+ {{ field(**kwargs) }} +

+{% endmacro %} + {% macro render_file_field(field) %}
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 90467b92..e8cad4f1 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