diff --git a/calendarpush.py b/calendarpush.py
index 260aece3ccf9b3ceb39004122df8f023122278d5..331f82a5d1ca808c90417af82dde7139c9faeed2 100644
--- a/calendarpush.py
+++ b/calendarpush.py
@@ -3,6 +3,7 @@ import random
 import quopri
 
 from caldav import DAVClient, Principal, Calendar, Event
+from caldav.lib.error import PropfindError
 from vobject.base import ContentLine
 
 import config
@@ -14,17 +15,33 @@ class Client:
     def __init__(self, calendar=None, url=None):
         self.url = url if url is not None else config.CALENDAR_URL
         self.client = DAVClient(self.url)
-        self.principal = self.client.principal()
+        self.principal = None
+        for _ in range(config.CALENDAR_MAX_REQUESTS):
+            try:
+                self.principal = self.client.principal()
+                break
+            except PropfindError as exc:
+                print(exc)
+        if self.principal is None:
+            raise CalendarException("Got {} PropfindErrors from the CalDAV server.".format(config.CALENDAR_MAX_REQUESTS))
         if calendar is not None:
             self.calendar = self.get_calendar(calendar)
         else:
             self.calendar = calendar
 
     def get_calendars(self):
-        return [
-            calendar.name
-            for calendar in self.principal.calendars()
-        ]
+        if not config.CALENDAR_ACTIVE:
+            return
+        for _ in range(config.CALENDAR_MAX_REQUESTS):
+            try:
+                return [
+                    calendar.name
+                    for calendar in self.principal.calendars()
+                ]
+            except PropfindError as exc:
+                print(exc)
+        raise CalendarException("Got {} PropfindErrors from the CalDAV server.".format(config.CALENDAR_MAX_REQUESTS))
+
 
     def get_calendar(self, calendar_name):
         candidates = self.principal.calendars()
@@ -34,6 +51,8 @@ class Client:
         raise CalendarException("No calendar named {}.".format(calendar_name))
 
     def set_event_at(self, begin, name, description):
