diff --git a/models/database.py b/models/database.py
index 5304fbf3c9b72d5e56b094cf42cde3a65f3b6d11..9c9bca1b695f4b9c9cb9f9776dc3ad21e667d9b0 100644
--- a/models/database.py
+++ b/models/database.py
@@ -48,6 +48,10 @@ class DatabaseModel(db.Model):
             columns.append("{}={}".format(column_name, value))
         return "{}({})".format(self.__class__.__name__, ", ".join(columns))
 
+    @classmethod
+    def first_by_id(cls, instance_id):
+        return cls.query.filter_by(id=instance_id).first()
+
 class ProtocolType(DatabaseModel):
     __tablename__ = "protocoltypes"
     __model_name__ = "protocoltype"
@@ -235,6 +239,11 @@ class Protocol(DatabaseModel):
             or self.protocoltype.has_private_view_right(user)
         )
 
+    def get_visible_content(self, user):
+        if self.has_private_view_right(user):
+            return self.content_private
+        return self.content_public
+
     def is_done(self):
         return self.done
 
diff --git a/server.py b/server.py
index 43e46d76d3765f69044536a32928b2919dea56fc..606d92ef18291665edeb10236bb2e14416208a85 100755
--- a/server.py
+++ b/server.py
@@ -2,34 +2,58 @@
 import locale
 locale.setlocale(locale.LC_TIME, "de_DE.utf8")
 
-from flask import Flask, g, current_app, request, session, flash, redirect, url_for, abort, render_template, Response, send_file as flask_send_file, Markup
+from flask import (
+    Flask, request, session, flash, redirect,
+    url_for, abort, render_template, Response, Markup)
 from werkzeug.utils import secure_filename
 from flask_script import Manager, prompt
 from flask_migrate import Migrate, MigrateCommand
-#from flask_socketio import SocketIO
 from celery import Celery
-from sqlalchemy import or_, and_
+from sqlalchemy import or_
 from apscheduler.schedulers.background import BackgroundScheduler
 from apscheduler.triggers.cron import CronTrigger
-from apscheduler.triggers.interval import IntervalTrigger
 import atexit
 import feedgen.feed
 import icalendar
-from io import StringIO, BytesIO
+from io import BytesIO
 import os
-from datetime import datetime, time, timedelta
+from datetime import datetime, timedelta
 import math
 import mimetypes
-import subprocess
-from dateutil import tz
 
 import config
-from shared import db, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, time_filter_short, user_manager, security_manager, current_user, check_login, login_required, group_required, class_filter, needs_date_test, todostate_name_filter, code_filter, indent_tab_filter
-from utils import is_past, mail_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg, fancy_join, footnote_hash
-from decorators import db_lookup, require_public_view_right, require_private_view_right, require_modify_right, require_publish_right, require_admin_right
-from models.database import ProtocolType, Protocol, DefaultTOP, TOP, LocalTOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta, DecisionCategory, Like
-from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, generate_protocol_form, TopForm, LocalTopForm, SearchForm, DecisionSearchForm, ProtocolSearchForm, TodoSearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm, MergeTodosForm, DecisionCategoryForm, DocumentEditForm
-from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable
+from shared import (
+    db, date_filter, datetime_filter, date_filter_long,
+    date_filter_short, time_filter, time_filter_short, user_manager,
+    security_manager, current_user, check_login, login_required,
+    class_filter, needs_date_test, todostate_name_filter,
+    code_filter, indent_tab_filter)
+from utils import (
+    get_first_unused_int, get_etherpad_text, split_terms, optional_int_arg,
+    fancy_join, footnote_hash, get_git_revision, get_max_page_length_exp,
+    get_internal_filename)
+from decorators import (
+    db_lookup,
+    require_private_view_right, require_modify_right, require_publish_right,
+    require_admin_right)
+from models.database import (
+    ProtocolType, Protocol, DefaultTOP, TOP, LocalTOP,
+    Document, Todo, Decision, MeetingReminder, Error, TodoMail,
+    DecisionDocument, TodoState, DefaultMeta, DecisionCategory, Like)
+from views.forms import (
+    LoginForm, ProtocolTypeForm, DefaultTopForm,
+    MeetingReminderForm, NewProtocolForm, DocumentUploadForm,
+    KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm,
+    generate_protocol_form, TopForm, LocalTopForm,
+    DecisionSearchForm, ProtocolSearchForm, TodoSearchForm,
+    NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm,
+    DefaultMetaForm, MergeTodosForm, DecisionCategoryForm,
+    DocumentEditForm)
+from views.tables import (
+    ProtocolsTable, ProtocolTypesTable,
+    ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable,
+    TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable,
+    TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable)
 from legacy import import_old_todos, import_old_protocols, import_old_todomails
 import back
 
@@ -40,16 +64,14 @@ migrate = Migrate(app, db)
 manager = Manager(app)
 manager.add_command("db", MigrateCommand)
 
+
 def make_celery(app, config):
     celery = Celery(app.import_name, broker=config.CELERY_BROKER_URL)
     celery.conf.update(app.config)
     return celery
-celery = make_celery(app, config)
 
-#def make_socketio(app, config):
-#    socketio = SocketIO(app)
-#    return socketio
-#socketio = make_socketio(app, config)
+
+celery = make_celery(app, config)
 
 app.jinja_env.trim_blocks = True
 app.jinja_env.lstrip_blocks = True
@@ -59,7 +81,6 @@ app.jinja_env.filters["timify"] = time_filter
 app.jinja_env.filters["timify_short"] = time_filter_short
 app.jinja_env.filters["datify_short"] = date_filter_short
 app.jinja_env.filters["datify_long"] = date_filter_long
-#app.jinja_env.filters["url_complete"] = url_manager.complete
 app.jinja_env.filters["class"] = class_filter
 app.jinja_env.filters["todo_get_name"] = todostate_name_filter
 app.jinja_env.filters["code"] = code_filter
@@ -73,7 +94,7 @@ additional_templates = getattr(config, "LATEX_LOCAL_TEMPLATES", None)
 if additional_templates is not None and os.path.isdir(additional_templates):
     if additional_templates not in app.jinja_loader.searchpath:
         app.jinja_loader.searchpath.append(additional_templates)
-    
+
 
 import tasks
 
@@ -84,32 +105,20 @@ app.jinja_env.globals.update(min=min)
 app.jinja_env.globals.update(max=max)
 app.jinja_env.globals.update(dir=dir)
 app.jinja_env.globals.update(now=datetime.now)
+app.jinja_env.globals["git_revision"] = get_git_revision()
 
-def get_git_revision():
-    gitlab_url = "https://git.fsmpi.rwth-aachen.de/protokollsystem/proto3"
-    commit_hash = subprocess.check_output(["git", "log", "-g", "-1", "--pretty=%H"]).decode("UTF-8").strip()
-    timestamp = int(subprocess.check_output(["git", "log", "-g", "-1", "--pretty=%at"]).strip())
-    commit_date = datetime.fromtimestamp(timestamp)
-    return {"url": gitlab_url, "hash": commit_hash, "date": commit_date}
-
-try:
-    app.jinja_env.globals["git_revision"] = get_git_revision()
-except:
-    pass
-
-# blueprints here
 
 @manager.command
 def import_legacy():
     """Import the old todos and protocols from an sql dump"""
     filename = prompt("SQL-file")
-    #filename = "legacy.sql"
     with open(filename, "rb") as sqlfile:
         content = sqlfile.read().decode("utf-8")
         import_old_todos(content)
         import_old_protocols(content)
         import_old_todomails(content)
 
+
 @manager.command
 def recompile_all():
     for protocol in sorted(Protocol.query.all(), key=lambda p: p.date):
@@ -117,8 +126,9 @@ def recompile_all():
             print(protocol.get_short_identifier())
             tasks.parse_protocol(protocol)
 
+
 @manager.command
-def merge_todos():
+def merge_duplicate_todos():
     todo_by_id = {}
     todos = Todo.query.all()
     for todo in todos:
@@ -139,34 +149,42 @@ def merge_todos():
         else:
             todo_by_id[todo_id] = todo
 
+
 @manager.command
 def runserver():
     app.run()
     make_scheduler()
 
-# cause uwsgi currently has a bug
+
 def send_file(file_like, cache_timeout, as_attachment, attachment_filename):
+    """
+    Replaces flask.send_file since that uses an uwsgi function that is buggy.
+    """
     mimetype, _ = mimetypes.guess_type(attachment_filename)
     response = Response(file_like.read(), mimetype)
     if as_attachment:
-        response.headers["Content-Disposition"] = 'attachment; filename="{}"'.format(attachment_filename)
+        response.headers["Content-Disposition"] = (
+            'attachment; filename="{}"'.format(attachment_filename))
     content_type = mimetype
     if mimetype.startswith("text/"):
         content_type = "{}; charset=utf-8".format(content_type)
     response.headers["Content-Type"] = content_type
-    response.headers["Cache-Control"] = "public, max-age={}".format(cache_timeout)
+    response.headers["Cache-Control"] = (
+        "public, max-age={}".format(cache_timeout))
     response.headers["Connection"] = "close"
     return response
 
