From 29f92eef3c82b669d6263760fc60ea8e1b639a71 Mon Sep 17 00:00:00 2001
From: Robin Sonnabend <robin@fsmpi.rwth-aachen.de>
Date: Wed, 22 Feb 2017 16:02:49 +0100
Subject: [PATCH] Implemented default tops

---
 migrations/versions/a3d9d1b87ba0_.py          |  28 +++
 models/database.py                            |  29 ++-
 server.py                                     | 169 +++++++++++++++++-
 static/css/style.css                          |   4 +-
 templates/default-top-edit.html               |   9 +
 templates/default-top-new.html                |   9 +
 templates/layout.html                         |   3 +
 templates/macros.html                         |  27 ++-
 ...protocol-list.html => protocols-list.html} |   0
 templates/type-edit.html                      |   9 +
 templates/type-new.html                       |   9 +
 templates/type-show.html                      |  11 ++
 templates/types-list.html                     |   9 +
 views/forms.py                                |  20 ++-
 views/tables.py                               |  67 ++++++-
 15 files changed, 384 insertions(+), 19 deletions(-)
 create mode 100644 migrations/versions/a3d9d1b87ba0_.py
 create mode 100644 templates/default-top-edit.html
 create mode 100644 templates/default-top-new.html
 rename templates/{protocol-list.html => protocols-list.html} (100%)
 create mode 100644 templates/type-edit.html
 create mode 100644 templates/type-new.html
 create mode 100644 templates/type-show.html
 create mode 100644 templates/types-list.html