+        if not config.CALENDAR_ACTIVE:
+            return
         candidates = [
             Event.from_raw_event(raw_event)
             for raw_event in self.calendar.date_search(begin)
@@ -126,10 +145,3 @@ def get_timezone_offset():
 def encode_quopri(text):
     return quopri.encodestring(text.encode("utf-8")).replace(b"\n", b"=0D=0A").decode("utf-8")
 
-def main():
-    client = Client("Protokolltest")
-    client.set_event_at(datetime(2017, 2, 27, 19, 0), "FSS", "Tagesordnung\nTOP 1")
-
-if __name__ == "__main__":
-    if config.CALENDAR_ACTIVE:
-        main()
diff --git a/config.py.example b/config.py.example
index 0f0399a6032489a2236ccaa4893868925c889490..fd0c4e334ab8cc816288f54c340a4e8a19ce5144 100644
--- a/config.py.example
+++ b/config.py.example
@@ -85,7 +85,7 @@ ADMIN_MAIL = "admin@example.com"
 PARSER_LAZY = False
 
 # minimum similarity (0-100) todos need to have to be considered equal
-FUZZY_MIN_SCORE = 50
+FUZZY_MIN_SCORE = 90
 
 # choose something nice from fc-list
 # Nimbus Sans looks very much like Computer Modern
@@ -122,3 +122,9 @@ DOCUMENTS_PATH = "documents"
 # keywords indicating private protocol parts
 PRIVATE_KEYWORDS = ["private", "internal", "privat", "intern"]
 
+LATEX_BULLETPOINTS = [
+    r"\textbullet",
+    r"\normalfont \bfseries \textendash",
+    r"\textasteriskcentered",
+    r"\textperiodcentered"
+]
diff --git a/legacy.py b/legacy.py
index 73748361b78d8cd1ecaa7193c7eec70475d93412..7cf650ed4111543d0580ed5b73d45a7641f7cd4e 100644
--- a/legacy.py
+++ b/legacy.py
@@ -1,10 +1,17 @@
-from models.database import Todo, OldTodo
+from datetime import datetime
 from fuzzywuzzy import fuzz, process
+import tempfile
 
+from models.database import Todo, OldTodo, Protocol, ProtocolType
 from shared import db
 
 import config
 
+def log_fuzzy(text):
+    with tempfile.NamedTemporaryFile(delete=False, mode="w") as tmpfile:
+        tmpfile.write(text + "\n\n")
+    print(text)
+
 def lookup_todo_id(old_candidates, new_who, new_description):
     # Check for perfect matches
     for candidate in old_candidates:
@@ -22,25 +29,62 @@ def lookup_todo_id(old_candidates, new_who, new_description):
     best_match, best_match_score = process.extractOne(
         new_description, content_to_number.keys())
     if best_match_score >= config.FUZZY_MIN_SCORE:
-        print("Used fuzzy matching on '{}', got '{}' with score {}.".format(
+        log_fuzzy("Used fuzzy matching on '{}', got '{}' with score {}.".format(
             new_description, best_match, best_match_score))
         return content_to_number[best_match]
     else:
-        print("Best match for '{}' is '{}' with score {}, rejecting.".format(
+        log_fuzzy("Best match for '{}' is '{}' with score {}, rejecting.".format(
             new_description, best_match, best_match_score))
         return None
 
+INSERT_PROTOCOLTYPE = "INSERT INTO `protocolManager_protocoltype`"
+INSERT_PROTOCOL = "INSERT INTO `protocolManager_protocol`"
+INSERT_TODO = "INSERT INTO `protocolManager_todo`"
+
+def import_old_protocols(sql_text):
+    protocoltype_lines = []
+    protocol_lines = []
+    for line in sql_text.splitlines():
+        if line.startswith(INSERT_PROTOCOLTYPE):
+            protocoltype_lines.append(line)
+        elif line.startswith(INSERT_PROTOCOL):
+            protocol_lines.append(line)
+    if (len(protocoltype_lines) == 0
+    or len(protocol_lines) == 0):
+        raise ValueError("Necessary lines not found.")
+    type_id_to_handle = {}
+    for type_line in protocoltype_lines:
+        for id, handle, name, mail, protocol_id in _split_insert_line(type_line):
+            type_id_to_handle[int(id)] = handle.lower()
+    protocols = []
+    for protocol_line in protocol_lines:
+        for (protocol_id, old_type_id, date, source, textsummary, htmlsummary,
+            deleted, sent, document_id) in _split_insert_line(protocol_line):
+            date = datetime.strptime(date, "%Y-%m-%d")
+            handle = type_id_to_handle[int(old_type_id)]
+            type = ProtocolType.query.filter(ProtocolType.short_name.ilike(handle)).first()
+            if type is None:
+                raise KeyError("No protocoltype for handle '{}'.".format(handle))
+            protocol = Protocol(type.id, date, source=source)
+            db.session.add(protocol)
+            db.session.commit()
+            import tasks
+            protocols.append(protocol)
+    for protocol in sorted(protocols, key=lambda p: p.date):
+        print(protocol.date)
+        tasks.parse_protocol(protocol)
+
 
 def import_old_todos(sql_text):
     protocoltype_lines = []
     protocol_lines = []
     todo_lines = []
     for line in sql_text.splitlines():
-        if line.startswith("INSERT INTO `protocolManager_protocoltype`"):
+        if line.startswith(INSERT_PROTOCOLTYPE):
             protocoltype_lines.append(line)
-        elif line.startswith("INSERT INTO `protocolManager_protocol`"):
+        elif line.startswith(INSERT_PROTOCOL):
             protocol_lines.append(line)
-        elif line.startswith("INSERT INTO `protocolManager_todo`"):
+        elif line.startswith(INSERT_TODO):
             todo_lines.append(line)
     if (len(protocoltype_lines) == 0
     or len(protocol_lines) == 0
@@ -125,7 +169,16 @@ def _split_base_level(text, begin="(", end=")", separator=",", string_terminator
         escaped = False
         for char in part:
             if escaped:
-                current_field += char
+                if char == "n":
+                    current_field += "\n"
+                elif char == "r":
+                    current_field += "\r"
+                elif char == "t":
+                    current_field += "\t"
+                else:
+                    if char not in "\"'\\":
+                        print("escaped char: '{}'".format(char))
+                    current_field += char
                 escaped = False
             elif in_string:
                 if char == escape:
diff --git a/models/database.py b/models/database.py
index baa6790b53491d4fe61de3121b5d4c731ab6ddd4..1fe696c3a31eee8ef68c0ea4c44753433108085c 100644
--- a/models/database.py
+++ b/models/database.py
@@ -43,7 +43,7 @@ class ProtocolType(db.Model):
 
     def __init__(self, name, short_name, organization, usual_time,
             is_public, private_group, public_group, private_mail, public_mail,
-            use_wiki, wiki_category, wiki_only_public, printer):
+            use_wiki, wiki_category, wiki_only_public, printer, calendar):
         self.name = name
         self.short_name = short_name
         self.organization = organization
@@ -416,6 +416,44 @@ class TodoState(Enum):
             raise ValueError("Unknown state: '{}'".format(name))
         return NAME_TO_STATE[name]
 
+    @staticmethod
+    def from_name_lazy(name):
+        name = name.strip().lower()
+        STATE_TO_NAME, NAME_TO_STATE = make_states(TodoState)
+        for key in NAME_TO_STATE:
+            if name.startswith(key):
+                return NAME_TO_STATE[key]
+        raise ValueError("{} does not start with a state.".format(name))
+
+    @staticmethod
+    def from_name_with_date(name, protocol=None):
+        name = name.strip().lower()
+        if not " " in name:
+            raise ValueError("{} does definitely not contain a state and a date".format(name))
+        name_part, date_part = name.split(" ", 1)
+        state = TodoState.from_name(name_part)
+        date = None
+        last_exc = None
+        formats = [("%d.%m.%Y", False)]
+        if config.PARSER_LAZY:
+            formats.extend([("%d.%m.", True), ("%d.%m", True)])
+        for format, year_missing in formats:
+            try:
+                date = datetime.strptime(date_part.strip(), format).date()
+                if year_missing:
+                    year = datetime.now().year
+                    if protocol is not None:
+                        year = protocol.date.year
+                    date = datetime(year=year, month=date.month, day=date.day).date()
+                break
+            except ValueError as exc:
+                last_exc = exc
+                continue
+        if date is None:
+            raise last_exc
+        return state, date
+
+
 class Todo(db.Model):
     __tablename__ = "todos"
     id = db.Column(db.Integer, primary_key=True)
@@ -443,9 +481,9 @@ class Todo(db.Model):
     def is_done(self):
         if self.state.needs_date():
             if self.state == TodoState.after:
-                return datetime.now().date() >= self.date
-            elif self.state == TodoState.before:
                 return datetime.now().date() <= self.date
+            elif self.state == TodoState.before:
+                return datetime.now().date() >= self.date
         return self.state.is_done()
 
     def get_id(self):
diff --git a/parser.py b/parser.py
index d2c5bedd265e978b832a1ea1d205920ff0988610..c78ee74c8f2fa198525965c7f253b1c992e578b2 100644
--- a/parser.py
+++ b/parser.py
@@ -156,7 +156,7 @@ class Content(Element):
     # v2: does not require the semicolon, but the newline
     #PATTERN = r"\s*(?<content>(?:[^\[\];\r\n]+)?(?:\[[^\]\r\n]+\][^;\[\]\r\n]*)*);?"
     # v3: does not allow braces in the content
-    PATTERN = r"\s*(?<content>(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n]+\][^;\[\]\r\n]*)*);?"
+    PATTERN = r"\s*(?<content>(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)*);?"
 
 class Text:
     def __init__(self, text, linenumber, fork):
@@ -188,7 +188,10 @@ class Text:
             raise ParserException("Text is empty!", linenumber)
         return Text(content, linenumber, current)
 
-    PATTERN = r"(?<text>[^\[]+)(?:(?=\[)|$)"
+    # v1: does not allow any [, as that is part of a tag
+    # PATTERN = r"(?<text>[^\[]+)(?:(?=\[)|$)"
+    # v2: does allow one [ at the beginning, which is used if it did not match a tag
+    PATTERN = r"(?<text>\[?[^\[{}]+)(?:(?=\[)|$)"
 
 
 class Tag:
@@ -206,7 +209,7 @@ class Tag:
                 if not show_private:
                     return ""
                 return self.todo.render_latex(current_protocol=protocol)
-            return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(self.values[0]))
+            return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(";".join(self.values)))
         elif render_type == RenderType.plaintext:
             if self.name == "url":
                 return self.values[0]
@@ -214,7 +217,7 @@ class Tag:
                 if not show_private:
                     return ""
                 return self.values[0]
-            return "{}: {}".format(self.name.capitalize(), self.values[0])
+            return "{}: {}".format(self.name.capitalize(), ";".join(self.values))
         elif render_type == RenderType.wikitext:
             if self.name == "url":
                 return "[{0} {0}]".format(self.values[0])
@@ -222,7 +225,7 @@ class Tag:
                 if not show_private:
                     return ""
                 return self.todo.render_wikitext(current_protocol=protocol)
-            return "'''{}:''' {}".format(self.name.capitalize(), self.values[0])
+            return "'''{}:''' {}".format(self.name.capitalize(), ";".join(self.values))
         else:
             raise _not_implemented(self, render_type)
 
@@ -240,8 +243,12 @@ class Tag:
             raise ParserException("Tag is empty!", linenumber)
         parts = content.split(";")
         return Tag(parts[0], parts[1:], linenumber, current)
+    
+    # v1: matches [text without semicolons]
+    #PATTERN = r"\[(?<content>(?:[^;\]]*;)*(?:[^;\]]*))\]"
+    # v2: needs at least two parts separated by a semicolon
+    PATTERN = r"\[(?<content>(?:[^;\]]*;)+(?:[^;\]]*))\]"
 
-    PATTERN = r"\[(?<content>(?:[^;\]]*;)*(?:[^;\]]*))\]"
 
 class Empty(Element):
     def __init__(self, linenumber):
@@ -301,8 +308,8 @@ class Remark(Element):
     PATTERN = r"\s*\#(?<content>[^\n]+)"
 
 class Fork(Element):
-    def __init__(self, environment, name, parent, linenumber, children=None):
-        self.environment = environment if environment is None or len(environment) > 0 else None
+    def __init__(self, is_top, name, parent, linenumber, children=None):
+        self.is_top = is_top
         self.name = name.strip() if (name is not None and len(name) > 0) else None
         self.parent = parent
         self.linenumber = linenumber
@@ -311,22 +318,19 @@ class Fork(Element):
     def dump(self, level=None):
         if level is None:
             level = 0
-        result_lines = ["{}fork: {}".format(INDENT_LETTER * level, self.name)]
+        result_lines = ["{}fork: {}'{}'".format(INDENT_LETTER * level, "TOP " if self.is_top else "", self.name)]
         for child in self.children:
             result_lines.append(child.dump(level + 1))
         return "\n".join(result_lines)
 
     def test_private(self, name):
+        if name is None:
+            return False
         stripped_name = name.replace(":", "").strip()
         return stripped_name in config.PRIVATE_KEYWORDS
 
     def render(self, render_type, show_private, level, protocol=None):
-        name_parts = []
-        if self.environment is not None:
-            name_parts.append(self.environment)
-        if self.name is not None:
-            name_parts.append(self.name)
-        name_line = " ".join(name_parts)
+        name_line = escape_tex(self.name if self.name is not None else "")
         if level == 0 and self.name == "Todos" and not show_private:
             return ""
         if render_type == RenderType.latex:
@@ -341,6 +345,8 @@ class Fork(Element):
                     part = r"\item {}".format(part)
                 content_parts.append(part)
             content_lines = "\n".join(content_parts)
+            if len(content_lines.strip()) == 0:
+                content_lines = "\\item Nichts\n"
             if level == 0:
                 return "\n".join([begin_line, content_lines, end_line])
             elif self.test_private(self.name):
@@ -388,7 +394,7 @@ class Fork(Element):
         return tags
 
     def is_anonymous(self):
-        return self.environment == None
+        return self.name == None
 
     def is_root(self):
         return self.parent is None
@@ -398,6 +404,17 @@ class Fork(Element):
             return self
         return self.parent.get_top()
 
+    def get_maxdepth(self):
+        child_depths = [
+            child.get_maxdepth()
+            for child in self.children
+            if isinstance(child, Fork)
+        ]
+        if len(child_depths) > 0:
+            return max(child_depths) + 1
+        else:
+            return 1
+
     @staticmethod
     def create_root():
         return Fork(None, None, None, 0)
@@ -405,18 +422,13 @@ class Fork(Element):
     @staticmethod
     def parse(match, current, linenumber=None):
         linenumber = Element.parse_inner(match, current, linenumber)
-        environment = match.group("environment")
-        name1 = match.group("name1")
-        name2 = match.group("name2")
-        name = ""
-        if name1 is not None:
-            name = name1
-        if name2 is not None:
-            if len(name) > 0:
-                name += " {}".format(name2)
-            else:
-                name = name2
-        element = Fork(environment, name, current, linenumber)
+        topname = match.group("topname")
+        name = match.group("name")
+        is_top = False
+        if topname is not None:
+            is_top = True
+            name = topname
+        element = Fork(is_top, name, current, linenumber)
         current = Element.parse_outer(element, current)
         return current, linenumber
 
@@ -434,7 +446,11 @@ class Fork(Element):
     # v1: has a problem with old protocols that do not use a lot of semicolons
     #PATTERN = r"\s*(?<name1>[^{};]+)?{(?<environment>\S+)?\h*(?<name2>[^\n]+)?"
     # v2: do not allow newlines in name1 or semicolons in name2
-    PATTERN = r"\s*(?<name1>[^{};\n]+)?{(?<environment>\S+)?\h*(?<name2>[^;\n]+)?"
+    #PATTERN = r"\s*(?<name1>[^{};\n]+)?{(?<environment>[^\s{};]+)?\h*(?<name2>[^;{}\n]+)?"
+    # v3: no environment/name2 for normal lists, only for tops
+    #PATTERN = r"\s*(?<name>[^{};\n]+)?{(?:TOP\h*(?<topname>[^;{}\n]+))?"
+    # v4: do allow one newline between name and {
+    PATTERN = r"\s*(?<name>(?:[^{};\n])+)?\n?\s*{(?:TOP\h*(?<topname>[^;{}\n]+))?"
     END_PATTERN = r"\s*};?"
 
 PATTERNS = OrderedDict([
@@ -446,8 +462,8 @@ PATTERNS = OrderedDict([
 ])
 
 TEXT_PATTERNS = OrderedDict([
-    (re.compile(Text.PATTERN), Text.parse),
-    (re.compile(Tag.PATTERN), Tag.parse)
+    (re.compile(Tag.PATTERN), Tag.parse),
+    (re.compile(Text.PATTERN), Text.parse)
 ])
 
 def parse(source):
@@ -460,11 +476,15 @@ def parse(source):
             match = pattern.match(source)
             if match is not None:
                 source = source[len(match.group()):]
-                current, linenumber = PATTERNS[pattern](match, current, linenumber)
+                try:
+                    current, linenumber = PATTERNS[pattern](match, current, linenumber)
+                except ParserException as exc:
+                    exc.tree = tree
+                    raise exc
                 found = True
                 break
         if not found:
-            raise ParserException("No matching syntax element found!", linenumber)
+            raise ParserException("No matching syntax element found!", linenumber, tree=tree)
     if current is not tree:
         raise ParserException("Source ended within fork! (started at line {})".format(current.linenumber), linenumber=current.linenumber, tree=tree)
     return tree
diff --git a/server.py b/server.py
index 854dea017684910ec59c6036752150fcf51e4581..1f8dce4263c771ff05e172a55bf4fb5af592b00b 100755
--- a/server.py
+++ b/server.py
@@ -24,7 +24,7 @@ from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_
 from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState
 from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm
 from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable
-from legacy import import_old_todos
+from legacy import import_old_todos, import_old_protocols
 
 app = Flask(__name__)
 app.config.from_object(config)
@@ -81,10 +81,15 @@ app.jinja_env.globals.update(dir=dir)
 
 @manager.command
 def import_legacy():
-    """Import the old todos from an sql dump"""
+    """Import the old todos and protocols from an sql dump"""
     filename = prompt("SQL-file")
+    #filename = "legacy.sql"
     with open(filename, "r") as sqlfile:
-        import_old_todos(sqlfile.read())
+        content = sqlfile.read()
+        import_old_todos(content)
+        import_old_protocols(content)
+    
+
 
 @app.route("/")
 def index():
@@ -93,7 +98,7 @@ def index():
         protocol for protocol in Protocol.query.all()
         if protocol.protocoltype.has_public_view_right(user)
     ]
-    def _sort_key(protocol):
+    def _protocol_sort_key(protocol):
         if protocol.date is not None:
             return protocol.date
         return datetime.now().date()
@@ -104,7 +109,7 @@ def index():
             if not protocol.done
             and (protocol.date - current_day).days < config.MAX_INDEX_DAYS
         ],
-        key=_sort_key
+        key=_protocol_sort_key
     )
     finished_protocols = sorted(
         [
@@ -113,7 +118,7 @@ def index():
             and (protocol.has_public_view_right(user)
                 or protocol.has_private_view_right(user))
         ],
-        key=_sort_key
+        key=_protocol_sort_key
     )
     protocol = finished_protocols[0] if len(finished_protocols) > 0 else None
     todos = None
@@ -123,6 +128,10 @@ def index():
             if todo.protocoltype.has_private_view_right(user)
             and not todo.is_done()
         ]
+        def _todo_sort_key(todo):
+            protocol = todo.get_first_protocol()
+            return protocol.date if protocol.date is not None else 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, todos_table=todos_table)
 
@@ -143,6 +152,7 @@ def list_types():
         if (protocoltype.public_group in user.groups
         or protocoltype.private_group in user.groups
         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)
 
@@ -160,7 +170,8 @@ def new_type():
                 form.private_group.data, form.public_group.data,
                 form.private_mail.data, form.public_mail.data,
                 form.use_wiki.data, form.wiki_category.data,
-                form.wiki_only_public.data, form.printer.data)
+                form.wiki_only_public.data, form.printer.data,
+                form.calendar.data)
             db.session.add(protocoltype)
             db.session.commit()
             flash("Der Protokolltyp {} wurde angelegt.".format(protocoltype.name), "alert-success")