+
 @app.route("/")
 @back.anchor
 def index():
     user = current_user()
     protocols = [
         protocol for protocol in Protocol.query.all()
-        if protocol.protocoltype.has_public_view_right(user,
-            check_networks=False)
+        if protocol.protocoltype.has_public_view_right(
+            user, check_networks=False)
     ]
+
     def _protocol_sort_key(protocol):
         if protocol.date is not None:
             return protocol.date
@@ -185,8 +203,10 @@ def index():
         [
             protocol for protocol in protocols
             if protocol.done and protocol.public
-            and (protocol.has_private_view_right(user)
-                or protocol.protocoltype.has_public_view_right(user, check_networks=False))
+            and (
+                protocol.has_private_view_right(user)
+                or protocol.protocoltype.has_public_view_right(
+                    user, check_networks=False))
         ],
         key=_protocol_sort_key,
         reverse=True
@@ -197,7 +217,8 @@ def index():
     if len(finished_protocols) > 0:
         protocol = finished_protocols[0]
         show_private = protocol.has_private_view_right(user)
-        has_public_view_right = protocol.protocoltype.has_public_view_right(user)
+        has_public_view_right = (
+            protocol.protocoltype.has_public_view_right(user))
     todos = None
     if check_login():
         todos = [
@@ -207,16 +228,23 @@ def index():
         ]
         user_todos = [
             todo for todo in todos
-            if user.username.lower() in list(map(str.strip, todo.who.lower().split(",")))
+            if user.username.lower()
+            in list(map(str.strip, todo.who.lower().split(",")))
         ]
         if len(user_todos) > 0:
             todos = user_todos
+
         def _todo_sort_key(todo):
             protocol = todo.get_first_protocol()
-            return protocol.date if protocol is not None and protocol.date is not None else datetime.now().date()
+            if protocol is not None and protocol.date is not None:
+                return protocol.date
+            return datetime.now().date()
         todos = sorted(todos, key=_todo_sort_key, reverse=True)
-    todos_table = TodosTable(todos) if todos is not None else None
-    return render_template("index.html", open_protocols=open_protocols, protocol=protocol, todos=todos, show_private=show_private, has_public_view_right=has_public_view_right)
+    return render_template(
+        "index.html", open_protocols=open_protocols,
+        protocol=protocol, todos=todos, show_private=show_private,
+        has_public_view_right=has_public_view_right)
+
 
 @app.route("/documentation")
 @back.anchor
@@ -224,22 +252,26 @@ def index():
 def documentation():
     todostates = list(TodoState)
     name_to_state = TodoState.get_name_to_state()
-    return render_template("documentation.html", todostates=todostates, name_to_state=name_to_state)
+    return render_template(
+        "documentation.html", todostates=todostates,
+        name_to_state=name_to_state)
+
 
 @app.route("/types/list")
 @back.anchor
 @login_required
 def list_types():
-    is_logged_in = check_login()
     user = current_user()
     types = [
         protocoltype for protocoltype in ProtocolType.query.all()
         if (protocoltype.has_private_view_right(user)
-        or protocoltype.has_public_view_right(user)
-        or protocoltype.is_public)]
+            or protocoltype.has_public_view_right(user)
+            or protocoltype.is_public)]
     types = sorted(types, key=lambda t: t.short_name)
     types_table = ProtocolTypesTable(types)
-    return render_template("types-list.html", types=types, types_table=types_table)
+    return render_template(
+        "types-list.html", types=types, types_table=types_table)
+
 
 @app.route("/type/new", methods=["GET", "POST"])
 @login_required
@@ -248,16 +280,19 @@ def new_type():
     if form.validate_on_submit():
         user = current_user()
         if form.private_group.data not in user.groups:
-            flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error")
+            flash("Du kannst keinen internen Protokolltypen anlegen, "
+                  "zu dem du selbst keinen Zugang hast.", "alert-error")
         else:
             protocoltype = ProtocolType()
             form.populate_obj(protocoltype)
             db.session.add(protocoltype)
             db.session.commit()
-            flash("Der Protokolltyp {} wurde angelegt.".format(protocoltype.name), "alert-success")
+            flash("Der Protokolltyp {} wurde angelegt.".format(
+                protocoltype.name), "alert-success")
         return back.redirect("list_types")
     return render_template("type-new.html", form=form)
 
+
 @app.route("/type/edit/<int:protocoltype_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType)
@@ -267,12 +302,15 @@ def edit_type(protocoltype):
     form = ProtocolTypeForm(obj=protocoltype)
     if form.validate_on_submit():
         if form.private_group.data not in user.groups:
-            flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error")
+            flash("Du kannst keinen internen Protokolltypen anlegen, "
+                  "zu dem du selbst keinen Zugang hast.", "alert-error")
         else:
             form.populate_obj(protocoltype)
             db.session.commit()
             return back.redirect("show_type", protocoltype_id=protocoltype.id)
-    return render_template("type-edit.html", form=form, protocoltype=protocoltype)
+    return render_template(
+        "type-edit.html", form=form, protocoltype=protocoltype)
+
 
 @app.route("/type/show/<int:protocoltype_id>")
 @back.anchor
@@ -281,25 +319,36 @@ def edit_type(protocoltype):
 @require_private_view_right()
 def show_type(protocoltype):
     protocoltype_table = ProtocolTypeTable(protocoltype)
-    default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype)
-    reminders_table = MeetingRemindersTable(protocoltype.reminders, protocoltype)
+    default_tops_table = DefaultTOPsTable(
+        protocoltype.default_tops, protocoltype)
+    reminders_table = MeetingRemindersTable(
+        protocoltype.reminders, protocoltype)
     metas_table = DefaultMetasTable(protocoltype.metas, protocoltype)
-    categories_table = DecisionCategoriesTable(protocoltype.decisioncategories, protocoltype)
-    return render_template("type-show.html", protocoltype=protocoltype, protocoltype_table=protocoltype_table, default_tops_table=default_tops_table, metas_table=metas_table, reminders_table=reminders_table, mail_active=config.MAIL_ACTIVE, categories_table=categories_table)
+    categories_table = DecisionCategoriesTable(
+        protocoltype.decisioncategories, protocoltype)
+    return render_template(
+        "type-show.html", protocoltype=protocoltype,
+        protocoltype_table=protocoltype_table,
+        default_tops_table=default_tops_table, metas_table=metas_table,
+        reminders_table=reminders_table, mail_active=config.MAIL_ACTIVE,
+        categories_table=categories_table)
+
 
 @app.route("/type/delete/<int:protocoltype_id>")
 @login_required
-@group_required(config.ADMIN_GROUP)
 @db_lookup(ProtocolType)
+@require_admin_right()
 @require_modify_right()
 def delete_type(protocoltype):
     name = protocoltype.name
-    db.session.delete(protocoltype) 
+    db.session.delete(protocoltype)
     db.session.commit()
     flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
     return back.redirect("list_types")
 
-@app.route("/type/reminders/new/<int:protocoltype_id>", methods=["GET", "POST"])
+
+@app.route("/type/reminders/new/<int:protocoltype_id>",
+           methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType)
 @require_modify_right()
@@ -311,9 +360,12 @@ def new_reminder(protocoltype):
         db.session.add(meetingreminder)
         db.session.commit()
         return back.redirect("show_type", protocoltype_id=protocoltype.id)
-    return render_template("reminder-new.html", form=form, protocoltype=protocoltype)
+    return render_template(
+        "reminder-new.html", form=form, protocoltype=protocoltype)
 
-@app.route("/type/reminder/edit/<int:meetingreminder_id>", methods=["GET", "POST"])
+
+@app.route("/type/reminder/edit/<int:meetingreminder_id>",
+           methods=["GET", "POST"])
 @login_required
 @db_lookup(MeetingReminder)
 @require_modify_right()
@@ -322,8 +374,11 @@ def edit_reminder(meetingreminder):
     if form.validate_on_submit():
         form.populate_obj(meetingreminder)
         db.session.commit()
-        return back.redirect("show_type", protocoltype_id=protocoltype.id)
-    return render_template("reminder-edit.html", form=form, meetingreminder=meetingreminder)
+        return back.redirect(
+            "show_type", protocoltype_id=meetingreminder.protocoltype.id)
+    return render_template(
+        "reminder-edit.html", form=form, meetingreminder=meetingreminder)
+
 
 @app.route("/type/reminder/delete/<int:meetingreminder_id>")
 @login_required
@@ -335,6 +390,7 @@ def delete_reminder(meetingreminder):
     db.session.commit()
     return back.redirect("show_type", protocoltype_id=protocoltype.id)
 