diff --git a/migrations/versions/a3d9d1b87ba0_.py b/migrations/versions/a3d9d1b87ba0_.py
new file mode 100644
index 0000000..989795b
--- /dev/null
+++ b/migrations/versions/a3d9d1b87ba0_.py
@@ -0,0 +1,28 @@
+"""empty message
+
+Revision ID: a3d9d1b87ba0
+Revises: efaa3b4fd3e8
+Create Date: 2017-02-22 16:00:02.816515
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'a3d9d1b87ba0'
+down_revision = 'efaa3b4fd3e8'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('protocols', sa.Column('done', sa.Boolean(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('protocols', 'done')
+    # ### end Alembic commands ###
diff --git a/models/database.py b/models/database.py
index 9479ca0..9d0a6f6 100644
--- a/models/database.py
+++ b/models/database.py
@@ -39,8 +39,27 @@ class ProtocolType(db.Model):
         self.public_mail = public_mail
 
     def __repr__(self):
-        return "<ProtocolType(id={}, short_name={}, name={}, organization={})>".format(
-            self.id, self.short_name, self.name, self.organization)
+        return "<ProtocolType(id={}, short_name={}, name={}, organization={}, is_public={}, private_group={}, public_group={})>".format(
+            self.id, self.short_name, self.name, self.organization, self.is_public, self.private_group, self.public_group)
+
+    def get_latest_protocol(self):
+        candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.data, reverse=True)
+        if len(candidates) == 0:
+            return None
+        return candidates[0]
+
+    def has_public_view_right(self, user):
+        return (self.is_public
+            or (user is not None and 
+                ((self.public_group != "" and self.public_group in user.groups)
+                or (self.private_group != "" and self.private_group in user.groups))))
+
+    def has_private_view_right(self, user):
+        return (self.private_group != "" and self.private_group in user.groups)
+
+    def has_modify_right(self, user):
+        return self.has_private_view_right(user)
+
 
 class Protocol(db.Model):
     __tablename__ = "protocols"
@@ -53,6 +72,7 @@ class Protocol(db.Model):
     author = db.Column(db.String)
     participants = db.Column(db.String)
     location = db.Column(db.String)
+    done = db.Column(db.Boolean)
 
     tops = relationship("TOP", backref=backref("protocol"), cascade="all, delete-orphan", order_by="TOP.number")
     decisions = relationship("Decision", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Decision.id")
@@ -85,6 +105,9 @@ class Protocol(db.Model):
         self.participants = remarks["Anwesende"].value
         self.location = remarks["Ort"].value
 
+    def is_done(self):
+        return self.done
+
 
 class DefaultTOP(db.Model):
     __tablename__ = "defaulttops"
@@ -103,7 +126,7 @@ class DefaultTOP(db.Model):
             self.id, self.protocoltype_id, self.name, self.number)
 
     def is_at_end(self):
-        return self.number < 0
+        return self.number > 0
 
 class TOP(db.Model):
     __tablename__ = "tops"
diff --git a/server.py b/server.py
index b2c6849..03dccc6 100755
--- a/server.py
+++ b/server.py
@@ -13,8 +13,8 @@ import config
 from shared import db, date_filter, datetime_filter, ldap_manager, security_manager
 from utils import is_past, mail_manager, url_manager
 from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error
-from views.forms import LoginForm
-from views.tables import ProtocolsTable
+from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm
+from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable
 
 app = Flask(__name__)
 app.config.from_object(config)
@@ -61,16 +61,175 @@ def login_required(function):
             return redirect(url_for("login", next=request.url))
     return decorated_function
 
+def group_required(function, group):
+    @wraps(function)
+    def decorated_function(*args, **kwargs):
+        if group in current_user.groups:
+            return function(*args, **kwargs)
+        else:
+            flash("You do not have the necessary permissions to view this page.")
+            return redirect(request.args.get("next") or url_for("index"))
+    return decorated_function
+
 app.jinja_env.globals.update(check_login=check_login)
 app.jinja_env.globals.update(current_user=current_user)
+app.jinja_env.globals.update(zip=zip)
 
 # blueprints here
 
 @app.route("/")
-#@login_required
 def index():
     return render_template("index.html")
 
+@login_required
+@app.route("/types/list")
+def list_types():
+    is_logged_in = check_login()
+    user = current_user()
+    types = [
+        protocoltype for protocoltype in ProtocolType.query.all()
+        if (protocoltype.public_group in user.groups
+        or protocoltype.private_group in user.groups
+        or protocoltype.is_public)]
+    types_table = ProtocolTypesTable(types)
+    return render_template("types-list.html", types=types, types_table=types_table)
+
+@app.route("/type/new", methods=["GET", "POST"])
+@login_required
+def new_type():
+    form = ProtocolTypeForm()
+    if form.validate_on_submit():
+        user = current_user()
+        if form.private_group.data not in user.groups:
+            flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error")
+        else:
+            protocoltype = ProtocolType(form.name.data, form.short_name.data,
+                form.organization.data, form.is_public.data,
+                form.private_group.data, form.public_group.data,
+                form.private_mail.data, form.public_mail.data)
+            db.session.add(protocoltype)
+            db.session.commit()
+            flash("Der Protokolltyp {} wurde angelegt.".format(protocoltype.name), "alert-success")
+        return redirect(request.args.get("next") or url_for("list_types"))
+    return render_template("type-new.html", form=form)
+
+@app.route("/type/edit/<int:type_id>", methods=["GET", "POST"])
+@login_required
+def edit_type(type_id):
+    protocoltype = ProtocolType.query.filter_by(id=type_id).first()
+    if protocoltype is None:
+        flash("Dieser Protokolltyp existiert nicht.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    user = current_user()
+    if not protocoltype.has_private_view_right(user):
+        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    form = ProtocolTypeForm(obj=protocoltype)
+    if form.validate_on_submit():
+        if form.private_group.data not in user.groups:
+            flash("Du kannst keinen internen Protokolltypen anlegen, zu dem du selbst keinen Zugang hast.", "alert-error")
+        else:
+            form.populate_obj(protocoltype)
+            db.session.commit()
+            return redirect(request.args.get("next") or url_for("show_type", type_id=protocoltype.id))
+    return render_template("type-edit.html", form=form, protocoltype=protocoltype)
+
+@app.route("/type/show/<int:type_id>")
+@login_required
+def show_type(type_id):
+    protocoltype = ProtocolType.query.filter_by(id=type_id).first()
+    if protocoltype is None:
+        flash("Dieser Protokolltyp existiert nicht.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    user = current_user()
+    if not protocoltype.has_private_view_right(user):
+        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    protocoltype_table = ProtocolTypeTable(protocoltype)
+    default_tops_table = DefaultTOPsTable(protocoltype.default_tops, protocoltype)
+    return render_template("type-show.html", protocoltype=protocoltype, protocoltype_table=protocoltype_table, default_tops_table=default_tops_table)
+
+@app.route("/type/tops/new/<int:type_id>", methods=["GET", "POST"])
+@login_required
+def new_default_top(type_id):
+    protocoltype = ProtocolType.query.filter_by(id=type_id).first()
+    if protocoltype is None:
+        flash("Dieser Protokolltyp existiert nicht.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    user = current_user()
+    if not protocoltype.has_modify_right(user):
+        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    form = DefaultTopForm()
+    if form.validate_on_submit():
+        default_top = DefaultTOP(protocoltype.id, form.name.data, form.number.data)
+        db.session.add(default_top)
+        db.session.commit()
+        flash("Der Standard-TOP {} wurde für dem Protokolltyp {} hinzugefügt.".format(default_top.name, protocoltype.name), "alert-success")
+        return redirect(request.args.get("next") or url_for("index"))
+    return render_template("default-top-new.html", form=form, protocoltype=protocoltype)
+
+@app.route("/type/tops/edit/<int:type_id>/<int:top_id>", methods=["GET", "POST"])
+@login_required
+def edit_default_top(type_id, top_id):
+    protocoltype = ProtocolType.query.filter_by(id=type_id).first()
+    if protocoltype is None:
+        flash("Dieser Protokolltyp existiert nicht.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    user = current_user()
+    if not protocoltype.has_modify_right(user):
+        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    default_top = DefaultTOP.query.filter_by(id=top_id).first()
+    if default_top is None or default_top.protocoltype != protocoltype:
+        flash("Invalider Standard-TOP.", "alert-error")
+        return redirect(request.args.get("nexT") or url_for("index"))
+    form = DefaultTopForm(obj=default_top)
+    if form.validate_on_submit():
+        form.populate_obj(default_top)
+        db.session.commit()
+        return redirect(request.args.get("next") or url_for("show_type", type_id=protocoltype.id))
+    return render_template("default-top-edit.html", form=form, protocoltype=protocoltype, default_top=default_top)
+
+@app.route("/type/tops/delete/<int:type_id>/<int:top_id>")
+@login_required
+def delete_default_top(type_id, top_id):
+    protocoltype = ProtocolType.query.filter_by(id=type_id).first()
+    if protocoltype is None:
+        flash("Dieser Protokolltyp existiert nicht.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    user = current_user()
+    if not protocoltype.has_modify_right(user):
+        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    default_top = DefaultTOP.query.filter_by(id=top_id).first()
+    if default_top is None or default_top.protocoltype != protocoltype:
+        flash("Invalider Standard-TOP.", "alert-error")
+        return redirect(request.args.get("nexT") or url_for("index"))
+    db.session.delete(default_top)
+    db.session.commit()
+    return redirect(request.args.get("next") or url_for("show_type", type_id=protocoltype.id))
+
+@app.route("/type/tops/move/<int:type_id>/<int:top_id>/<diff>/")
+@login_required
+def move_default_top(type_id, top_id, diff):
+    protocoltype = ProtocolType.query.filter_by(id=type_id).first()
+    if protocoltype is None:
+        flash("Dieser Protokolltyp existiert nicht.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    user = current_user()
+    if not protocoltype.has_modify_right(user):
+        flash("Dir fehlen die nötigen Zugriffsrechte.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    default_top = DefaultTOP.query.filter_by(id=top_id).first()
+    if default_top is None or default_top.protocoltype != protocoltype:
+        flash("Invalider Standard-TOP.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    default_top.number += int(diff)
+    db.session.commit()
+    return redirect(request.args.get("next") or url_for("show_type", type_id=protocoltype.id))
+    
+
 @app.route("/protocol/list")
 def list_protocols():
     is_logged_in = check_login()
@@ -82,14 +241,14 @@ def list_protocols():
             protocol.protocoltype.public_group in user.groups
             or protocol.protocoltype.private_group in user.groups))]
     protocols_table = ProtocolsTable(protocols)
-    return render_template("protocol-list.html", protocols=protocols, protocols_table=protocols_table)
+    return render_template("protocols-list.html", protocols=protocols, protocols_table=protocols_table)
     
 
 @app.route("/login", methods=["GET", "POST"])
 def login():
     if "auth" in session:
         flash("You are already logged in.", "alert-success")
-        return redirect(url_for(request.args.get("next") or "index"))
+        return redirect(request.args.get("next") or url_for("index"))
     form = LoginForm()
     if form.validate_on_submit():
         user = ldap_manager.login(form.username.data, form.password.data)
diff --git a/static/css/style.css b/static/css/style.css
index 895b254..062285a 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -19,4 +19,6 @@ body {
     color: #808000;
 }
 
-
+h3 > a {
+    font-size: 18px;
+}
diff --git a/templates/default-top-edit.html b/templates/default-top-edit.html
new file mode 100644
index 0000000..8dee99e
--- /dev/null
+++ b/templates/default-top-edit.html
@@ -0,0 +1,9 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}Standard-TOP bearbeiten{% endblock %}
+
+{% block content %}
+<div class="container">
+    {{render_form(form, action_url=url_for("edit_default_top", type_id=protocoltype.id, top_id=default_top.id, next=url_for("show_type", type_id=protocoltype.id)), action_text="Ändern")}}
+</div>
+{% endblock %}
diff --git a/templates/default-top-new.html b/templates/default-top-new.html
new file mode 100644
index 0000000..d65a621
--- /dev/null
+++ b/templates/default-top-new.html
@@ -0,0 +1,9 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}Standard-TOP hinzufügen{% endblock %}
+
+{% block content %}
+<div class="container">
+    {{render_form(form, action_url=url_for("new_default_top", type_id=protocoltype.id, next=url_for("show_type", type_id=protocoltype.id)), action_text="Anlegen")}}
+</div>
+{% endblock %}
diff --git a/templates/layout.html b/templates/layout.html
index 31b61d5..9156b39 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -28,6 +28,9 @@
             <ul class="nav navbar-nav">
                 <li><a href="{{url_for("index")}}">Zuhause</a></li>
                 <li><a href="{{url_for("list_protocols")}}">Protokolle</a></li>
+                {% if check_login() %}
+                <li><a href="{{url_for("list_types")}}">Typen</a></li>
+                {% endif %}
                 {# todo: add more links #}
             </ul>
             <ul class="nav navbar-nav navbar-right">
diff --git a/templates/macros.html b/templates/macros.html
index 8ef7e6e..86a8989 100644
--- a/templates/macros.html
+++ b/templates/macros.html
@@ -42,7 +42,7 @@ to not render a label for the CRSFTokenField -->
         <label>
             {{ field(type='checkbox', **kwargs) }} {{ field.label }} 
         </label>
-        <span onclick="el=document.getElementById('{{field.id}}-description');el.style.display=(el.style.display=='none'?'flex':'none')" class="field-description-questionmark">?</span>
+        <!--<span onclick="el=document.getElementById('{{field.id}}-description');el.style.display=(el.style.display=='none'?'flex':'none')" class="field-description-questionmark">?</span>-->
         {% if field.errors %}
             {% for e in field.errors %}
                 <p class="help-block">{{ e }}</p>
@@ -109,6 +109,12 @@ to not render a label for the CRSFTokenField -->
 {%- endmacro %}
 
 {% macro render_table(table) -%}
+    <h3>
+        {{table.title}}
+        {% if table.newlink is not none %}
+            <a href="{{table.newlink}}">{{table.newtext}}</a>
+        {% endif %}
+    </h3>
     <table class="table table-striped">
         <thead>
             <tr>
@@ -128,3 +134,22 @@ to not render a label for the CRSFTokenField -->
         </tbody>
     </table>
 {%- endmacro %}
+
+{% macro render_single_table(table) -%}
+    <h3>
+        {{table.title}}
+        {% if table.newlink is not none %}
+            <a href="{{table.newlink}}">{{table.newtext}}</a>
+        {% endif %}
+    </h3>
+    <table class="table table-striped">
+        <tbody>
+            {% for key, value in zip(table.headers(), table.row()) %}
+                <tr>
+                    <td>{{key}}</td>
+                    <td>{{value}}</td>
+                </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+{%- endmacro %}
diff --git a/templates/protocol-list.html b/templates/protocols-list.html
similarity index 100%
rename from templates/protocol-list.html
rename to templates/protocols-list.html
diff --git a/templates/type-edit.html b/templates/type-edit.html
new file mode 100644
index 0000000..1bb548a
--- /dev/null
+++ b/templates/type-edit.html
@@ -0,0 +1,9 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}Protokolltyp ändern{% endblock %}
+
+{% block content %}
+<div class="container">
+    {{render_form(form, action_url=url_for("edit_type", type_id=protocoltype.id), action_text="Ändern")}}
+</div>
+{% endblock %}
diff --git a/templates/type-new.html b/templates/type-new.html
new file mode 100644
index 0000000..1fa1972
--- /dev/null
+++ b/templates/type-new.html
@@ -0,0 +1,9 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}Protokolltyp anlegen{% endblock %}
+
+{% block content %}
+<div class="container">
+    {{render_form(form, action_url=url_for("new_type"), action_text="Anlegen")}}
+</div>
+{% endblock %}
diff --git a/templates/type-show.html b/templates/type-show.html
new file mode 100644
index 0000000..ee4f3aa
--- /dev/null
+++ b/templates/type-show.html
@@ -0,0 +1,11 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_table, render_single_table %}
+{% block title %}Protokolltyp {{protocoltype.short_name}}{% endblock %}
+
+{% block content %}
+<div class="container">
+    {{render_single_table(protocoltype_table)}}
+    {{render_table(default_tops_table)}}
+    Standard-TOPs mit negativer Sortierung werden vor und die mit positiver Sortierung nach den TOPs eingefügt.
+</div>
+{% endblock %}
diff --git a/templates/types-list.html b/templates/types-list.html
new file mode 100644
index 0000000..cb0b4ba
--- /dev/null
+++ b/templates/types-list.html
@@ -0,0 +1,9 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_table %}
+{% block title %}Protokolltypen{% endblock %}
+
+{% block content %}
+<div class="container">
+    {{render_table(types_table)}}
+</div>
+{% endblock %}
diff --git a/views/forms.py b/views/forms.py
index 175fcc2..095c5b8 100644
--- a/views/forms.py
+++ b/views/forms.py
@@ -1,7 +1,21 @@
 from flask_wtf import FlaskForm
-from wtforms import StringField, PasswordField
+from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField
 from wtforms.validators import InputRequired
 
 class LoginForm(FlaskForm):
-    username = StringField("User", validators=[InputRequired("Please input the username.")])
-    password = PasswordField("Password", validators=[InputRequired("Please input the password.")])
+    username = StringField("Benutzer", validators=[InputRequired("Bitte gib deinen Benutzernamen ein.")])
+    password = PasswordField("Passwort", validators=[InputRequired("Bitte gib dein Passwort ein.")])
+
+class ProtocolTypeForm(FlaskForm):
+    name = StringField("Name", validators=[InputRequired("Du musst einen Namen angeben.")])
+    short_name = StringField("Abkürzung", validators=[InputRequired("Du musst eine Abkürzung angebene.")])
+    organization = StringField("Organisation", validators=[InputRequired("Du musst eine zugehörige Organisation angeben.")])
+    is_public = BooleanField("Öffentlich sichtbar")
+    private_group = StringField("Interne Gruppe")
+    public_group = StringField("Öffentliche Gruppe")
+    private_mail = StringField("Interner Verteiler")
+    public_mail = StringField("Öffentlicher Verteiler")
+
+class DefaultTopForm(FlaskForm):
+    name = StringField("Name", validators=[InputRequired("Du musst einen Namen angeben.")])
+    number = IntegerField("Nummer", validators=[InputRequired("Du musst eine Nummer angeben.")])
diff --git a/views/tables.py b/views/tables.py
index bb3a44d..034ea0a 100644
--- a/views/tables.py
+++ b/views/tables.py
@@ -4,11 +4,11 @@ from models.database import Protocol, ProtocolType, DefaultTOP, TOP, Todo, Decis
 from shared import date_filter
 
 class Table:
-    def __init__(self, title, values, newlink=None):
+    def __init__(self, title, values, newlink=None, newtext=None):
         self.title = title
         self.values = values
         self.newlink = newlink
-        self.newtext = "New"
+        self.newtext = newtext or "Neu"
 
     def rows(self):
         return [row for row in [self.row(value) for value in self.values] if row is not None]
@@ -26,11 +26,11 @@ class Table:
 
     @staticmethod
     def bool(value):
-        return "Yes" if value else "No"
+        return "Ja" if value else "Nein"
 
     @staticmethod
     def concat(values):
-        return ", ".join(values)
+        return Markup(", ".join(values))
         #if len(values) <= 1:
         #    return "".join(values)
         #else:
@@ -38,11 +38,11 @@ class Table:
             
 
 class SingleValueTable:
-    def __init__(self, title, value, newlink=None):
+    def __init__(self, title, value, newlink=None, newtext=None):
         self.title = title
         self.value = value
         self.newlink = newlink if newlink else None
-        self.newtext = "Edit"
+        self.newtext = newtext or "Ändern"
 
     def rows(self):
         return [self.row()]
@@ -61,3 +61,58 @@ class ProtocolsTable(Table):
             date_filter(protocol.data)
         ]
 
+class ProtocolTypesTable(Table):
+    def __init__(self, types):
+        super().__init__("Protokolltypen", types, newlink=url_for("new_type"))
+
+    def headers(self):
+        return ["Typ", "Name", "Neuestes Protokoll", ""]
+
+    def row(self, protocoltype):
+        return [
+            Table.link(url_for("show_type", type_id=protocoltype.id), protocoltype.short_name),
+            protocoltype.name,
+            protocoltype.get_latest_protocol() or "Noch kein Protokoll",
+            "" # TODO: add links for new, modify, delete
+        ]
+
+class ProtocolTypeTable(SingleValueTable):
+    def __init__(self, protocoltype):
+        super().__init__(protocoltype.name, protocoltype, newlink=url_for("edit_type", type_id=protocoltype.id))
+
+    def headers(self):
+        return ["Name", "Abkürzung", "Organisation", "Öffentlich",
+            "Interne Gruppe", "Öffentliche Gruppe",
+            "Interner Verteiler", "Öffentlicher Verteiler"]
+
+    def row(self):
+        return [
+            self.value.name,
+            self.value.short_name,
+            self.value.organization,
+            Table.bool(self.value.is_public),
+            self.value.private_group,
+            self.value.public_group,
+            self.value.private_mail,
+            self.value.public_mail
+        ]
+
+class DefaultTOPsTable(Table):
+    def __init__(self, tops, protocoltype=None):
+        super().__init__("Standard-TOPs", tops, newlink=url_for("new_default_top", type_id=protocoltype.id) if protocoltype is not None else None)
+        self.protocoltype = protocoltype
+
+    def headers(self):
+        return ["TOP", "Sortierung", ""]
+
+    def row(self, top):
+        return [
+            top.name,
+            top.number,
+            Table.concat([
+                Table.link(url_for("move_default_top", type_id=self.protocoltype.id, top_id=top.id, diff=1), "Runter"),
+                Table.link(url_for("move_default_top", type_id=self.protocoltype.id, top_id=top.id, diff=-1), "Hoch"),
+                Table.link(url_for("edit_default_top", type_id=self.protocoltype.id, top_id=top.id), "Ändern"),
+                Table.link(url_for("delete_default_top", type_id=self.protocoltype.id, top_id=top.id), "Löschen", confirm="Bist du dir sicher, dass du den Standard-TOP {} löschen willst?".format(top.name))
+            ])
+        ]
-- 
GitLab