@@ -204,6 +215,20 @@ def show_type(type_id):
     reminders_table = MeetingRemindersTable(protocoltype.reminders, protocoltype)
     return render_template("type-show.html", protocoltype=protocoltype, protocoltype_table=protocoltype_table, default_tops_table=default_tops_table, reminders_table=reminders_table, mail_active=config.MAIL_ACTIVE)
 
+@app.route("/type/delete/<int:type_id>")
+@login_required
+def delete_type(type_id):
+    user = current_user()
+    protocoltype = ProtocolType.query.filter_by(id=type_id).first()
+    if protocoltype is None or not protocoltype.has_modify_right(user):
+        flash("Invalider Protokolltyp oder fehlende Zugriffsrechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    name = protocoltype.name
+    db.session.delete(protocoltype) 
+    db.session.commit()
+    flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success")
+    return redirect(request.args.get("next") or url_for("list_types"))
+
 @app.route("/type/reminders/new/<int:type_id>", methods=["GET", "POST"])
 @login_required
 def new_reminder(type_id):
@@ -436,7 +461,7 @@ def list_protocols():
             search_results[protocol] = "<br />\n".join(formatted_lines)
     protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True)
     page = _get_page()
-    page_count = int(math.ceil(len(protocols)) / config.PAGE_LENGTH)
+    page_count = int(math.ceil(len(protocols) / config.PAGE_LENGTH))
     if page >= page_count:
         page = 0
     begin_index = page * config.PAGE_LENGTH
