From e918b00ef0347998e8eebea81e4bf3dcf5068e49 Mon Sep 17 00:00:00 2001
From: Robin Sonnabend <robin@fsmpi.rwth-aachen.de>
Date: Wed, 1 Mar 2017 06:15:17 +0100
Subject: [PATCH] Configurable metadata /close #16

---
 migrations/versions/4bdc217932c3_.py | 32 ++++++++++++++
 migrations/versions/b4156f71ee73_.py | 44 +++++++++++++++++++
 migrations/versions/d5be0f66b32d_.py | 62 ++++++++++++++++++++++++++
 models/database.py                   | 65 +++++++++++++++++++++-------
 server.py                            | 60 ++++++++++++++++++++++---
 shared.py                            | 11 ++---
 tasks.py                             |  9 +++-
 templates/decision.tex               | 14 ++----
 templates/defaultmeta-edit.html      |  9 ++++
 templates/defaultmeta-new.html       |  9 ++++
 templates/index.html                 | 12 ++---
 templates/protocol-show.html         | 12 ++---
 templates/protocol-template.txt      | 26 ++++++-----
 templates/protocol.tex               | 12 ++---
 templates/protocol.wiki              | 16 +++++++
 templates/type-show.html             |  1 +
 views/forms.py                       | 11 ++++-
 views/tables.py                      | 22 +++++++++-
 18 files changed, 347 insertions(+), 80 deletions(-)
 create mode 100644 migrations/versions/4bdc217932c3_.py
 create mode 100644 migrations/versions/b4156f71ee73_.py
 create mode 100644 migrations/versions/d5be0f66b32d_.py
 create mode 100644 templates/defaultmeta-edit.html
 create mode 100644 templates/defaultmeta-new.html

