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