@@ -637,11 +662,9 @@ def etherpush_protocol(protocol_id):
     if not config.ETHERPAD_ACTIVE:
         flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error")
         return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id))
-    if set_etherpad_text(protocol.get_identifier(), protocol.get_template()):
-        flash("Vorlage von {} in Etherpad hochgeladen.".format(protocol.get_identifier()), "alert-success")
-    else:
-        flash("Das Etherpad wurde bereits bearbeitet.", "alert-error")
-    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
+    if not protocol.is_done():
+        tasks.set_etherpad_content(protocol)
+    return redirect(request.args.get("next") or protocol.get_etherpad_link())
 
 @app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"])
 @login_required
diff --git a/start_celery.sh b/start_celery.sh
index 397bab8183fb1252615b6c0a965862f24bcdbfe1..027f65aee548c6b7b2ff2eb2cdd0dbf21185a288 100755
--- a/start_celery.sh
+++ b/start_celery.sh
@@ -1,2 +1,2 @@
 #!/bin/bash
-celery -A server.celery worker --loglevel=debug --concurrency=4
+celery -A server.celery worker --loglevel=debug --concurrency=1
diff --git a/tasks.py b/tasks.py
index bea4291cf8b07d29b1101ad9b36116c663a28b48..b8822feca05d305c09b37052dddcfe845cd76e57 100644
--- a/tasks.py
+++ b/tasks.py
@@ -10,7 +10,7 @@ from models.database import Document, Protocol, Error, Todo, Decision, TOP, Defa
 from models.errors import DateNotMatchingException
 from server import celery, app
 from shared import db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, class_filter, KNOWN_KEYS
-from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs
+from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs, add_line_numbers, set_etherpad_text, get_etherpad_text
 from parser import parse, ParserException, Element, Content, Text, Tag, Remark, Fork, RenderType
 from wiki import WikiClient, WikiException
 from calendarpush import Client as CalendarClient, CalendarException
@@ -53,215 +53,237 @@ def parse_protocol(protocol, **kwargs):
 def parse_protocol_async(protocol_id, encoded_kwargs):
     with app.app_context():
         with app.test_request_context("/"):
-            kwargs = decode_kwargs(encoded_kwargs)
-            protocol = Protocol.query.filter_by(id=protocol_id).first()
-            if protocol is None:
-                raise Exception("No protocol given. Aborting parsing.")
-            old_errors = list(protocol.errors)
-            for error in old_errors:
-                protocol.errors.remove(error)
-            db.session.commit()
-            if protocol.source is None:
-                error = protocol.create_error("Parsing", "Protocol source is None", "")
-                db.session.add(error)
-                db.session.commit()
-                return
-            tree = None
-            try:
-                tree = parse(protocol.source)
-            except ParserException as exc:
-                context = ""
-                if exc.linenumber is not None:
-                    source_lines = protocol.source.splitlines()
-                    start_index = max(0, exc.linenumber - config.ERROR_CONTEXT_LINES)
-                    end_index = min(len(source_lines) - 1, exc.linenumber + config.ERROR_CONTEXT_LINES)
-                    context = "\n".join(source_lines[start_index:end_index])
-                if exc.tree is not None:
-                    context += "\n\nParsed syntax tree was:\n" + str(exc.tree.dump())
-                error = protocol.create_error("Parsing", str(exc), context)
-                db.session.add(error)
-                db.session.commit()
-                return
-            remarks = {element.name: element for element in tree.children if isinstance(element, Remark)}
-            required_fields = KNOWN_KEYS
-            if not config.PARSER_LAZY:
-                missing_fields = [field for field in required_fields if field not in remarks]
-                if len(missing_fields) > 0:
-                    error = protocol.create_error("Parsing", "Missing fields", ", ".join(missing_fields))
-                    db.session.add(error)
-                    db.session.commit()
-                    return
             try:
-                protocol.fill_from_remarks(remarks)
-            except ValueError:
-                error = protocol.create_error(
-                    "Parsing", "Invalid fields",
-                    "Date or time fields are not '%d.%m.%Y' respectively '%H:%M', "
-                    "but rather {}".format(
-                    ", ".join([
-                        remarks["Datum"].value.strip(),
-                        remarks["Beginn"].value.strip(),
-                        remarks["Ende"].value.strip()
-                    ])))
+                kwargs = decode_kwargs(encoded_kwargs)
+                protocol = Protocol.query.filter_by(id=protocol_id).first()
+                if protocol is None:
+                    raise Exception("No protocol given. Aborting parsing.")
+                parse_protocol_async_inner(protocol, encoded_kwargs)
+            except Exception as exc:
+                error = protocol.create_error("Parsing", "Exception", str(exc))
                 db.session.add(error)
                 db.session.commit()
-                return
-            except DateNotMatchingException as exc:
-                error = protocol.create_error("Parsing", "Date not matching",
-                    "This protocol's date should be {}, but the protocol source says {}.".format(date_filter(exc.original_date), date_filter(exc.protocol_date)))
-                db.session.add(error)
-                db.session.commit()
-                return
-            protocol.delete_orphan_todos()
+
+def parse_protocol_async_inner(protocol, encoded_kwargs):
+    old_errors = list(protocol.errors)
+    for error in old_errors:
+        protocol.errors.remove(error)
+    db.session.commit()
+    if protocol.source is None:
+        error = protocol.create_error("Parsing", "Protocol source is None", "")
+        db.session.add(error)
+        db.session.commit()
+        return
+    tree = None
+    try:
+        tree = parse(protocol.source)
+    except ParserException as exc:
+        context = ""
+        if exc.linenumber is not None:
+            source_lines = protocol.source.splitlines()
+            start_index = max(0, exc.linenumber - config.ERROR_CONTEXT_LINES)
+            end_index = min(len(source_lines) - 1, exc.linenumber + config.ERROR_CONTEXT_LINES)
+            context = "\n".join(source_lines[start_index:end_index])
+        if exc.tree is not None:
+            context += "\n\nParsed syntax tree was:\n" + str(exc.tree.dump())
+        error = protocol.create_error("Parsing", str(exc), context)
+        db.session.add(error)
+        db.session.commit()
+        return
+    remarks = {element.name: element for element in tree.children if isinstance(element, Remark)}
+    required_fields = KNOWN_KEYS
+    if not config.PARSER_LAZY:
+        missing_fields = [field for field in required_fields if field not in remarks]
+        if len(missing_fields) > 0:
+            error = protocol.create_error("Parsing", "Missing fields", ", ".join(missing_fields))
+            db.session.add(error)
             db.session.commit()