+
 @app.route("/type/tops/new/<int:protocoltype_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType)
@@ -348,15 +404,20 @@ def new_default_top(protocoltype):
         db.session.commit()
         for protocol in protocoltype.protocols:
             if not protocol.done:
-                localtop = LocalTOP(protocol_id=protocol.id,
+                localtop = LocalTOP(
+                    protocol_id=protocol.id,
                     defaulttop_id=defaulttop.id, description="")
                 db.session.add(localtop)
         db.session.commit()
-        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt.".format(defaulttop.name, protocoltype.name), "alert-success")
+        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt."
+              .format(defaulttop.name, protocoltype.name), "alert-success")
         return back.redirect()
-    return render_template("default-top-new.html", form=form, protocoltype=protocoltype)
+    return render_template(
+        "default-top-new.html", form=form, protocoltype=protocoltype)
+
 
-@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>", methods=["GET", "POST"])
+@app.route("/type/tops/edit/<int:protocoltype_id>/<int:defaulttop_id>",
+           methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType, DefaultTOP)
 @require_modify_right()
@@ -366,7 +427,10 @@ def edit_default_top(protocoltype, defaulttop):
         form.populate_obj(defaulttop)
         db.session.commit()
         return back.redirect("show_type", protocoltype_id=protocoltype.id)
-    return render_template("default-top-edit.html", form=form, protocoltype=protocoltype, defaulttop=defaulttop)
+    return render_template(
+        "default-top-edit.html", form=form,
+        protocoltype=protocoltype, defaulttop=defaulttop)
+
 
 @app.route("/type/tops/delete/<int:defaulttop_id>")
 @login_required
@@ -375,7 +439,9 @@ def edit_default_top(protocoltype, defaulttop):
 def delete_default_top(defaulttop):
     db.session.delete(defaulttop)
     db.session.commit()
-    return back.redirect("show_type", protocoltype_id=defaulttop.protocoltype.id)
+    return back.redirect(
+        "show_type", protocoltype_id=defaulttop.protocoltype.id)
+
 
 @app.route("/type/tops/move/<int:defaulttop_id>/<diff>/")
 @login_required
@@ -387,14 +453,14 @@ def move_default_top(defaulttop, diff):
         db.session.commit()
     except ValueError:
         flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
-    return back.redirect("show_type", protocoltype_id=defaulttop.protocoltype.id)
+    return back.redirect(
+        "show_type", protocoltype_id=defaulttop.protocoltype.id)
+
 
 @app.route("/protocols/list")
 @back.anchor
 def list_protocols():
-    is_logged_in = check_login()
     user = current_user()
-    protocoltype = None
     protocoltype_id = None
     try:
         protocoltype_id = int(request.args.get("protocoltype_id"))
@@ -406,11 +472,11 @@ def list_protocols():
     except (ValueError, TypeError):
         pass
     search_term = request.args.get("search")
-    protocoltypes = ProtocolType.get_public_protocoltypes(user, check_networks=False)
+    protocoltypes = ProtocolType.get_public_protocoltypes(
+        user, check_networks=False)
     search_form = ProtocolSearchForm(protocoltypes)
     if protocoltype_id is not None:
         search_form.protocoltype_id.data = protocoltype_id
-        protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
     if state_open is not None:
         search_form.state_open.data = state_open
     if search_term is not None:
@@ -427,14 +493,17 @@ def list_protocols():
             ))
     protocols = [
         protocol for protocol in protocol_query.all()
-        if protocol.protocoltype.has_public_view_right(user, check_networks=False)
+        if protocol.protocoltype.has_public_view_right(
+            user, check_networks=False)
     ]
+
     def _matches_search(content):
         content = content.lower()
         for search_term in search_terms:
             if search_term.lower() not in content:
                 return False
         return True
+
     def _matches_search_lazy(content):
         content = content.lower()
         for search_term in search_terms:
@@ -462,7 +531,7 @@ def list_protocols():
                 and _matches_search(protocol.content_public))
         ]
         for protocol in protocols:
-            content = protocol.content_private if protocol.protocoltype.has_private_view_right(user) else protocol.content_public
+            content = protocol.get_visible_content(user)
             lines = content.splitlines()
             matches = [line for line in lines if _matches_search_lazy(line)]
             formatted_lines = []
@@ -471,13 +540,18 @@ def list_protocols():
                 lower_line = line.lower()
                 last_index = 0
                 while last_index < len(line):
-                    index_candidates = list(filter(lambda t: t[0] != -1, 
-                        [(lower_line.find(term, last_index), term) for term in search_terms]))
+                    index_candidates = list(filter(
+                        lambda t: t[0] != -1,
+                        [
+                            (lower_line.find(term, last_index), term)
+                            for term in search_terms
+                        ]))
                     if len(index_candidates) == 0:
                         parts.append((line[last_index:], False))
                         break
                     else:
-                        new_index, term = min(index_candidates, key=lambda t: t[0])
+                        new_index, term = min(
+                            index_candidates, key=lambda t: t[0])
                         new_end_index = new_index + len(term)
                         parts.append((line[last_index:new_index], False))
                         parts.append((line[new_index:new_end_index], True))
@@ -487,7 +561,8 @@ def list_protocols():
                     for text, matched in parts
                 ]))
             search_results[protocol] = " …<br />\n".join(formatted_lines)
-    protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True)
+    protocols = sorted(
+        protocols, key=lambda protocol: protocol.date, reverse=True)
     page = _get_page()
     page_length = _get_page_length()
     page_count = int(math.ceil(len(protocols) / page_length))
@@ -495,10 +570,17 @@ def list_protocols():
         page = 0
     begin_index = page * page_length
     end_index = (page + 1) * page_length
-    max_page_length_exp = math.ceil(math.log10(len(protocols))) if len(protocols) > 0 else 1
+    max_page_length_exp = get_max_page_length_exp(protocols)
     protocols = protocols[begin_index:end_index]
     protocols_table = ProtocolsTable(protocols, search_results=search_results)
-    return render_template("protocols-list.html", protocols=protocols, protocols_table=protocols_table, search_form=search_form, page=page, page_count=page_count, page_diff=config.PAGE_DIFF, protocoltype_id=protocoltype_id, search_term=search_term, state_open=state_open, page_length=page_length, max_page_length_exp=max_page_length_exp)
+    return render_template(
+        "protocols-list.html", protocols=protocols,
+        protocols_table=protocols_table, search_form=search_form, page=page,
+        page_count=page_count, page_diff=config.PAGE_DIFF,
+        protocoltype_id=protocoltype_id, search_term=search_term,
+        state_open=state_open, page_length=page_length,
+        max_page_length_exp=max_page_length_exp)
+
 
 @app.route("/protocol/new", methods=["GET", "POST"])
 @login_required
@@ -509,18 +591,23 @@ def new_protocol():
     upload_form = NewProtocolSourceUploadForm(protocoltypes)
     file_upload_form = NewProtocolFileUploadForm(protocoltypes)
     if form.validate_on_submit():
-        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
+        protocoltype = ProtocolType.query.filter_by(
+            id=form.protocoltype_id.data).first()
         if protocoltype is None or not protocoltype.has_modify_right(user):
             flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
             return back.redirect()
-        protocol = Protocol.create_new_protocol(protocoltype,
-            form.date.data, form.start_time.data)
+        protocol = Protocol.create_new_protocol(
+            protocoltype, form.date.data, form.start_time.data)
         return back.redirect("show_protocol", protocol_id=protocol.id)
     type_id = request.args.get("protocoltype_id")
     if type_id is not None:
         form.protocoltype.data = type_id
         upload_form.protocoltype.data = type_id
-    return render_template("protocol-new.html", form=form, upload_form=upload_form, file_upload_form=file_upload_form, protocoltypes=protocoltypes)
+    return render_template(
+        "protocol-new.html", form=form,
+        upload_form=upload_form, file_upload_form=file_upload_form,
+        protocoltypes=protocoltypes)
+
 
 @app.route("/protocol/show/<int:protocol_id>")
 @back.anchor
@@ -528,32 +615,42 @@ def new_protocol():
 def show_protocol(protocol):
     user = current_user()
     errors_table = ErrorsTable(protocol.errors)
-    if not protocol.protocoltype.has_public_view_right(user, check_networks=False):
+    if not protocol.protocoltype.has_public_view_right(
+            user, check_networks=False):
         flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
         if check_login():
             return redirect(url_for("index"))
         return redirect(url_for("login"))
     visible_documents = [
         document for document in protocol.documents
-        if (not document.is_private and document.protocol.has_public_view_right(user))
-        or (document.is_private and document.protocol.protocoltype.has_private_view_right(user))
+        if (not document.is_private
+            and document.protocol.has_public_view_right(user))
+        or (document.is_private
+            and document.protocol.protocoltype.has_private_view_right(user))
     ]
     documents_table = DocumentsTable(visible_documents, protocol)
     document_upload_form = DocumentUploadForm()
     source_upload_form = KnownProtocolSourceUploadForm()
     time_diff = protocol.date - datetime.now().date()
     large_time_diff = not protocol.is_done() and time_diff.days > 0