diff --git a/migrations/versions/4bdc217932c3_.py b/migrations/versions/4bdc217932c3_.py
new file mode 100644
index 0000000..b0dc922
--- /dev/null
+++ b/migrations/versions/4bdc217932c3_.py
@@ -0,0 +1,32 @@
+"""empty message
+
+Revision ID: 4bdc217932c3
+Revises: d5be0f66b32d
+Create Date: 2017-03-01 05:19:03.947825
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '4bdc217932c3'
+down_revision = 'd5be0f66b32d'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('protocols', 'location')
+    op.drop_column('protocols', 'author')
+    op.drop_column('protocols', 'participants')
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('protocols', sa.Column('participants', sa.VARCHAR(), autoincrement=False, nullable=True))
+    op.add_column('protocols', sa.Column('author', sa.VARCHAR(), autoincrement=False, nullable=True))
+    op.add_column('protocols', sa.Column('location', sa.VARCHAR(), autoincrement=False, nullable=True))
+    # ### end Alembic commands ###
diff --git a/migrations/versions/b4156f71ee73_.py b/migrations/versions/b4156f71ee73_.py
new file mode 100644
index 0000000..8171a91
--- /dev/null
+++ b/migrations/versions/b4156f71ee73_.py
@@ -0,0 +1,44 @@
+"""empty message
+
+Revision ID: b4156f71ee73
+Revises: 7dd3c479c048
+Create Date: 2017-03-01 04:24:24.534003
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'b4156f71ee73'
+down_revision = '7dd3c479c048'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('defaultmeta',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('protocoltype_id', sa.Integer(), nullable=True),
+    sa.Column('key', sa.String(), nullable=True),
+    sa.Column('name', sa.String(), nullable=True),
+    sa.ForeignKeyConstraint(['protocoltype_id'], ['protocoltypes.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('meta',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('protocol_id', sa.Integer(), nullable=True),
+    sa.Column('name', sa.String(), nullable=True),
+    sa.Column('value', sa.String(), nullable=True),
+    sa.ForeignKeyConstraint(['protocol_id'], ['protocols.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('meta')
+    op.drop_table('defaultmeta')
+    # ### end Alembic commands ###
diff --git a/migrations/versions/d5be0f66b32d_.py b/migrations/versions/d5be0f66b32d_.py
new file mode 100644
index 0000000..aa7913c
--- /dev/null
+++ b/migrations/versions/d5be0f66b32d_.py
@@ -0,0 +1,62 @@
+"""empty message
+
+Revision ID: d5be0f66b32d
+Revises: b4156f71ee73
+Create Date: 2017-03-01 04:32:20.328667
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'd5be0f66b32d'
+down_revision = 'b4156f71ee73'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('defaultmetas',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('protocoltype_id', sa.Integer(), nullable=True),
+    sa.Column('key', sa.String(), nullable=True),
+    sa.Column('name', sa.String(), nullable=True),
+    sa.ForeignKeyConstraint(['protocoltype_id'], ['protocoltypes.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('metas',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('protocol_id', sa.Integer(), nullable=True),
+    sa.Column('name', sa.String(), nullable=True),
+    sa.Column('value', sa.String(), nullable=True),
+    sa.ForeignKeyConstraint(['protocol_id'], ['protocols.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.drop_table('defaultmeta')
+    op.drop_table('meta')
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('meta',
+    sa.Column('id', sa.INTEGER(), nullable=False),
+    sa.Column('protocol_id', sa.INTEGER(), autoincrement=False, nullable=True),
+    sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True),
+    sa.Column('value', sa.VARCHAR(), autoincrement=False, nullable=True),
+    sa.ForeignKeyConstraint(['protocol_id'], ['protocols.id'], name='meta_protocol_id_fkey'),
+    sa.PrimaryKeyConstraint('id', name='meta_pkey')
+    )
+    op.create_table('defaultmeta',
+    sa.Column('id', sa.INTEGER(), nullable=False),
+    sa.Column('protocoltype_id', sa.INTEGER(), autoincrement=False, nullable=True),
+    sa.Column('key', sa.VARCHAR(), autoincrement=False, nullable=True),
+    sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True),
+    sa.ForeignKeyConstraint(['protocoltype_id'], ['protocoltypes.id'], name='defaultmeta_protocoltype_id_fkey'),
+    sa.PrimaryKeyConstraint('id', name='defaultmeta_pkey')
+    )
+    op.drop_table('metas')
+    op.drop_table('defaultmetas')
+    # ### end Alembic commands ###
diff --git a/models/database.py b/models/database.py
index 85ae45a..a95cc80 100644
--- a/models/database.py
+++ b/models/database.py
@@ -5,7 +5,7 @@ import math
 from io import StringIO, BytesIO
 from enum import Enum
 
-from shared import db, date_filter, date_filter_short, escape_tex, DATE_KEY, START_TIME_KEY, END_TIME_KEY, AUTHOR_KEY, PARTICIPANTS_KEY, LOCATION_KEY
+from shared import db, date_filter, date_filter_short, escape_tex, DATE_KEY, START_TIME_KEY, END_TIME_KEY
 from utils import random_string, url_manager, get_etherpad_url, split_terms
 from models.errors import DateNotMatchingException
 
@@ -41,6 +41,7 @@ class ProtocolType(db.Model):
     default_tops = relationship("DefaultTOP", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="DefaultTOP.number")
     reminders = relationship("MeetingReminder", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="MeetingReminder.days_before")
     todos = relationship("Todo", backref=backref("protocoltype"), order_by="Todo.id")
+    metas = relationship("DefaultMeta", backref=backref("protocoltype"), cascade="all, delete-orphan")
 
     def __init__(self, name, short_name, organization, usual_time,
             is_public, modify_group, private_group, public_group,
@@ -124,9 +125,6 @@ class Protocol(db.Model):
     date = db.Column(db.Date)
     start_time = db.Column(db.Time)
     end_time = db.Column(db.Time)
-    author = db.Column(db.String)
-    participants = db.Column(db.String)
-    location = db.Column(db.String)
     done = db.Column(db.Boolean)
     public = db.Column(db.Boolean)
 
@@ -134,8 +132,9 @@ class Protocol(db.Model):
     decisions = relationship("Decision", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Decision.id")
     documents = relationship("Document", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Document.is_compiled")
     errors = relationship("Error", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Error.id")
+    metas = relationship("Meta", backref=backref("protocol"), cascade="all, delete-orphan")
 
-    def __init__(self, protocoltype_id, date, source=None, content_public=None, content_private=None, start_time=None, end_time=None, author=None, participants=None, location=None, done=False, public=False):
+    def __init__(self, protocoltype_id, date, source=None, content_public=None, content_private=None, start_time=None, end_time=None, done=False, public=False):
         self.protocoltype_id = protocoltype_id
         self.date = date
         self.source = source
@@ -143,9 +142,6 @@ class Protocol(db.Model):
         self.content_public = content_public
         self.start_time = start_time
         self.end_time = end_time
-        self.author = author
-        self.participants = participants
-        self.location = location
         self.done = done
         self.public = public
 
@@ -188,12 +184,16 @@ class Protocol(db.Model):
             self.start_time = _date_or_lazy(START_TIME_KEY, get_time=True)
         if END_TIME_KEY in remarks:
             self.end_time = _date_or_lazy(END_TIME_KEY, get_time=True)
-        if AUTHOR_KEY in remarks:
-            self.author = remarks[AUTHOR_KEY].value.strip()
-        if PARTICIPANTS_KEY in remarks:
-            self.participants = remarks[PARTICIPANTS_KEY].value.strip()
-        if LOCATION_KEY in remarks:
-            self.location = remarks[LOCATION_KEY].value.strip()
+        old_metas = list(self.metas)
+        for meta in old_metas:
+            db.session.delete(meta)
+        db.session.commit()
+        for default_meta in self.protocoltype.metas:
+            if default_meta.key in remarks:
+                value = remarks[default_meta.key].value.strip()
+                meta = Meta(self.id, default_meta.name, value)
+                db.session.add(meta)
+        db.session.commit()
 
     def has_public_view_right(self, user):
         return (
@@ -307,12 +307,12 @@ class TOP(db.Model):
     planned = db.Column(db.Boolean)
     description = db.Column(db.String)
 
-    def __init__(self, protocol_id, name, number, planned, description):
+    def __init__(self, protocol_id, name, number, planned, description=None):
         self.protocol_id = protocol_id
         self.name = name
         self.number = number
         self.planned = planned
-        self.description = description
+        self.description = description if description is not None else ""
 
     def __repr__(self):
         return "<TOP(id={}, protocol_id={}, name={}, number={}, planned={})>".format(
@@ -655,3 +655,36 @@ class OldTodo(db.Model):
         return ("<OldTodo(id={}, old_id={}, who='{}', description='{}', "
             "protocol={}".format(self.id, self.old_id, self.who,
             self.description, self.protocol_key))
+
+class DefaultMeta(db.Model):
+    __tablename__ = "defaultmetas"
+    id = db.Column(db.Integer, primary_key=True)
+    protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
+    key = db.Column(db.String)
+    name = db.Column(db.String)
+
+    def __init__(self, protocoltype_id, key, name):
+        self.protocoltype_id = protocoltype_id
+        self.key = key
+        self.name = name
+
+    def __repr__(self):
+        return ("<DefaultMeta(id={}, protocoltype_id={}, key='{}', "
+            "name='{}')>".format(self.id, self.protocoltype_id, self.key))
+
+class Meta(db.Model):
+    __tablename__ = "metas"
+    id = db.Column(db.Integer, primary_key=True)
+    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
+    name = db.Column(db.String)
+    value = db.Column(db.String)
+
+    def __init__(self, protocol_id, name, value):
+        self.protocol_id = protocol_id
+        self.name = name
+        self.value = value
+
+    def __repr__(self):
+        return "<Meta(id={}, protocoltype_id={}, name={}, value={})>".format(
+            self.id, self.protocoltype_id, self.name, self.value)
+
diff --git a/server.py b/server.py
index 1f8dce4..8b07694 100755
--- a/server.py
+++ b/server.py
@@ -19,11 +19,11 @@ from datetime import datetime
 import math
 
 import config
-from shared import db, date_filter, datetime_filter, date_filter_long, time_filter, ldap_manager, security_manager, current_user, check_login, login_required, group_required, class_filter, needs_date_test, todostate_name_filter, code_filter
+from shared import db, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, ldap_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, url_manager, get_first_unused_int, set_etherpad_text, get_etherpad_text, split_terms, optional_int_arg
-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 models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta
+from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm
+from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable
 from legacy import import_old_todos, import_old_protocols
 
 app = Flask(__name__)
@@ -60,11 +60,13 @@ app.jinja_env.lstrip_blocks = True
 app.jinja_env.filters["datify"] = date_filter
 app.jinja_env.filters["datetimify"] = datetime_filter
 app.jinja_env.filters["timify"] = time_filter
+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
+app.jinja_env.filters["indent_tab"] = indent_tab_filter
 app.jinja_env.tests["auth_valid"] = security_manager.check_user
 app.jinja_env.tests["needs_date"] = needs_date_test
 
@@ -213,7 +215,8 @@ def show_type(type_id):
     protocoltype_table = ProtocolTypeTable(protocoltype)
     default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype)
     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)
+    metas_table = DefaultMetasTable(protocoltype.metas, 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)
 
 @app.route("/type/delete/<int:type_id>")
 @login_required
@@ -1135,6 +1138,53 @@ def delete_todomail(todomail_id):
     flash("Die Todo-Mail-Zuordnung für {} wurde gelöscht.".format(name), "alert-success")
     return redirect(request.args.get("next") or url_for("list_todomails"))
     
+@app.route("/defaultmeta/new/<int:type_id>", methods=["GET", "POST"])
+@login_required
+def new_defaultmeta(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 unzureichende Rechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    form = DefaultMetaForm()
+    if form.validate_on_submit():
+        meta = DefaultMeta(protocoltype_id=type_id, key=form.key.data,
+            name=form.name.data)
+        db.session.add(meta)
+        db.session.commit()
+        flash("Metadatenfeld hinzugefügt.", "alert-success")
+        return redirect(request.args.get("next") or url_for("show_type", type_id=type_id))
+    return render_template("defaultmeta-new.html", form=form, protocoltype=protocoltype)
+
+@app.route("/defaultmeta/edit/<int:meta_id>", methods=["GET", "POST"])
+@login_required
+def edit_defaultmeta(meta_id):
+    user = current_user()
+    meta = DefaultMeta.query.filter_by(id=meta_id).first()
+    if meta is None or not meta.protocoltype.has_modify_right(user):
+        flash("Invalider Protokolltyp oder unzureichende Rechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    form = DefaultMetaForm(obj=meta)
+    if form.validate_on_submit():
+        form.populate_obj(meta)
+        db.session.commit()
+        return redirect(request.args.get("next") or url_for("show_type", type_id=meta.protocoltype.id))
+    return render_template("defaultmeta-edit.html", form=form, meta=meta)
+
+@app.route("/defaultmeta/delete/<int:meta_id>")
+@login_required
+def delete_defaultmeta(meta_id):
+    user = current_user()
+    meta = DefaultMeta.query.filter_by(id=meta_id).first()
+    if meta is None or not meta.protocoltype.has_modify_right(user):
+        flash("Invalider Protokolltyp oder unzureichende Rechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    name = meta.name
+    type_id = meta.protocoltype.id
+    db.session.delete(meta)
+    db.session.delete()
+    flash("Metadatenfeld '{}' gelöscht.", "alert-error")
+    return redirect(request.args.get("next") or url_for("show_type", type_id=type_id))
 
 @app.route("/login", methods=["GET", "POST"])
 def login():
diff --git a/shared.py b/shared.py
index c27e87f..9874755 100644
--- a/shared.py
+++ b/shared.py
@@ -81,9 +81,11 @@ def needs_date_test(todostate):
 def todostate_name_filter(todostate):
     return todostate.get_name()
 
+def indent_tab_filter(text):
+    return "\n".join(map(lambda l: "\t{}".format(l), text.splitlines()))
+
 def class_filter(obj):
     return obj.__class__.__name__
-
 def code_filter(text):
     return "<code>{}</code>".format(text)
 
@@ -122,9 +124,4 @@ def group_required(function, group):
 DATE_KEY = "Datum"
 START_TIME_KEY = "Beginn"
 END_TIME_KEY = "Ende"
-AUTHOR_KEY = "Autor"
-PARTICIPANTS_KEY = "Anwesende"
-LOCATION_KEY = "Ort"
-KNOWN_KEYS = [DATE_KEY, START_TIME_KEY, END_TIME_KEY, AUTHOR_KEY,
-    PARTICIPANTS_KEY, LOCATION_KEY
-]
+KNOWN_KEYS = [DATE_KEY, START_TIME_KEY, END_TIME_KEY]
diff --git a/tasks.py b/tasks.py
index b8822fe..ebcd560 100644
--- a/tasks.py
+++ b/tasks.py
@@ -5,6 +5,7 @@ import subprocess
 import shutil
 import tempfile
 from datetime import datetime
+import traceback
 
 from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP, MeetingReminder, TodoMail, DecisionDocument, TodoState, OldTodo
 from models.errors import DateNotMatchingException
@@ -60,7 +61,9 @@ def parse_protocol_async(protocol_id, encoded_kwargs):
                     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))
+                stacktrace = traceback.format_exc()
+                error = protocol.create_error("Parsing", "Exception",
+                    "{}\n\n{}".format(str(exc), stacktrace))
                 db.session.add(error)
                 db.session.commit()
 
@@ -92,6 +95,8 @@ def parse_protocol_async_inner(protocol, encoded_kwargs):
         return
     remarks = {element.name: element for element in tree.children if isinstance(element, Remark)}
     required_fields = KNOWN_KEYS
+    for default_meta in protocol.protocoltype.metas:
+        required_fields.append(default_meta.key)
     if not config.PARSER_LAZY:
         missing_fields = [field for field in required_fields if field not in remarks]
         if len(missing_fields) > 0:
@@ -489,7 +494,7 @@ def push_tops_to_calendar_async(protocol_id):
             db.session.commit()
 
 def set_etherpad_content(protocol):
-    set_etherpad_content_async.delay(protocol_id)
+    set_etherpad_content_async.delay(protocol.id)
 
 @celery.task
 def set_etherpad_content_async(protocol_id):
diff --git a/templates/decision.tex b/templates/decision.tex
index ad042ba..6a98ccb 100644
--- a/templates/decision.tex
+++ b/templates/decision.tex
@@ -24,17 +24,11 @@
 }{}
 \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}\\
-\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}\\
+    {\bf Datum:} & \VAR{protocol.date|datify_long|escape_tex}\\
 \ENV{endif}
+\ENV{for meta in protocol.metas}
+    {\bf \ENV{meta.name|escape_tex}:} & \VAR{meta.value|escape_tex}\\
+\ENV{endfor}
 \end{tabular}
 \normalsize
 
diff --git a/templates/defaultmeta-edit.html b/templates/defaultmeta-edit.html
new file mode 100644
index 0000000..9418992
--- /dev/null
+++ b/templates/defaultmeta-edit.html
@@ -0,0 +1,9 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}Metadatum ändern{% endblock %}
+
+{% block content %}
+<div class="container">
+    {{render_form(form, action_url=url_for("edit_defaultmeta", meta_id=meta.id, next=url_for("show_type", type_id=meta.protocoltype.id)), action_text="Ändern")}}
+</div>
+{% endblock %}
diff --git a/templates/defaultmeta-new.html b/templates/defaultmeta-new.html
new file mode 100644
index 0000000..8a7c0cc
--- /dev/null
+++ b/templates/defaultmeta-new.html
@@ -0,0 +1,9 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}Metadatum hinzufügen{% endblock %}
+
+{% block content %}
+<div class="container">
+    {{render_form(form, action_url=url_for("new_defaultmeta", type_id=protocoltype.id, next=url_for("show_type", type_id=protocoltype.id)), action_text="Anlegen")}}
+</div>
+{% endblock %}
diff --git a/templates/index.html b/templates/index.html
index e32617a..f185605 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -54,15 +54,9 @@
             {% if protocol.start_time is not none and protocol.end_time is not none %}
                 <p><strong>Zeit:</strong> von {{protocol.start_time|timify}} bis {{protocol.end_time|timify}}</p>
             {% endif %}
-            {% if protocol.location is not none %}
-                <p><strong>Ort:</strong> {{protocol.location}}</p>
-            {% endif %}
-            {% if protocol.author is not none %}
-                <p><strong>Protokoll:</strong> {{protocol.author}}</p>
-            {% endif %}
-            {% if protocol.participants is not none %}
-                <p><strong>Anwesende:</strong> {{protocol.participants}}</p>
-            {% endif %}
+            {% for meta in protocol.metas %}
+                <p><strong>{{meta.name}}:</strong> {{meta.value}}</p>
+            {% endfor %}
             <h3>Tagesordnung{% if has_modify_right and not protocol.has_nonplanned_tops() %} <a href="{{url_for("new_top", protocol_id=protocol.id)}}">Top hinzufügen</a>{% endif %}</h3>
             {% include "protocol-tops-include.html" %}
 
diff --git a/templates/protocol-show.html b/templates/protocol-show.html
index 3185396..a78aa6d 100644
--- a/templates/protocol-show.html
+++ b/templates/protocol-show.html
@@ -59,15 +59,9 @@
                 {% if protocol.start_time is not none and protocol.end_time is not none %}
                     <p><strong>Zeit:</strong> von {{protocol.start_time|timify}} bis {{protocol.end_time|timify}}</p>
                 {% endif %}
-                {% if protocol.location is not none %}
-                    <p><strong>Ort:</strong> {{protocol.location}}</p>
-                {% endif %}
-                {% 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 and has_public_view_right %}
-                    <p><strong>Anwesende:</strong> {{protocol.participants}}</p>
-                {% endif %}
+                {% for meta in protocol.metas %}
+                    <p><strong>{{meta.name}}:</strong> {{meta.value}}</p>
+                {% endfor %}
             {% else %}
                 {% if protocol.date is not none %}<p><strong>Geplant:</strong> {{protocol.date|datify_long}}{% endif %}</p>
             {% endif %}
diff --git a/templates/protocol-template.txt b/templates/protocol-template.txt
index 04e09c7..d250c82 100644
--- a/templates/protocol-template.txt
+++ b/templates/protocol-template.txt
@@ -1,9 +1,9 @@
-#Datum;{{protocol.date|datify}}
-#Anwesende;
-#Beginn;
+#Datum;{{protocol.date|datify_short}}
+#Beginn;{{protocol.protocoltype.usual_time|timify}}
 #Ende;
-#Autor;
-#Ort;
+{% for defaultmeta in protocol.protocoltype.metas %}
+#{{defaultmeta.key}};
+{% endfor %}
 
 {% macro render_top(top, use_description=False) %}
 {TOP {{top.name}}
@@ -11,16 +11,20 @@
                 {% set todos=protocol.get_open_todos() %}
                 {% if todos|length > 0 %}
                     {% for todo in todos %}
-                        {{-todo.render_template()|indent(indentfirst=True)}};
+	{{todo.render_template()}};
                     {% endfor %}
                 {% else %}
-
                 {% endif %}
             {% else %}
                 {% if use_description %}
-                    {{-top.description|indent(indentfirst=True)}}
+					{% if top.description|length > 0 %}
+{{top.description|indent_tab}}
+					{% else %}
+	
+					{% endif %}
+				{% else %}
+	
                 {% endif %}
-
             {% endif %}
 }
 {% endmacro -%}
@@ -33,12 +37,12 @@
     {% endfor %}
 {% endif %}
 {% for top in protocol.tops %}
-    {{-render_top(top, use_description=True)}}
+{{render_top(top, use_description=True)}}
 {% endfor %}
 {% if not protocol.has_nonplanned_tops() %}
     {% for default_top in protocol.protocoltype.default_tops %}
         {% if default_top.is_at_end() %}
-            {{-render_top(default_top)}}
+{{render_top(default_top)}}
         {% endif %}
     {% endfor %}
 {% endif %}
diff --git a/templates/protocol.tex b/templates/protocol.tex
index 784684d..2004b24 100644
--- a/templates/protocol.tex
+++ b/templates/protocol.tex
@@ -26,15 +26,9 @@
 \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}\\
-\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}
+\ENV{for meta in protocol.metas}
+    {\bf \VAR{meta.name|escape_tex}:} & \VAR{meta.value|escape_tex}\\
+\ENV{endfor}
 \end{tabular}
 \normalsize
 
diff --git a/templates/protocol.wiki b/templates/protocol.wiki
index 7d9c816..1bfc8ef 100644
--- a/templates/protocol.wiki
+++ b/templates/protocol.wiki
@@ -1,3 +1,4 @@
+{#
 {{'{{'}}Infobox Protokoll
 | name = {{protocol.protocoltype.name}}
 {% if protocol.date is not none %}
@@ -13,6 +14,21 @@
 | anwesende = {{protocol.participants}}
 {% endif %}
 {{'}}'}}
+#}
+'''{{protocol.protocoltype.name}}'''
+
+{% if protocol.date is not none %}
+Datum: '''{{protocol.date|datify_long}}'''
+{% endif %}
+
+{% if protocol.start_time is not none and protocol.end_time is not none %}
+Zeit: '''von {{protocol.start_time|timify}} bis {{protocol.end_time|timify}}'''
+{% endif %}
+
+{% for meta in protocol.metas %}
+{{meta.name}}: '''{{meta.value}}'''
+
+{% endfor %}
 
 == Beschlüsse ==
 {% if protocol.decisions|length > 0 %}
diff --git a/templates/type-show.html b/templates/type-show.html
index 415ad18..6049d10 100644
--- a/templates/type-show.html
+++ b/templates/type-show.html
@@ -10,5 +10,6 @@
     {% if mail_active %}
         {{render_table(reminders_table)}}
     {% endif %}
+    {{render_table(metas_table)}}
 </div>
 {% endblock %}
diff --git a/views/forms.py b/views/forms.py
index f33f3e9..0f7764e 100644
--- a/views/forms.py
+++ b/views/forms.py
@@ -43,7 +43,8 @@ def get_printer_choices():
 
 def get_group_choices():
     user = current_user()
-    choices = list(zip(user.groups, user.groups))
+    groups = sorted(user.groups)
+    choices = list(zip(groups, groups))
     choices.insert(0, ("", "Keine Gruppe"))
     return choices
 
@@ -173,3 +174,11 @@ class TodoForm(FlaskForm):
 class TodoMailForm(FlaskForm):
     name = StringField("Name", validators=[InputRequired("Du musst den Namen angeben, der zugeordnet werden soll.")])
     mail = StringField("Mail", validators=[InputRequired("Du musst die Mailadresse angeben, die zugeordnet werden soll.")])
+
+class MetaForm(FlaskForm):
+    name = StringField("Name", validators=[InputRequired("Bitte gib den Namen der Metadaten an.")])
+    value = StringField("Wert")
+
+class DefaultMetaForm(FlaskForm):
+    key = StringField("Key", validators=[InputRequired("Bitte gib den Protokoll-Syntax-Schlüssel der Metadaten an.")])
+    name = StringField("Name", validators=[InputRequired("Bitte gib den Namen der Metadaten an.")])
diff --git a/views/tables.py b/views/tables.py
index c0fc831..9599273 100644
--- a/views/tables.py
+++ b/views/tables.py
@@ -1,6 +1,6 @@
 # coding: utf-8
 from flask import Markup, url_for, request
-from models.database import Protocol, ProtocolType, DefaultTOP, TOP, Todo, Decision
+from models.database import Protocol, ProtocolType, DefaultTOP, TOP, Todo, Decision, Meta, DefaultMeta
 from shared import date_filter, datetime_filter, date_filter_short, current_user, check_login 
 
 import config
@@ -352,3 +352,23 @@ class TodoMailsTable(Table):
             ])
         ]
 
+class DefaultMetasTable(Table):
+    def __init__(self, metas, protocoltype):
+        super().__init__(
+            "Metadatenfelder",
+            metas,
+            url_for("new_defaultmeta", type_id=protocoltype.id)
+        )
+
+    def headers(self):
+        return ["Name", "Key", ""]
+
+    def row(self, meta):
+        return [
+            meta.name,
+            meta.key,
+            Table.concat([
+                Table.link(url_for("edit_defaultmeta", meta_id=meta.id), "Ändern"),
+                Table.link(url_for("delete_defaultmeta", meta_id=meta.id, confirm="Bist du dir sicher, dass du das Metadatenfeld {} löschen willst?".format(meta.name)), "Löschen")
+            ])
+        ]
-- 
GitLab