-            old_todos = list(protocol.todos)
-            for todo in old_todos:
-                protocol.todos.remove(todo)
+            return
+    try:
+        protocol.fill_from_remarks(remarks)
+    except ValueError:
+        error = protocol.create_error(
+            "Parsing", "Invalid fields",
+            "Date or time fields are not '%d.%m.%Y' respectively '%H:%M', "
+            "but rather {}".format(
+            ", ".join([
+                remarks["Datum"].value.strip(),
+                remarks["Beginn"].value.strip(),
+                remarks["Ende"].value.strip()
+            ])))
+        db.session.add(error)
+        db.session.commit()
+        return
+    except DateNotMatchingException as exc:
+        error = protocol.create_error("Parsing", "Date not matching",
+            "This protocol's date should be {}, but the protocol source says {}.".format(date_filter(exc.original_date) if exc.original_date is not None else "not present", date_filter(exc.protocol_date) if exc.protocol_date is not None else "not present"))
+        db.session.add(error)
+        db.session.commit()
+        return
+    # todos
+    protocol.delete_orphan_todos()
+    db.session.commit()
+    old_todos = list(protocol.todos)
+    for todo in old_todos:
+        protocol.todos.remove(todo)
+    db.session.commit()
+    tags = tree.get_tags()
+    todo_tags = [tag for tag in tags if tag.name == "todo"]
+    for todo_tag in todo_tags:
+        if len(todo_tag.values) < 2:
+            error = protocol.create_error("Parsing", "Invalid todo-tag",
+                "The todo tag in line {} needs at least "
+                "information on who and what, "
+                "but has less than that.".format(todo_tag.linenumber))
+            db.session.add(error)
             db.session.commit()
-            tags = tree.get_tags()
-            # todos
-            todo_tags = [tag for tag in tags if tag.name == "todo"]
-            for todo_tag in todo_tags:
-                if len(todo_tag.values) < 2:
-                    error = protocol.create_error("Parsing", "Invalid todo-tag",
-                        "The todo tag in line {} needs at least "
-                        "information on who and what, "
-                        "but has less than that.".format(todo_tag.linenumber))
+            return
+        who = todo_tag.values[0]
+        what = todo_tag.values[1]
+        field_id = None
+        field_state = None
+        field_date = None
+        for other_field in todo_tag.values[2:]:
+            other_field = other_field.strip()
+            if len(other_field) == 0:
+                continue
+            if other_field.startswith(ID_FIELD_BEGINNING):
+                try:
+                    field_id = int(other_field[len(ID_FIELD_BEGINNING):])
+                except ValueError:
+                    error = protocol.create_error("Parsing", "Non-numerical todo ID",
+                    "The todo in line {} has a nonnumerical ID, but needs "
+                    "something like \"id 1234\"".format(todo_tag.linenumber))
                     db.session.add(error)
                     db.session.commit()
                     return
-                who = todo_tag.values[0]
-                what = todo_tag.values[1]
-                field_id = None
-                field_state = None
-                field_date = None
-                for other_field in todo_tag.values[2:]:
-                    if len(other_field) == 0:
-                        continue
-                    if other_field.startswith(ID_FIELD_BEGINNING):
-                        try:
-                            field_id = int(other_field[len(ID_FIELD_BEGINNING):])
-                        except ValueError:
-                            error = protocol.create_error("Parsing", "Non-numerical todo ID",
-                            "The todo in line {} has a nonnumerical ID, but needs "
-                            "something like \"id 1234\"".format(todo_tag.linenumber))
-                            db.session.add(error)
-                            db.session.commit()
-                            return
-                    else:
+            else:
+                try:
+                    field_state = TodoState.from_name(other_field)
+                except ValueError:
+                    try:
+                        field_date = datetime.strptime(other_field, "%d.%m.%Y")
+                    except ValueError:
                         try:
-                            field_state = TodoState.from_name(other_field.strip())
+                            field_state, field_date = TodoState.from_name_with_date(other_field.strip(), protocol=protocol)
                         except ValueError:
                             try:
-                                field_date = datetime.strptime(other_field.strip(), "%d.%m.%Y")
+                                field_state = TodoState.from_name_lazy(other_field)
                             except ValueError:
                                 error = protocol.create_error("Parsing",
                                 "Invalid field",
-                                "The todo in line {} has the field '{}', but"
-                                "this does neither match a date (\"%d.%m.%Y\")"
+                                "The todo in line {} has the field '{}', but "
+                                "this does neither match a date (\"%d.%m.%Y\") "
                                 "nor a state.".format(
                                     todo_tag.linenumber, other_field))
                                 db.session.add(error)
                                 db.session.commit()
                                 return
-                if field_state is None:
-                    field_state = TodoState.open
-                if field_state.needs_date() and field_date is None:
-                    error = protocol.create_error("Parsing",
-                        "Todo missing date",
-                        "The todo in line {} has a state that needs a date, "
-                        "but the todo does not have one.".format(todo_tag.line))
-                    db.session.add(error)
-                    db.session.commit()
-                    return
-                who = who.strip()
-                what = what.strip()
-                todo = None
-                if field_id is not None:
-                    todo = Todo.query.filter_by(number=field_id).first()
-                    if todo is None and not config.PARSER_LAZY:
-                        # TODO: add non-strict mode (at least for importing old protocols)
-                        error = protocol.create_error("Parsing",
-                        "Invalid Todo ID",
-                        "The todo in line {} has the ID {}, but there is no "
-                        "Todo with that ID.".format(todo_tag.linenumber, field_id))
-                        db.session.add(error)
-                        db.session.commit()
-                        return
-                if todo is None:
-                    protocol_key = protocol.get_identifier()
-                    old_candidates = OldTodo.query.filter(
-                        OldTodo.protocol_key == protocol_key).all()
-                    if len(old_candidates) == 0:
-                        # new protocol
-                        todo = Todo(type_id=protocol.protocoltype.id,
-                            who=who, description=what, state=field_state,
-                            date=field_date)
-                        db.session.add(todo)
-                        db.session.commit()
-                        todo.number = field_id or todo.id
-                        db.session.commit()
-                    else:
-                        # old protocol
-                        number = field_id or lookup_todo_id(old_candidates, who, what)
-                        todo = Todo.query.filter_by(number=number).first()
-                        if todo is None:
-                            todo = Todo(type_id=protocol.protocoltype.id,
-                                who=who, description=what, state=field_state,
-                                date=field_date, number=number)
-                            db.session.add(todo)
-                            db.session.commit()
-                todo.protocols.append(protocol)
-                db.session.commit()
-                todo_tag.todo = todo
-            # Decisions
-            old_decisions = list(protocol.decisions)
-            for decision in old_decisions:
-                protocol.decisions.remove(decision)
+        if field_state is None:
+            field_state = TodoState.open
+        if field_state.needs_date() and field_date is None:
+            error = protocol.create_error("Parsing",
+                "Todo missing date",
+                "The todo in line {} has a state that needs a date, "
+                "but the todo does not have one.".format(todo_tag.linenumber))
+            db.session.add(error)
             db.session.commit()