-    content_html = (protocol.content_html_private
+    content_html = (
+        protocol.content_html_private
         if protocol.has_private_view_right(user)
         else protocol.content_html_public)
     if content_html is not None:
         content_html = Markup(content_html)
-    return render_template("protocol-show.html", protocol=protocol, errors_table=errors_table, documents_table=documents_table, document_upload_form=document_upload_form, source_upload_form=source_upload_form, time_diff=time_diff, large_time_diff=large_time_diff, content_html=content_html)
+    return render_template(
+        "protocol-show.html", protocol=protocol,
+        errors_table=errors_table, documents_table=documents_table,
+        document_upload_form=document_upload_form,
+        source_upload_form=source_upload_form, time_diff=time_diff,
+        large_time_diff=large_time_diff, content_html=content_html)
+
 
 @app.route("/protocol/delete/<int:protocol_id>")
 @login_required
-@group_required(config.ADMIN_GROUP)
 @db_lookup(Protocol)
+@require_admin_right()
 @require_modify_right()
 def delete_protocol(protocol):
     name = protocol.get_short_identifier()
@@ -563,6 +660,7 @@ def delete_protocol(protocol):
     flash("Protokoll {} ist gelöscht.".format(name), "alert-success")
     return back.redirect("list_protocols")
 
+
 @app.route("/protocol/etherpull/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
@@ -570,13 +668,14 @@ def delete_protocol(protocol):
 def etherpull_protocol(protocol):
     if not config.ETHERPAD_ACTIVE:
         flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
-        return back.redirect("show_protocol", protocol_id=protocol_id)
+        return back.redirect("show_protocol", protocol_id=protocol.id)
     protocol.source = get_etherpad_text(protocol.get_identifier())
     db.session.commit()
     tasks.parse_protocol(protocol)
     flash("Das Protokoll wird kompiliert.", "alert-success")
     return back.redirect("show_protocol", protocol_id=protocol.id)
 
+
 @app.route("/protocol/upload/known/<int:protocol_id>", methods=["POST"])
 @login_required
 @db_lookup(Protocol)
@@ -599,6 +698,7 @@ def upload_source_to_known_protocol(protocol):
                 flash("Das Protokoll wird kompiliert.", "alert-success")
     return back.redirect("show_protocol", protocol_id=protocol.id)
 
+
 @app.route("/protocol/upload/new/", methods=["POST"])
 @login_required
 def upload_new_protocol():
@@ -608,16 +708,20 @@ def upload_new_protocol():
     if form.validate_on_submit():
         if form.source.data is None:
             flash("Es wurde keine Datei ausgewählt.", "alert-error")
-            return redirect(request.args.get("fail") or url_for("new_protocol"))
+            return redirect(request.args.get("fail")
+                            or url_for("new_protocol"))
         file = form.source.data
         if file.filename == "":
             flash("Es wurde keine Datei ausgewählt.", "alert-error")
-            return redirect(request.args.get("fail") or url_for("new_protocol"))
+            return redirect(request.args.get("fail")
+                            or url_for("new_protocol"))
         source = file.stream.read().decode("utf-8")
-        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
+        protocoltype = ProtocolType.query.filter_by(
+            id=form.protocoltype_id.data).first()
         if protocoltype is None or not protocoltype.has_modify_right(user):
             flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
-            return redirect(request.args.get("fail") or url_for("new_protocol"))
+            return redirect(request.args.get("fail")
+                            or url_for("new_protocol"))
         protocol = Protocol(protocoltype_id=protocoltype.id, source=source)
         db.session.add(protocol)
         db.session.commit()
@@ -628,6 +732,7 @@ def upload_new_protocol():
         return back.redirect("show_protocol", protocol_id=protocol.id)
     return redirect(request.args.get("fail") or url_for("new_protocol"))
 
+
 @app.route("/protocol/upload/new/file/", methods=["POST"])
 @login_required
 def upload_new_protocol_by_file():
@@ -637,50 +742,63 @@ def upload_new_protocol_by_file():
     if form.validate_on_submit():
         if form.file.data is None:
             flash("Es wurde keine Datei ausgewählt.", "alert-error")
-            return redirect(request.args.get("fail") or url_for("new_protocol"))
+            return redirect(request.args.get("fail")
+                            or url_for("new_protocol"))
         file = form.file.data
         if file.filename == "":
             flash("Es wurde keine Datei ausgewählt.", "alert-error")
-            return redirect(request.args.get("fail") or url_for("new_protocol"))
+            return redirect(request.args.get("fail")
+                            or url_for("new_protocol"))
         filename = secure_filename(file.filename)
-        protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
+        protocoltype = ProtocolType.query.filter_by(
+            id=form.protocoltype_id.data).first()
         if protocoltype is None or not protocoltype.has_modify_right(user):
             flash("Invalider Protokolltyp oder keine Rechte.", "alert-error")
-            return redirect(request.args.get("fail") or url_for("new_protocol"))
-        protocol = Protocol(protocoltype_id=protocoltype.id, date=datetime.now().date(), done=True)
+            return redirect(request.args.get("fail")
+                            or url_for("new_protocol"))
+        protocol = Protocol(
+            protocoltype_id=protocoltype.id,
+            date=datetime.now().date(), done=True)
         db.session.add(protocol)
         db.session.commit()
         for local_top in protocol.create_localtops:
             db.session.add(local_top)
         db.session.commit()
-        document = Document(protocol_id=protocol.id, name=filename,
+        document = Document(
+            protocol_id=protocol.id, name=filename,
             filename="", is_compiled=False)
         form.populate_obj(document)
         db.session.add(document)
         db.session.commit()
-        internal_filename = "{}-{}-{}".format(protocol.id, document.id, filename)
+        internal_filename = get_internal_filename(
+            protocol.id, document.id, filename)
         document.filename = internal_filename
         file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
         db.session.commit()
         return back.redirect("show_protocol", protocol_id=protocol.id)
     return redirect(request.args.get("fail") or url_for("new_protocol"))
 
+
 @app.route("/protocol/recompile/<int:protocol_id>")
 @login_required
-@group_required(config.ADMIN_GROUP)
 @db_lookup(Protocol)
+@require_admin_right()
 @require_modify_right()
 def recompile_protocol(protocol):
     tasks.parse_protocol(protocol)
     return back.redirect("show_protocol", protocol_id=protocol.id)
 
+
 @app.route("/protocol/source/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
 @require_modify_right()
 def get_protocol_source(protocol):
     file_like = BytesIO(protocol.source.encode("utf-8"))
-    return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename="{}.txt".format(protocol.get_short_identifier()))
+    return send_file(
+        file_like, cache_timeout=1, as_attachment=True,
+        attachment_filename="{}.txt".format(protocol.get_short_identifier()))
+
 
 @app.route("/protocol/template/<int:protocol_id>")
 @login_required
@@ -688,7 +806,11 @@ def get_protocol_source(protocol):
 @require_modify_right()
 def get_protocol_template(protocol):
     file_like = BytesIO(protocol.get_template().encode("utf-8"))
-    return send_file(file_like, cache_timeout=1, as_attachment=True, attachment_filename="{}-template.txt".format(protocol.get_short_identifier()))
+    return send_file(
+        file_like, cache_timeout=1, as_attachment=True,
+        attachment_filename="{}-template.txt".format(
+            protocol.get_short_identifier()))
+
 
 @app.route("/protocol/etherpush/<int:protocol_id>")
 @login_required
@@ -697,11 +819,12 @@ def get_protocol_template(protocol):
 def etherpush_protocol(protocol):
     if not config.ETHERPAD_ACTIVE:
         flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
-        return back.redirect("show_protocol", protocol_id=protocol_id)
+        return back.redirect("show_protocol", protocol_id=protocol.id)
     if not protocol.is_done():
         tasks.set_etherpad_content(protocol)
     return redirect(protocol.get_etherpad_link())
 
+
 @app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(Protocol)
@@ -718,7 +841,10 @@ def update_protocol(protocol):
         return back.redirect("show_protocol", protocol_id=protocol.id)
     for meta in protocol.metas:
         getattr(edit_form.metas, meta.name).data = meta.value
-    return render_template("protocol-update.html", upload_form=upload_form, edit_form=edit_form, protocol=protocol)
+    return render_template(
+        "protocol-update.html", upload_form=upload_form,
+        edit_form=edit_form, protocol=protocol)
+
 
 @app.route("/protocol/publish/<int:protocol_id>")
 @login_required
@@ -729,6 +855,7 @@ def publish_protocol(protocol):
     db.session.commit()
     return back.redirect("show_protocol", protocol_id=protocol.id)
 
+
 @app.route("/prococol/send/private/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
@@ -736,11 +863,12 @@ def publish_protocol(protocol):
 def send_protocol_private(protocol):
     if not config.MAIL_ACTIVE:
         flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
-        return back.redirect("show_protocol", protocol_id=protocol_id)
+        return back.redirect("show_protocol", protocol_id=protocol.id)
     tasks.send_protocol_private(protocol)
     flash("Das Protokoll wurde versandt.", "alert-success")
     return back.redirect("show_protocol", protocol_id=protocol.id)
 
+
 @app.route("/prococol/send/public/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
@@ -748,11 +876,12 @@ def send_protocol_private(protocol):
 def send_protocol_public(protocol):
     if not config.MAIL_ACTIVE:
         flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
-        return back.redirect("show_protocol", protocol_id=protocol_id)
+        return back.redirect("show_protocol", protocol_id=protocol.id)
     tasks.send_protocol_public(protocol)
     flash("Das Protokoll wurde versandt.", "alert-success")
     return back.redirect("show_protocol", protocol_id=protocol.id)
 
+
 @app.route("/protocol/reminder/<int:protocol_id>")
 @login_required
 @db_lookup(Protocol)
@@ -760,25 +889,27 @@ def send_protocol_public(protocol):
 def send_protocol_reminder(protocol):
     if not config.MAIL_ACTIVE:
         flash("Die Mailfunktion ist nicht aktiviert.", "alert-error")
-        return back.redirect("show_protocol", protocol_id=protocol_id)
-    meetingreminders = MeetingReminder.query.filter_by(protocoltype_id=protocol.protocoltype.id).all()
+        return back.redirect("show_protocol", protocol_id=protocol.id)
+    meetingreminders = protocol.reminders
     if len(meetingreminders) == 0:
-        flash("Für diesen Protokolltyp sind keine Einladungsmails konfiguriert.", "alert-error")
-        return back.redirect("show_protocol", protocol_id=protocol_id)
+        flash("Für diesen Protokolltyp sind keine Einladungsmails "
+              "konfiguriert.", "alert-error")
+        return back.redirect("show_protocol", protocol_id=protocol.id)
     day_difference = (protocol.date - datetime.now().date()).days
     past_reminders = [
         meetingreminder for meetingreminder in meetingreminders
         if meetingreminder.days_before > day_difference
     ]
     if len(past_reminders) == 0:
-        flash("Bisher hätte keine Einladungsmail verschickt werden sollen, schicke letzte.", "alert-info")
+        flash("Bisher hätte keine Einladungsmail verschickt werden sollen, "
+              "schicke letzte.", "alert-info")
         past_reminders = meetingreminders
     past_reminders = sorted(past_reminders, key=lambda r: r.days_before)
     choosen_reminder = past_reminders[0]
     tasks.send_reminder(choosen_reminder, protocol)
     flash("Einladungsmail ist versandt.", "alert-success")
     return back.redirect("show_protocol", protocol_id=protocol.id)
-        
+
 
 @app.route("/protocol/tops/new/<int:protocol_id>", methods=["GET", "POST"])
 @login_required
@@ -799,6 +930,7 @@ def new_top(protocol):
         form.number.data = suggested_number
     return render_template("top-new.html", form=form, protocol=protocol)
 
+
 @app.route("/protocol/top/edit/<int:top_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(TOP)
@@ -812,6 +944,7 @@ def edit_top(top):
         return back.redirect("show_protocol", protocol_id=top.protocol.id)
     return render_template("top-edit.html", form=form, top=top)
 
+
 @app.route("/protocol/top/delete/<int:top_id>")
 @login_required
 @db_lookup(TOP)
@@ -825,6 +958,7 @@ def delete_top(top):
     flash("Der TOP {} wurde gelöscht.".format(name), "alert-success")
     return back.redirect("show_protocol", protocol_id=protocol.id)
 
+
 @app.route("/protocol/top/move/<int:top_id>/<diff>")
 @login_required
 @db_lookup(TOP)
@@ -838,7 +972,9 @@ def move_top(top, diff):
         flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
     return back.redirect("show_protocol", protocol_id=top.protocol.id)
 
-@app.route("/protocol/localtop/edit/<int:localtop_id>", methods=["GET", "POST"])
+
+@app.route("/protocol/localtop/edit/<int:localtop_id>",
+           methods=["GET", "POST"])
 @login_required
 @db_lookup(LocalTOP)
 @require_modify_right()
@@ -850,6 +986,7 @@ def edit_localtop(localtop):
         return back.redirect("show_protocol", protocol_id=localtop.protocol.id)
     return render_template("localtop-edit.html", form=form, localtop=localtop)
 
+
 def _get_page():
     try:
         page = request.args.get("page")
@@ -859,6 +996,7 @@ def _get_page():
     except ValueError:
         return 0
 
+
 def _get_page_length():
     try:
         page_length = request.args.get("page_length")
@@ -868,12 +1006,12 @@ def _get_page_length():
     except ValueError:
         return config.PAGE_LENGTH
 
+
 @app.route("/todos/list")
 @back.anchor
 @login_required
 def list_todos():
     user = current_user()
-    protocoltype = None
     protocoltype_id = None
     try:
         protocoltype_id = int(request.args.get("protocoltype_id"))
@@ -889,7 +1027,6 @@ def list_todos():
     search_form = TodoSearchForm(protocoltypes)
     if protocoltype_id is not None:
         search_form.protocoltype_id.data = protocoltype_id
-        protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
     if state_open is not None:
         search_form.state_open.data = state_open
     if search_term is not None:
@@ -915,6 +1052,7 @@ def list_todos():
             if search_term.lower() in todo.description.lower()
             or search_term.lower() in todo.who.lower()
         ]
+
     def _sort_key(todo):
         return (not todo.is_done(), todo.get_id())
     todos = sorted(todos, key=_sort_key, reverse=True)
@@ -925,10 +1063,17 @@ def list_todos():
         page = 0
     begin_index = page * page_length
     end_index = (page + 1) * page_length
-    max_page_length_exp = math.ceil(math.log10(len(todos))) if len(todos) > 0 else 1
+    max_page_length_exp = get_max_page_length_exp(todos)
     todos = todos[begin_index:end_index]
     todos_table = TodosTable(todos)
-    return render_template("todos-list.html", todos=todos, todos_table=todos_table, search_form=search_form, page=page, page_count=page_count, page_diff=config.PAGE_DIFF, protocoltype_id=protocoltype_id, search_term=search_term, state_open=state_open, page_length=page_length, max_page_length_exp=max_page_length_exp)
+    return render_template(
+        "todos-list.html", todos=todos,
+        todos_table=todos_table, search_form=search_form, page=page,
+        page_count=page_count, page_diff=config.PAGE_DIFF,
+        protocoltype_id=protocoltype_id, search_term=search_term,
+        state_open=state_open, page_length=page_length,
+        max_page_length_exp=max_page_length_exp)
+
 
 @app.route("/todo/new", methods=["GET", "POST"])
 @login_required
@@ -947,8 +1092,10 @@ def new_todo():
     protocoltypes = ProtocolType.get_modifiable_protocoltypes(user)
     form = NewTodoForm(protocoltypes)
     if form.validate_on_submit():
-        added_protocoltype = ProtocolType.query.filter_by(id=form.protocoltype_id.data).first()
-        if added_protocoltype is None or not added_protocoltype.has_modify_right(user):
+        added_protocoltype = ProtocolType.query.filter_by(
+            id=form.protocoltype_id.data).first()
+        if (added_protocoltype is None
+                or not added_protocoltype.has_modify_right(user)):
             flash("Invalider Protokolltyp.")
             return back.redirect()
         todo = Todo()
@@ -967,7 +1114,10 @@ def new_todo():
     else:
         if protocoltype is not None:
             form.protocoltype_id.data = protocoltype.id
-    return render_template("todo-new.html", form=form, protocol=protocol, protocoltype=protocoltype)
+    return render_template(
+        "todo-new.html", form=form, protocol=protocol,
+        protocoltype=protocoltype)
+
 
 @app.route("/todo/edit/<int:todo_id>", methods=["GET", "POST"])
 @login_required
@@ -978,9 +1128,11 @@ def edit_todo(todo):
     if form.validate_on_submit():
         form.populate_obj(todo)
         db.session.commit()
-        return back.redirect("list_todos", protocoltype_id=todo.protocoltype.id)
+        return back.redirect(
+            "list_todos", protocoltype_id=todo.protocoltype.id)
     return render_template("todo-edit.html", form=form, todo=todo)
 
+
 @app.route("/todo/show/<int:todo_id>")
 @back.anchor
 @login_required
@@ -990,6 +1142,7 @@ def show_todo(todo):
     todo_table = TodoTable(todo)
     return render_template("todo-show.html", todo=todo, todo_table=todo_table)
 
+
 @app.route("/todo/delete/<int:todo_id>")
 @login_required
 @db_lookup(Todo)
@@ -1001,14 +1154,15 @@ def delete_todo(todo):
     flash("Todo gelöscht.", "alert-success")
     return back.redirect("list_todos", protocoltype_id=type_id)
 
+
 @app.route("/todo/merge", methods=["GET", "POST"])
 @login_required
-@group_required(config.ADMIN_GROUP)
+@require_admin_right()
 def merge_todos():
     form = MergeTodosForm(request.args.get("todo_id"))
     if form.validate_on_submit():
         todo1 = Todo.query.filter_by(id=form.todo1.data).first()
-        todo2 = Todo.query.filter_by(id=todo.todo2.data).first()
+        todo2 = Todo.query.filter_by(id=form.todo2.data).first()
         if todo1 is None or todo2 is None:
             flash("Missing todos.", "alert-error")
         else:
@@ -1024,14 +1178,12 @@ def merge_todos():
             return back.redirect("list_todos")
     return render_template("todos-merge.html", form=form)
 
+
 @app.route("/decisions/list")
 @back.anchor
 def list_decisions():
-    is_logged_In = check_login()
     user = current_user()
-    protocoltype = None
     protocoltype_id = None
-    decisioncategory = None
     decisioncategory_id = None
     try:
         protocoltype_id = int(request.args.get("protocoltype_id"))
@@ -1042,7 +1194,8 @@ def list_decisions():
     except (ValueError, TypeError):
         pass
     search_term = request.args.get("search")
-    protocoltypes = ProtocolType.get_public_protocoltypes(user, check_networks=False)
+    protocoltypes = ProtocolType.get_public_protocoltypes(
+        user, check_networks=False)
     decisioncategories = [
         category
         for protocoltype in protocoltypes
@@ -1051,10 +1204,8 @@ def list_decisions():
     search_form = DecisionSearchForm(protocoltypes, decisioncategories)
     if protocoltype_id is not None:
         search_form.protocoltype_id.data = protocoltype_id
-        protocoltype = ProtocolType.query.filter_by(id=protocoltype_id).first()
     if decisioncategory_id is not None:
         search_form.decisioncategory_id.data = decisioncategory_id
-        decisioncategory = DecisionCategory.query.filter_by(id=decisioncategory_id).first()
     if search_term is not None:
         search_form.search.data = search_term
     decisions = [
@@ -1063,7 +1214,7 @@ def list_decisions():
     ]
     if protocoltype_id is not None and protocoltype_id != -1:
         decisions = [
-            decision for decision in decisions 
+            decision for decision in decisions
             if decision.protocol.protocoltype.id == protocoltype_id
         ]
     if decisioncategory_id is not None and decisioncategory_id != -1:
@@ -1077,7 +1228,7 @@ def list_decisions():
             if search_term.lower() in decision.content.lower()
         ]
     decisions = sorted(decisions, key=lambda d: d.protocol.date, reverse=True)
-        
+
     page = _get_page()
     page_length = _get_page_length()
 
@@ -1086,22 +1237,33 @@ def list_decisions():
         page = 0
     begin_index = page * page_length
     end_index = (page + 1) * page_length
-    max_page_length_exp = math.ceil(math.log10(len(decisions))) if len(decisions) > 0 else 1
+    max_page_length_exp = get_max_page_length_exp(decisions)
     decisions = decisions[begin_index:end_index]
     decisions_table = DecisionsTable(decisions)
-    return render_template("decisions-list.html", decisions=decisions, decisions_table=decisions_table, search_form=search_form, page=page, page_count=page_count, page_diff=config.PAGE_DIFF, protocoltype_id=protocoltype_id, search_term=search_term, decisioncategory_id=decisioncategory_id, page_length=page_length, max_page_length_exp=max_page_length_exp)
+    return render_template(
+        "decisions-list.html", decisions=decisions,
+        decisions_table=decisions_table, search_form=search_form, page=page,
+        page_count=page_count, page_diff=config.PAGE_DIFF,
+        protocoltype_id=protocoltype_id, search_term=search_term,
+        decisioncategory_id=decisioncategory_id, page_length=page_length,
+        max_page_length_exp=max_page_length_exp)
+
 
 @app.route("/document/download/<int:document_id>")
 @db_lookup(Document)
 def download_document(document):
     user = current_user()
     if ((document.is_private
-            and not document.protocol.protocoltype.has_private_view_right(user))
+            and not document.protocol.protocoltype
+            .has_private_view_right(user))
         or (not document.is_private
             and not document.protocol.has_public_view_right(user))):
         flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
         return back.redirect()
-    return send_file(document.as_file_like(), cache_timeout=1, as_attachment=True, attachment_filename=document.name)
+    return send_file(
+        document.as_file_like(), cache_timeout=1,
+        as_attachment=True, attachment_filename=document.name)
+
 
 @app.route("/document/upload/<int:protocol_id>", methods=["POST"])
 @login_required
@@ -1116,15 +1278,16 @@ def upload_document(protocol):
     if file.filename == "":
         flash("Es wurde keine Datei ausgewählt.", "alert-error")
         return back.redirect()
-    # todo: Dateitypen einschränken?
     if file:
         filename = secure_filename(file.filename)
-        document = Document(protocol_id=protocol.id, name=filename,
+        document = Document(
+            protocol_id=protocol.id, name=filename,
             filename="", is_compiled=False)
         form.populate_obj(document)
         db.session.add(document)
         db.session.commit()
-        internal_filename = "{}-{}-{}".format(protocol.id, document.id, filename)
+        internal_filename = get_internal_filename(
+            protocol, document, filename)
         document.filename = internal_filename
         file.save(os.path.join(config.DOCUMENTS_PATH, internal_filename))
         if datetime.now().date() >= protocol.date:
@@ -1132,6 +1295,7 @@ def upload_document(protocol):
         db.session.commit()
     return back.redirect("show_protocol", protocol_id=protocol.id)
 
+
 @app.route("/document/edit/<int:document_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(Document)
@@ -1144,10 +1308,11 @@ def edit_document(document):
         return back.redirect("show_protocol", protocol_id=document.protocol.id)
     return render_template("document-edit.html", document=document, form=form)
 
+
 @app.route("/document/delete/<int:document_id>")
 @login_required
-@group_required(config.ADMIN_GROUP)
 @db_lookup(Document)
+@require_admin_right()
 @require_modify_right()
 def delete_document(document):
     name = document.name
@@ -1157,6 +1322,7 @@ def delete_document(document):
     flash("Das Dokument {} wurde gelöscht.".format(name), "alert-success")
     return back.redirect("show_protocol", protocol_id=protocol.id)
 
+
 @app.route("/document/print/<int:document_id>")
 @login_required
 @db_lookup(Document)
@@ -1166,21 +1332,29 @@ def print_document(document):
         flash("Die Druckfunktion ist nicht aktiviert.", "alert-error")
         return back.redirect("show_protocol", protocol_id=document.protocol.id)
     tasks.print_file(document.get_filename(), document.protocol)
-    flash("Das Dokument {} wird gedruckt.".format(document.name), "alert-success")
+    flash("Das Dokument {} wird gedruckt.".format(document.name),
+          "alert-success")
     return back.redirect("show_protocol", protocol_id=document.protocol.id)
 
+
 @app.route("/decision/print/<int:decisiondocument_id>")
 @login_required
 @db_lookup(DecisionDocument)
 @require_modify_right()
 def print_decision(decisiondocument):
-    user = current_user()
     if not config.PRINTING_ACTIVE:
         flash("Die Druckfunktion ist nicht aktiviert.", "alert-error")
-        return back.redirect("show_protocol", protocol_id=decisiondocument.decision.protocol.id)
-    tasks.print_file(decisiondocument.get_filename(), decisiondocument.decision.protocol)
-    flash("Das Dokument {} wird gedruckt.".format(decisiondocument.name), "alert-success")
-    return back.redirect("show_protocol", protocol_id=decisiondocument.decision.protocol.id)
+        return back.redirect(
+            "show_protocol",
+            protocol_id=decisiondocument.decision.protocol.id)
+    tasks.print_file(
+        decisiondocument.get_filename(),
+        decisiondocument.decision.protocol)
+    flash("Das Dokument {} wird gedruckt.".format(decisiondocument.name),
+          "alert-success")
+    return back.redirect(
+        "show_protocol", protocol_id=decisiondocument.decision.protocol.id)
+
 
 @app.route("/errors/list")
 @back.anchor
@@ -1192,7 +1366,9 @@ def list_errors():
         if error.protocol.protocoltype.has_private_view_right(user)
     ]
     errors_table = ErrorsTable(errors)
-    return render_template("errors-list.html", errros=errors, errors_table=errors_table)
+    return render_template(
+        "errors-list.html", erros=errors, errors_table=errors_table)
+
 
 @app.route("/error/show/<int:error_id>")
 @back.anchor
@@ -1201,7 +1377,9 @@ def list_errors():
 @require_modify_right()
 def show_error(error):
     error_table = ErrorTable(error)
-    return render_template("error-show.html", error=error, error_table=error_table)
+    return render_template(
+        "error-show.html", error=error, error_table=error_table)
+
 
 @app.route("/error/delete/<int:error_id>")
 @login_required
@@ -1214,13 +1392,17 @@ def delete_error(error):
     flash("Fehler {} gelöscht.".format(name), "alert-success")
     return back.redirect("list_errors")
 
+
 @app.route("/todomails/list")
 @back.anchor
 @login_required
 def list_todomails():
     todomails = sorted(TodoMail.query.all(), key=lambda tm: tm.name.lower())
     todomails_table = TodoMailsTable(todomails)
-    return render_template("todomails-list.html", todomails=todomails, todomails_table=todomails_table)
+    return render_template(
+        "todomails-list.html", todomails=todomails,
+        todomails_table=todomails_table)
+
 
 @app.route("/todomail/new", methods=["GET", "POST"])
 @login_required
@@ -1231,10 +1413,12 @@ def new_todomail():
         form.populate_obj(todomail)
         db.session.add(todomail)
         db.session.commit()
-        flash("Die Todomailzuordnung für {} wurde angelegt.".format(todomail.name), "alert-success")
+        flash("Die Todomailzuordnung für {} wurde angelegt.".format(
+            todomail.name), "alert-success")
         return back.redirect("list_todomails")
     return render_template("todomail-new.html", form=form)
 
+
 @app.route("/todomail/edit/<int:todomail_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(TodoMail)
@@ -1247,6 +1431,7 @@ def edit_todomail(todomail):
         return back.redirect("list_todomails")
     return render_template("todomail-edit.html", todomail=todomail, form=form)
 
+
 @app.route("/todomail/delete/<int:todomail_id>")
 @login_required
 @db_lookup(TodoMail)
@@ -1254,9 +1439,11 @@ def delete_todomail(todomail):
     name = todomail.name
     db.session.delete(todomail)
     db.session.commit()
-    flash("Die Todo-Mail-Zuordnung für {} wurde gelöscht.".format(name), "alert-success")
+    flash("Die Todo-Mail-Zuordnung für {} wurde gelöscht.".format(name),
+          "alert-success")
     return back.redirect("list_todomails")
-    
+
+
 @app.route("/defaultmeta/new/<int:protocoltype_id>", methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType)
@@ -1270,7 +1457,9 @@ def new_defaultmeta(protocoltype):
         db.session.commit()
         flash("Metadatenfeld hinzugefügt.", "alert-success")
         return back.redirect("show_type", protocoltype_id=protocoltype.id)
-    return render_template("defaultmeta-new.html", form=form, protocoltype=protocoltype)
+    return render_template(
+        "defaultmeta-new.html", form=form, protocoltype=protocoltype)
+
 
 @app.route("/defaultmeta/edit/<int:defaultmeta_id>", methods=["GET", "POST"])
 @login_required
@@ -1281,13 +1470,16 @@ def edit_defaultmeta(defaultmeta):
     if form.validate_on_submit():
         form.populate_obj(defaultmeta)
         db.session.commit()
-        return back.redirect("show_type", protocoltype_id=defaultmeta.protocoltype.id)
-    return render_template("defaultmeta-edit.html", form=form, defaultmeta=defaultmeta)
+        return back.redirect(
+            "show_type", protocoltype_id=defaultmeta.protocoltype.id)
+    return render_template(
+        "defaultmeta-edit.html", form=form, defaultmeta=defaultmeta)
+
 
 @app.route("/defaultmeta/delete/<int:defaultmeta_id>")
 @login_required
-@group_required(config.ADMIN_GROUP)
 @db_lookup(DefaultMeta)
+@require_admin_right()
 @require_modify_right()
 def delete_defaultmeta(defaultmeta):
     name = defaultmeta.name
@@ -1297,7 +1489,9 @@ def delete_defaultmeta(defaultmeta):
     flash("Metadatenfeld '{}' gelöscht.".format(name), "alert-success")
     return back.redirect("show_type", protocoltype_id=type_id)
 
-@app.route("/decisioncategory/new/<int:protocoltype_id>", methods=["GET", "POST"])
+
+@app.route("/decisioncategory/new/<int:protocoltype_id>",
+           methods=["GET", "POST"])
 @login_required
 @db_lookup(ProtocolType)
 @require_modify_right()
@@ -1310,9 +1504,12 @@ def new_decisioncategory(protocoltype):
         db.session.commit()
         flash("Beschlusskategorie hinzugefügt.", "alert-success")
         return back.redirect("show_type", protocoltype_id=protocoltype.id)
-    return render_template("decisioncategory-new.html", form=form, protocoltype=protocoltype)
+    return render_template(
+        "decisioncategory-new.html", form=form, protocoltype=protocoltype)
 
-@app.route("/decisioncategory/edit/<int:decisioncategory_id>", methods=["GET", "POST"])
+
+@app.route("/decisioncategory/edit/<int:decisioncategory_id>",
+           methods=["GET", "POST"])
 @login_required
 @db_lookup(DecisionCategory)
 @require_modify_right()
@@ -1321,13 +1518,17 @@ def edit_decisioncategory(decisioncategory):
     if form.validate_on_submit():
         form.populate_obj(decisioncategory)
         db.session.commit()
-        return back.redirect("show_type", protocoltype_id=decisioncategory.protocoltype.id)
-    return render_template("decisioncategory-edit.html", form=form, decisioncategory=decisioncategory)
+        return back.redirect(
+            "show_type", protocoltype_id=decisioncategory.protocoltype.id)
+    return render_template(
+        "decisioncategory-edit.html", form=form,
+        decisioncategory=decisioncategory)
+
 
 @app.route("/decisioncategory/delete/<int:decisioncategory_id>")
 @login_required
-@group_required(config.ADMIN_GROUP)
 @db_lookup(DecisionCategory)
+@require_admin_right()
 @require_modify_right()
 def delete_decisioncategory(decisioncategory):
     name = decisioncategory.name
@@ -1337,31 +1538,37 @@ def delete_decisioncategory(decisioncategory):
     flash("Beschlusskategorie {} gelöscht.".format(name), "alert-success")
     return back.redirect("show_type", protocoltype_id=type_id)
 
+
 def create_protocols_feed(protocoltype):
     if not protocoltype.has_public_anonymous_view_right():
         abort(403)
-    protocols = [protocol
-        for protocol in protocoltype.protocols
+    protocols = [
+        protocol for protocol in protocoltype.protocols
         if protocol.is_done()
     ]
     feed = feedgen.feed.FeedGenerator()
     feed.description(protocoltype.name)
-    feed.generator("Protokollsystem 3",
+    feed.generator(
+        "Protokollsystem 3",
         uri="https://git.fsmpi.rwth-aachen.de/protokollsystem/proto3")
-    feed.id(url_for("show_type", protocoltype_id=protocoltype.id, _external=True))
-    feed.link(href=url_for("list_protocols", protocoltype_id=protocoltype.id,
+    feed.id(url_for(
+        "show_type", protocoltype_id=protocoltype.id, _external=True))
+    feed.link(href=url_for(
+        "list_protocols", protocoltype_id=protocoltype.id,
         state_open=False, _external=True), rel="alternate")
     feed.title(protocoltype.short_name)
     for protocol in protocols:
         entry = feed.add_entry()
-        entry.id(url_for("show_protocol",
-            protocol_id=protocol.id, _external=True))
-        entry.link(href=url_for("show_protocol", protocol_id=protocol.id,
+        entry.id(url_for(
+            "show_protocol", protocol_id=protocol.id, _external=True))
+        entry.link(href=url_for(
+            "show_protocol", protocol_id=protocol.id,
             _external=True), rel="alternate")
         document = protocol.get_compiled_document(private=False)
         if document is not None:
-            entry.link(href=url_for("download_document",
-                document_id=document.id, _external=True), rel="enclosure",
+            entry.link(href=url_for(
+                "download_document", document_id=document.id, _external=True),
+                rel="enclosure",
                 title="Protokoll", type="application/pdf")
         entry.title(protocol.get_title())
         entry.summary(",\n".join(top.name for top in protocol.get_tops()))
@@ -1373,73 +1580,86 @@ def create_protocols_feed(protocoltype):
 def create_appointments_feed(protocoltype):
     if not protocoltype.has_public_anonymous_view_right():
         abort(403)
-    protocols = [protocol
-        for protocol in protocoltype.protocols
+    protocols = [
+        protocol for protocol in protocoltype.protocols
         if not protocol.is_done()
     ]
     feed = feedgen.feed.FeedGenerator()
     feed.description(protocoltype.name)
-    feed.generator("Protokollsystem 3",
+    feed.generator(
+        "Protokollsystem 3",
         uri="https://git.fsmpi.rwth-aachen.de/protokollsystem/proto3")
-    feed.id(url_for("show_type", protocoltype_id=protocoltype.id, _external=True))
-    feed.link(href=url_for("list_protocols", protocoltype_id=protocoltype.id,
+    feed.id(url_for(
+        "show_type", protocoltype_id=protocoltype.id, _external=True))
+    feed.link(href=url_for(
+        "list_protocols", protocoltype_id=protocoltype.id,
         state_open=True, _external=True), rel="alternate")
     feed.title("{}-Termine".format(protocoltype.short_name))
     for protocol in protocols:
         entry = feed.add_entry()
-        entry.id(url_for("show_protocol",
-            protocol_id=protocol.id, _external=True))
-        entry.link(href=url_for("show_protocol", protocol_id=protocol.id,
-            _external=True), rel="alternate")
+        entry.id(url_for(
+            "show_protocol", protocol_id=protocol.id, _external=True))
+        entry.link(href=url_for(
+            "show_protocol", protocol_id=protocol.id, _external=True),
+            rel="alternate")
         entry.title(protocol.get_title())
         entry.summary("\n".join(
-            [",\n".join([
+            [
+                ",\n".join([
                     "Beginn: {}".format(protocol.get_time())
                 ] + [
                     "{}: {}".format(meta.name, meta.value)
                     for meta in protocol.metas
                     if not meta.internal
-                ]
-            ),
-            "Tagesordnung:",
-            ",\n".join(
-                "TOP {}".format(top.name)
-                for top in protocol.get_tops()
-            )]))
+                ]),
+                "Tagesordnung:",
+                ",\n".join(
+                    "TOP {}".format(top.name)
+                    for top in protocol.get_tops()
+                )
+            ]))
     return feed
 
 
 @app.route("/feed/protocols/rss/<int:protocoltype_id>")
 @db_lookup(ProtocolType)
 def feed_protocols_rss(protocoltype):
-    return Response(create_protocols_feed(protocoltype).rss_str(),
+    return Response(
+        create_protocols_feed(protocoltype).rss_str(),
         mimetype="application/rss+xml")
 
+
 @app.route("/feed/protocols/atom/<int:protocoltype_id>")
 @db_lookup(ProtocolType)
 def feed_protocols_atom(protocoltype):
-    return Response(create_protocols_feed(protocoltype).atom_str(),
+    return Response(
+        create_protocols_feed(protocoltype).atom_str(),
         mimetype="application/atom+xml")
 
+
 @app.route("/feed/appointments/rss/<int:protocoltype_id>")
 @db_lookup(ProtocolType)
 def feed_appointments_rss(protocoltype):
-    return Response(create_appointments_feed(protocoltype).rss_str(),
+    return Response(
+        create_appointments_feed(protocoltype).rss_str(),
         mimetype="application/rss+xml")
 
+
 @app.route("/feed/appointments/atom/<int:protocoltype_id>")
 @db_lookup(ProtocolType)
 def feed_appointments_atom(protocoltype):
-    return Response(create_appointments_feed(protocoltype).atom_str(),
+    return Response(
+        create_appointments_feed(protocoltype).atom_str(),
         mimetype="application/atom+xml")
 
+
 @app.route("/feed/appointments/ical/<int:protocoltype_id>")
 @db_lookup(ProtocolType)
 def feed_appointments_ical(protocoltype):
     if not protocoltype.has_public_anonymous_view_right():
         abort(403)
-    protocols = [protocol
-        for protocol in protocoltype.protocols
+    protocols = [
+        protocol for protocol in protocoltype.protocols
         if not protocol.is_done()
     ]
     calendar = icalendar.Calendar()
@@ -1455,12 +1675,13 @@ def feed_appointments_ical(protocoltype):
         event["dtstart"] = to_datetime(start)
         event["dtend"] = to_datetime(start + timedelta(hours=3))
         event["summary"] = protocoltype.short_name
-        event["description"] = "\n".join(top.name
-            for top in protocol.get_tops())
+        event["description"] = "\n".join(
+            top.name for top in protocol.get_tops())
         calendar.add_component(event)
     content = calendar.to_ical().decode("utf-8")
     for key in config.CALENDAR_TIMEZONE_MAP:
-        content = content.replace("TZID={}:".format(key),
+        content = content.replace(
+            "TZID={}:".format(key),
             "TZID={}:".format(config.CALENDAR_TIMEZONE_MAP[key]))
     return Response(content.encode("utf-8"), mimetype="text/calendar")
 
@@ -1471,13 +1692,13 @@ def new_like():
     user = current_user()
     parent = None
     if "protocol_id" in request.args:
-        parent = Protocol.query.filter_by(id=request.args.get("protocol_id")).first()
+        parent = Protocol.first_by_id(request.args.get("protocol_id"))
     elif "todo_id" in request.args:
-        parent = Todo.query.filter_by(id=request.args.get("todo_id")).first()
+        parent = Todo.first_by_id(request.args.get("todo_id"))
     elif "decision_id" in request.args:
-        parent = Decision.query.filter_by(id=request.args.get("decision_id")).first()
+        parent = Decision.first_by_id(request.args.get("decision_id"))
     elif "top_id" in request.args:
-        parent = TOP.query.filter_by(id=request.args.get("top_id")).first()
+        parent = TOP.first_by_id(request.args.get("top_id"))
     if parent is None or not parent.has_public_view_right(user):
         flash("Missing object to like.", "alert-error")
         return back.redirect()
@@ -1499,16 +1720,20 @@ def login():
         return redirect(url_for("index"))
     form = LoginForm()
     if form.validate_on_submit():
-        user = user_manager.login(form.username.data, form.password.data, permanent=form.permanent.data)
+        user = user_manager.login(
+            form.username.data, form.password.data,
+            permanent=form.permanent.data)
         if user is not None:
             session["auth"] = security_manager.hash_user(user)
             session.permanent = form.permanent.data
-            flash("Login successful, {}!".format(user.username), "alert-success")
+            flash("Login successful, {}!".format(user.username),
+                  "alert-success")
             return back.redirect()
         else:
             flash("Wrong login data. Try again.", "alert-error")
     return render_template("login.html", form=form)
 
+
 @app.route("/logout")
 @login_required
 def logout():
@@ -1518,18 +1743,19 @@ def logout():
         flash("You are not logged in.", "alert-error")
     return redirect(url_for(".index"))
 
-def make_scheduler():
-    pass
+
 try:
-    from uwsgidecorators import timer as uwsgitimer, signal as uwsgisignal, cron as uwsgicron
-    print("using uwsgi for cron-like tasks")
+    from uwsgidecorators import cron as uwsgicron
+
     @uwsgicron(30, -1, -1, -1, -1, target="mule")
     def uwsgi_timer(signum):
         if signum == 0:
             check_and_send_reminders()
+
+    def make_scheduler():
+        pass
 except ImportError as exc:
     def make_scheduler():
-        print("uwsgi not found, falling back to apscheduler for cron-like tasks")
         scheduler = BackgroundScheduler()
         scheduler.start()
         scheduler.add_job(
@@ -1540,24 +1766,29 @@ except ImportError as exc:
             replace_existing=True)
         atexit.register(scheduler.shutdown)
 
+
 def check_and_send_reminders():
     if not config.MAIL_ACTIVE:
         return
     with app.app_context():
         current_time = datetime.now()
         current_day = current_time.date()
-        for protocol in Protocol.query.filter(Protocol.done != True).all():
+        for protocol in Protocol.query.filter(not Protocol.done).all():
             day_difference = (protocol.date - current_day).days
             usual_time = protocol.get_time()
-            protocol_time = datetime(1, 1, 1, usual_time.hour, usual_time.minute)
+            protocol_time = datetime(
+                1, 1, 1, usual_time.hour, usual_time.minute)
             hour_difference = (protocol_time - current_time).seconds // 3600
             for reminder in protocol.protocoltype.reminders:
-                if day_difference == reminder.days_before and hour_difference == 0:
+                if (day_difference == reminder.days_before
+                        and hour_difference == 0):
                     tasks.send_reminder(reminder, protocol)
             if (day_difference < 0
-                and -day_difference > config.MAX_PAST_INDEX_DAYS_BEFORE_REMINDER
-                and hour_difference == 0): # once per day
-                tasks.remind_finishing(protocol, -day_difference,
+                    and (-day_difference
+                         > config.MAX_PAST_INDEX_DAYS_BEFORE_REMINDER)
+                    and hour_difference == 0):  # once per day
+                tasks.remind_finishing(
+                    protocol, -day_difference,
                     config.MAX_PAST_INDEX_DAYS_BEFORE_REMINDER)
 
 
diff --git a/utils.py b/utils.py
index 95baf755c9a03172d0b8a9771a8f6860b0c0cadc..1d3b0fea43847abfe3fb2313e7930b7765420a87 100644
--- a/utils.py
+++ b/utils.py
@@ -14,6 +14,7 @@ from io import BytesIO
 import ipaddress
 from socket import getfqdn
 from uuid import uuid4
+import subprocess
 
 import config
 
@@ -223,3 +224,28 @@ def parse_datetime_from_string(text):
         except ValueError as exc:
             print(exc)
     raise ValueError("Date '{}' does not match any known format!".format(text))
+
+
+def get_git_revision():
+    try:
+        gitlab_url = "https://git.fsmpi.rwth-aachen.de/protokollsystem/proto3"
+        commit_hash = subprocess.check_output(
+            ["git", "log", "-g", "-1", "--pretty=%H"]).decode("UTF-8").strip()
+        timestamp = int(subprocess.check_output(
+            ["git", "log", "-g", "-1", "--pretty=%at"]).strip())
+        commit_date = datetime.fromtimestamp(timestamp)
+        return {"url": gitlab_url, "hash": commit_hash, "date": commit_date}
+    except subprocess.SubprocessError:
+        pass
+
+
+def get_max_page_length_exp(objects):
+    length = len(objects)
+    if length > 0:
+        return math.ceil(math.log10(length))
+    return 1
+
+
+def get_internal_filename(protocol, document, filename):
+    return "{}-{}-{}".format(protocol.id, document.id, filename)
+