-            decision_tags = [tag for tag in tags if tag.name == "beschluss"]
-            for decision_tag in decision_tags:
-                if len(decision_tag.values) == 0:
-                    error = protocol.create_error("Parsing", "Empty decision found.",
-                        "The decision in line {} is empty.".format(decision_tag.linenumber))
-                    db.session.add(error)
-                    db.session.commit()
-                    return
-                decision = Decision(protocol_id=protocol.id, content=decision_tag.values[0])
-                db.session.add(decision)
+            return
+        who = who.strip()
+        what = what.strip()
+        todo = None
+        if field_id is not None:
+            todo = Todo.query.filter_by(number=field_id).first()
+            if todo is None and not config.PARSER_LAZY:
+                # TODO: add non-strict mode (at least for importing old protocols)
+                error = protocol.create_error("Parsing",
+                "Invalid Todo ID",
+                "The todo in line {} has the ID {}, but there is no "
+                "Todo with that ID.".format(todo_tag.linenumber, field_id))
+                db.session.add(error)
                 db.session.commit()
-                decision_content = texenv.get_template("decision.tex").render(render_type=RenderType.latex, decision=decision, protocol=protocol, top=decision_tag.fork.get_top(), show_private=False)
-                compile_decision(decision_content, decision)
-            old_tops = list(protocol.tops)
-            for top in old_tops:
-                protocol.tops.remove(top)
-            tops = []
-            for index, fork in enumerate((child for child in tree.children if isinstance(child, Fork))):
-                top = TOP(protocol.id, fork.name, index, False)
-                db.session.add(top)
+                return
+        if todo is None:
+            protocol_key = protocol.get_identifier()
+            old_candidates = OldTodo.query.filter(
+                OldTodo.protocol_key == protocol_key).all()
+            if len(old_candidates) == 0:
+                # new protocol
+                todo = Todo(type_id=protocol.protocoltype.id,
+                    who=who, description=what, state=field_state,
+                    date=field_date)
+                db.session.add(todo)
+                db.session.commit()
+                todo.number = field_id or todo.id
+                db.session.commit()
+            else:
+                # old protocol
+                number = field_id or lookup_todo_id(old_candidates, who, what)
+                todo = Todo.query.filter_by(number=number).first()
+                if todo is None:
+                    todo = Todo(type_id=protocol.protocoltype.id,
+                        who=who, description=what, state=field_state,
+                        date=field_date, number=number)
+                    db.session.add(todo)
+                    db.session.commit()
+        todo.protocols.append(protocol)
+        todo.state = field_state
+        todo.date = field_date
+        db.session.commit()
+        todo_tag.todo = todo
+    # Decisions
+    old_decisions = list(protocol.decisions)
+    for decision in old_decisions:
+        protocol.decisions.remove(decision)
+    db.session.commit()
+    decision_tags = [tag for tag in tags if tag.name == "beschluss"]
+    for decision_tag in decision_tags:
+        if len(decision_tag.values) == 0:
+            error = protocol.create_error("Parsing", "Empty decision found.",
+                "The decision in line {} is empty.".format(decision_tag.linenumber))
+            db.session.add(error)
             db.session.commit()
+            return
+        decision = Decision(protocol_id=protocol.id, content=decision_tag.values[0])
+        db.session.add(decision)
+        db.session.commit()
+        decision_top = decision_tag.fork.get_top()
+        decision_content = texenv.get_template("decision.tex").render(
+            render_type=RenderType.latex, decision=decision,
+            protocol=protocol, top=decision_top, show_private=False)
+        maxdepth = decision_top.get_maxdepth()
+        compile_decision(decision_content, decision, maxdepth=maxdepth)
+    old_tops = list(protocol.tops)
+    for top in old_tops:
+        protocol.tops.remove(top)
+    tops = []
+    for index, fork in enumerate((child for child in tree.children if isinstance(child, Fork))):
+        top = TOP(protocol.id, fork.name, index, False)
+        db.session.add(top)
+    db.session.commit()
 
-            render_kwargs = {
-                "protocol": protocol,
-                "tree": tree
-            }
-            privacy_states = [False]
-            content_private = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=True, **render_kwargs)
-            content_public = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=False, **render_kwargs)
-            if content_private != content_public:
-                privacy_states.append(True)
-            protocol.content_private = content_private
-            protocol.content_public = content_public
+    render_kwargs = {
+        "protocol": protocol,
+        "tree": tree
+    }
+    maxdepth = tree.get_maxdepth()
+    privacy_states = [False]
+    content_private = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=True, **render_kwargs)
+    content_public = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=False, **render_kwargs)
+    if content_private != content_public:
+        privacy_states.append(True)
+    protocol.content_private = content_private
+    protocol.content_public = content_public
 
-            for show_private in privacy_states:
-                latex_source = texenv.get_template("protocol.tex").render(render_type=RenderType.latex, show_private=show_private, **render_kwargs)
-                compile(latex_source, protocol, show_private=show_private)
+    for show_private in privacy_states:
+        latex_source = texenv.get_template("protocol.tex").render(render_type=RenderType.latex, show_private=show_private, **render_kwargs)
+        compile(latex_source, protocol, show_private=show_private, maxdepth=maxdepth)
 
-            if protocol.protocoltype.use_wiki:
-                wiki_source = render_template("protocol.wiki", render_type=RenderType.wikitext, show_private=not protocol.protocoltype.wiki_only_public, **render_kwargs).replace("\n\n\n", "\n\n")
-                push_to_wiki(protocol, wiki_source, "Automatisch generiert vom Protokollsystem 3.0")
-            protocol.done = True
-            db.session.commit()
+    if protocol.protocoltype.use_wiki:
+        wiki_source = render_template("protocol.wiki", render_type=RenderType.wikitext, show_private=not protocol.protocoltype.wiki_only_public, **render_kwargs).replace("\n\n\n", "\n\n")
+        push_to_wiki(protocol, wiki_source, "Automatisch generiert vom Protokollsystem 3.0")
+    protocol.done = True
+    db.session.commit()
 
 def push_to_wiki(protocol, content, summary):
     push_to_wiki_async.delay(protocol.id, content, summary)
@@ -278,14 +300,14 @@ def push_to_wiki_async(protocol_id, content, summary):
         except WikiException as exc:
             error = protocol.create_error("Pushing to Wiki", "Pushing to Wiki failed.", str(exc))
 
-def compile(content, protocol, show_private):
-   compile_async.delay(content, protocol.id, show_private=show_private)
+def compile(content, protocol, show_private, maxdepth):
+   compile_async.delay(content, protocol.id, show_private=show_private, maxdepth=maxdepth)
 
-def compile_decision(content, decision):
-    compile_async.delay(content, decision.id, use_decision=True)
+def compile_decision(content, decision, maxdepth):
+    compile_async.delay(content, decision.id, use_decision=True, maxdepth=maxdepth)
 
 @celery.task
-def compile_async(content, protocol_id, show_private=False, use_decision=False):
+def compile_async(content, protocol_id, show_private=False, use_decision=False, maxdepth=5):
     with tempfile.TemporaryDirectory() as compile_dir, app.app_context():
         decision = None
         protocol = None
@@ -301,7 +323,7 @@ def compile_async(content, protocol_id, show_private=False, use_decision=False):
             log_filename = "protocol.log"
             with open(os.path.join(compile_dir, protocol_source_filename), "w") as source_file:
                 source_file.write(content)
-            protocol2_class_source = texenv.get_template("protokoll2.cls").render(fonts=config.FONTS)
+            protocol2_class_source = texenv.get_template("protokoll2.cls").render(fonts=config.FONTS, maxdepth=maxdepth, bulletpoints=config.LATEX_BULLETPOINTS)
             with open(os.path.join(compile_dir, "protokoll2.cls"), "w") as protocol2_class_file:
                 protocol2_class_file.write(protocol2_class_source)
             os.chdir(compile_dir)
@@ -335,9 +357,13 @@ def compile_async(content, protocol_id, show_private=False, use_decision=False):
             total_log_filename = os.path.join(compile_dir, log_filename)
             if os.path.isfile(total_log_filename):
                 with open(total_log_filename, "r") as log_file:
-                    log = log_file.read()
+                    log = "Log:\n\n" + add_line_numbers(log_file.read())
             else:
                 log = "Logfile not found."
+            total_source_filename = os.path.join(compile_dir, protocol_source_filename)
+            if os.path.isfile(total_source_filename):
+                with open(total_source_filename, "r") as source_file:
+                    log += "\n\nSource:\n\n" + add_line_numbers(source_file.read())
             error = protocol.create_error("Compiling", "Compiling LaTeX failed", log)
             db.session.add(error)
             db.session.commit()
@@ -461,3 +487,13 @@ def push_tops_to_calendar_async(protocol_id):
                 "Pushing TOPs to Calendar failed", str(exc))
             db.session.add(error)
             db.session.commit()
+
+def set_etherpad_content(protocol):
+    set_etherpad_content_async.delay(protocol_id)
+
+@celery.task
+def set_etherpad_content_async(protocol_id):
+    with app.app_context():
+        protocol = Protocol.query.filter_by(id=protocol_id).first()
+        set_etherpad_text(protocol.get_identifier(), protocol.get_template())
+    
diff --git a/templates/decision.tex b/templates/decision.tex
index 8cc7a93f2e205041ab99b7392e7f64c730f80aca..ad042bae1e9418f92357df5947e3f69f41c94f67 100644
--- a/templates/decision.tex
+++ b/templates/decision.tex
@@ -23,10 +23,18 @@
 \\\normalsize \VAR{protocol.protocoltype.organization|escape_tex}
 }{}
 \begin{tabular}{rp{14cm}}
+\ENV{if protocol.date is not none}
 {\bf Datum:} & \VAR{protocol.date|datify_long|escape_tex}\\
+\ENV{endif}
+\ENV{if protocol.location is not none}
 {\bf Ort:} & \VAR{protocol.location|escape_tex}\\
-{\bf Protokollant:} & \VAR{protocol.author|escape_tex}\\
+\ENV{endif}
+\ENV{if protocol.author is not none}
+{\bf Protokoll:} & \VAR{protocol.author|escape_tex}\\
+\ENV{endif}
+\ENV{if protocol.participants is not none}
 {\bf Anwesend:} & \VAR{protocol.participants|escape_tex}\\
+\ENV{endif}
 \end{tabular}
 \normalsize
 
diff --git a/templates/index.html b/templates/index.html
index 17a8c516d10137d3a607626070b51b267456b56a..e32617ae54716853e166a0f8b2f09795b156f949 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -58,7 +58,7 @@
                 <p><strong>Ort:</strong> {{protocol.location}}</p>
             {% endif %}
             {% if protocol.author is not none %}
-                <p><strong>Protokollant:</strong> {{protocol.author}}</p>
+                <p><strong>Protokoll:</strong> {{protocol.author}}</p>
             {% endif %}
             {% if protocol.participants is not none %}
                 <p><strong>Anwesende:</strong> {{protocol.participants}}</p>
diff --git a/templates/protocol-mail.txt b/templates/protocol-mail.txt
index 9c228ea83116cdaf40ef55cf7141bf83b46432d3..c1e4a51ffc13cad582c09770666d9fb0f18bb443 100644
--- a/templates/protocol-mail.txt
+++ b/templates/protocol-mail.txt
@@ -2,7 +2,7 @@ Protocol der {{protocol.name}} vom {{protocol.date|datify}}
 
 Datum: {{protocol.date|datify_long}}
 Zeit: von {{protocol.start_time|timify}} bis {{protocol.end_time|timify}}
-Protokollant: {{protocol.author}}
+Protokoll: {{protocol.author}}
 Anwesende: {{protocol.participants}}
 
 Die Tagesordnung ist:
diff --git a/templates/protocol-show.html b/templates/protocol-show.html
index 2a11a9ca2155bcc8f87bb6c097a09d0106ef4e57..318539609da68a0754cb04d85ba04b734d170285 100644
--- a/templates/protocol-show.html
+++ b/templates/protocol-show.html
@@ -62,10 +62,10 @@
                 {% if protocol.location is not none %}
                     <p><strong>Ort:</strong> {{protocol.location}}</p>
                 {% endif %}
-                {% if protocol.author is not none %}
-                    <p><strong>Protokollant:</strong> {{protocol.author}}</p>
+                {% if protocol.author is not none and has_public_view_right %}
+                    <p><strong>Protokoll:</strong> {{protocol.author}}</p>
                 {% endif %}
-                {% if protocol.participants is not none %}
+                {% if protocol.participants is not none and has_public_view_right %}
                     <p><strong>Anwesende:</strong> {{protocol.participants}}</p>
                 {% endif %}
             {% else %}
diff --git a/templates/protocol.tex b/templates/protocol.tex
index 76d742e9850c7cade73d03acf778976d0504ad11..784684dab90104d66751a43ea43e79e8d545ce81 100644
--- a/templates/protocol.tex
+++ b/templates/protocol.tex
@@ -30,7 +30,7 @@
     {\bf Ort:} & \VAR{protocol.location|escape_tex}\\
 \ENV{endif}
 \ENV{if protocol.author is not none}
-    {\bf Protokollant:} & \VAR{protocol.author|escape_tex}\\
+    {\bf Protokoll:} & \VAR{protocol.author|escape_tex}\\
 \ENV{endif}
 \ENV{if protocol.participants is not none}
     {\bf Anwesend:} & \VAR{protocol.participants|escape_tex}\\
diff --git a/templates/protokoll2.cls b/templates/protokoll2.cls
index be443f0764032592e11c6c3b585020b1c35d6814..20945166bbe46cb893d3252af46d5e9f36df07a8 100644
--- a/templates/protokoll2.cls
+++ b/templates/protokoll2.cls
@@ -22,6 +22,13 @@
 \RequirePackage{eurosym}
 \RequirePackage[babel]{csquotes}
 \RequirePackage{polyglossia}
+\RequirePackage{enumitem}
+
+\setlistdepth{\VAR{maxdepth}}
+\renewlist{itemize}{itemize}{\VAR{maxdepth}}
+\ENV{for index in range(maxdepth)}
+    \setlist[itemize,\VAR{index+1}]{label=\VAR{loop.cycle(*bulletpoints)}}
+\ENV{endfor}
 
 \setmainlanguage[babelshorthands=true]{german}
 
diff --git a/templates/top-new.html b/templates/top-new.html
index b06524649de2031a10dd6938efc803131b536c1a..516ca1aef1e2e06701313990192bed61d25c2cdf 100644
--- a/templates/top-new.html
+++ b/templates/top-new.html
@@ -4,7 +4,7 @@
 
 {% set loggedin = check_login() %}
 {% set user = current_user() %}
-{% set has_public_type_view_right = protocol.has_public_type_view_right(user) %}
+{% set has_public_type_view_right = protocol.protocoltype.has_public_view_right(user) %}
 {% set has_public_view_right = protocol.has_public_view_right(user) %}
 {% set has_private_view_right = protocol.has_private_view_right(user) %}
 {% set has_modify_right = protocol.has_modify_right(user) %}
diff --git a/todostates.py b/todostates.py
index 443ac002cabb8c54d29a32094de6424c097c6d76..cd9a7e0a34999978c7dc9019b89c5a61dbc7ff55 100644
--- a/todostates.py
+++ b/todostates.py
@@ -33,6 +33,7 @@ def make_states(TodoState):
         "after": TodoState.after,
         "not before": TodoState.after,
         "vor": TodoState.before,
+        "bis": TodoState.before,
         "nur vor": TodoState.before,
         "nicht nach": TodoState.before,
         "before": TodoState.before,
diff --git a/utils.py b/utils.py
index efe13167bdf6078c8a3adb2eef31c5a3e9b1118c..63c82259437b6e9e9117e8910d4bd278f4a88645 100644
--- a/utils.py
+++ b/utils.py
@@ -3,6 +3,7 @@ from flask import render_template, request
 import random
 import string
 import regex
+import math
 import smtplib
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
@@ -159,3 +160,14 @@ def optional_int_arg(name):
         return int(request.args.get(name))
     except (ValueError, TypeError):
         return None
+
+def add_line_numbers(text):
+    raw_lines = text.splitlines()
+    linenumber_length = math.ceil(math.log10(len(raw_lines)) + 1)
+    lines = []
+    for linenumber, line in enumerate(raw_lines):
+        lines.append("{} {}".format(
+            str(linenumber+1).rjust(linenumber_length),
+            line
+        ))
+    return "\n".join(lines)
diff --git a/views/forms.py b/views/forms.py
index 0f8bb8791a8b9c137879f2a47a4a298b02961931..2a9dcf02cbddc05f867211e5872ead0ad6c4d9ad 100644
--- a/views/forms.py
+++ b/views/forms.py
@@ -5,6 +5,7 @@ from wtforms.validators import InputRequired, Optional
 from models.database import TodoState
 from validators import CheckTodoDateByState
 from calendarpush import Client as CalendarClient
+from shared import current_user
 
 import config
 
@@ -21,10 +22,21 @@ def get_todostate_choices():
     ]
 
 def get_calendar_choices():
-    return [
-        (calendar, calendar)
-        for calendar in CalendarClient().get_calendars()
-    ]
+    calendars = CalendarClient().get_calendars()
+    choices = list(zip(calendars, calendars))
+    choices.insert(0, ("", "Kein Kalender"))
+    return choices
+
+def get_printer_choices():
+    choices = list(zip(config.PRINTING_PRINTERS, config.PRINTING_PRINTERS))
+    choices.insert(0, ("", "Nicht drucken"))
+    return choices
+
+def get_group_choices():
+    user = current_user()
+    choices = list(zip(user.groups, user.groups))
+    choices.insert(0, ("", "Keine Gruppe"))
+    return choices
 
 def coerce_todostate(key):
     if isinstance(key, str):
@@ -42,19 +54,23 @@ class ProtocolTypeForm(FlaskForm):
     organization = StringField("Organisation", validators=[InputRequired("Du musst eine zugehörige Organisation angeben.")])
     usual_time = DateTimeField("Üblicher Beginn", validators=[InputRequired("Bitte gib die Zeit an, zu der die Sitzung beginnt.")], format="%H:%M")
     is_public = BooleanField("Öffentlich sichtbar")
-    private_group = StringField("Interne Gruppe")
-    public_group = StringField("Öffentliche Gruppe")
+    private_group = SelectField("Interne Gruppe", choices=[])
+    public_group = SelectField("Öffentliche Gruppe", choices=[])
     private_mail = StringField("Interner Verteiler")
     public_mail = StringField("Öffentlicher Verteiler")
     wiki_category = StringField("Wiki-Kategorie")
     use_wiki = BooleanField("Wiki benutzen")
     wiki_only_public = BooleanField("Wiki ist öffentlich")
-    printer = SelectField("Drucker", choices=list(zip(config.PRINTING_PRINTERS, config.PRINTING_PRINTERS)))
+    printer = SelectField("Drucker", choices=[])
     calendar = SelectField("Kalender", choices=[])
 
     def __init__(self, **kwargs):
-        super().__init__(self, **kwargs)
+        super().__init__(**kwargs)
         self.calendar.choices = get_calendar_choices()
+        self.printer.choices = get_printer_choices()
+        group_choices = get_group_choices()
+        self.private_group.choices = group_choices
+        self.public_group.choices = group_choices
 
 class DefaultTopForm(FlaskForm):
     name = StringField("Name", validators=[InputRequired("Du musst einen Namen angeben.")])
@@ -103,7 +119,7 @@ class ProtocolForm(FlaskForm):
     start_time = DateTimeField("Beginn", format="%H:%M", validators=[Optional()])
     end_time = DateTimeField("Ende", format="%H:%M", validators=[Optional()])
     location = StringField("Ort")
-    author = StringField("Protokollant")
+    author = StringField("Protokoll")
     participants = StringField("Anwesende")
     done = BooleanField("Fertig")
     public = BooleanField("Veröffentlicht")
diff --git a/views/tables.py b/views/tables.py
index 89d2c0323300c85f02af1c94275a4e0ee1a87739..e47a75d23c61ef73d09e0bb29f0f04a3f9adc9e4 100644
--- a/views/tables.py
+++ b/views/tables.py
@@ -121,8 +121,9 @@ class ProtocolTypeTable(SingleValueTable):
         calendar_headers = ["Kalender"]
         if not config.CALENDAR_ACTIVE:
             calendar_headers = []
+        action_headers = ["Aktion"]
         return (general_headers + mail_headers + printing_headers
-           + wiki_headers + calendar_headers)
+           + wiki_headers + calendar_headers + action_headers)
 
     def row(self):
         general_part = [
@@ -153,7 +154,8 @@ class ProtocolTypeTable(SingleValueTable):
         calendar_part = [self.value.calendar if self.value.calendar is not None else ""]
         if not config.CALENDAR_ACTIVE:
             calendar_part = []
-        return general_part + mail_part + printing_part + wiki_part + calendar_part
+        action_part = [Table.link(url_for("delete_type", type_id=self.value.id), "Löschen", confirm="Bist du dir sicher, dass du den Protokolltype {} löschen möchtest?".format(self.value.name))]
+        return general_part + mail_part + printing_part + wiki_part + calendar_part + action_part
 
 class DefaultTOPsTable(Table):
     def __init__(self, tops, protocoltype=None):
@@ -276,9 +278,10 @@ class TodoTable(SingleValueTable):
         row = [
             self.value.get_id(),
             self.value.get_state_plain(),
-            Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier())
-                if protocol is not None
-                else Table.link(url_for("list_protocols", protocolttype=self.value.protocoltype.id), self.value.protocoltype.short_name),
+            Table.concat([
+                Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier())
+                    for protocol in self.value.protocols
+            ]),
             self.value.who,
             self.value.description
         ]