From 42e8970d02531877e9531c4d5f0cebd9a94fa8d4 Mon Sep 17 00:00:00 2001
From: Robin Sonnabend <robin@fsmpi.rwth-aachen.de>
Date: Wed, 15 Mar 2017 22:59:28 +0100
Subject: [PATCH] Add descriptions for default tops (per protocol)

/close #64
---
 migrations/versions/a06cc03bdef4_.py | 36 +++++++++++++++++++++++
 models/database.py                   | 43 +++++++++++++++++++++++++++-
 server.py                            | 16 +++++++++--
 templates/calendar-tops.txt          | 16 +----------
 templates/localtop-edit.html         | 10 +++++++
 templates/protocol-mail.txt          | 18 ++----------
 templates/protocol-template.txt      |  4 +--
 templates/protocol-tops-include.html | 28 ++++++++++++++++--
 templates/reminder-mail.txt          | 18 ++----------
 views/forms.py                       |  3 ++
 10 files changed, 138 insertions(+), 54 deletions(-)
 create mode 100644 migrations/versions/a06cc03bdef4_.py
 create mode 100644 templates/localtop-edit.html

diff --git a/migrations/versions/a06cc03bdef4_.py b/migrations/versions/a06cc03bdef4_.py
new file mode 100644
index 0000000..fa7bc45
--- /dev/null
+++ b/migrations/versions/a06cc03bdef4_.py
@@ -0,0 +1,36 @@
+"""empty message
+
+Revision ID: a06cc03bdef4
+Revises: 4b813bbbd8ef
+Create Date: 2017-03-15 22:47:07.462793
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'a06cc03bdef4'
+down_revision = '4b813bbbd8ef'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('localtops',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('protocol_id', sa.Integer(), nullable=True),
+    sa.Column('defaulttop_id', sa.Integer(), nullable=True),
+    sa.Column('description', sa.String(), nullable=True),
+    sa.ForeignKeyConstraint(['defaulttop_id'], ['defaulttops.id'], ),
+    sa.ForeignKeyConstraint(['protocol_id'], ['protocols.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('localtops')
+    # ### end Alembic commands ###
diff --git a/models/database.py b/models/database.py
index 07e5cee..05b0f06 100644
--- a/models/database.py
+++ b/models/database.py
@@ -98,7 +98,6 @@ class ProtocolType(DatabaseModel):
         return ((user is not None
             and (self.private_group != "" and self.private_group in user.groups))
             or self.has_admin_right(user))
-            
 
     def has_modify_right(self, user):
         return ((user is not None
@@ -135,6 +134,7 @@ class ProtocolType(DatabaseModel):
     def get_wiki_infobox_title(self):
         return "Vorlage:{}".format(self.get_wiki_infobox())
 
+
 class Protocol(DatabaseModel):
     __tablename__ = "protocols"
     __model_name__ = "protocol"
@@ -155,6 +155,7 @@ class Protocol(DatabaseModel):
     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")
+    localtops = relationship("LocalTOP", backref=backref("protocol"), cascade="all, delete-orphan")
 
     def get_parent(self):
         return self.protocoltype
@@ -282,6 +283,17 @@ class Protocol(DatabaseModel):
             self.todos.remove(todo)
             db.session.delete(todo)
 
+    def get_tops(self):
+        tops_before, tops_after = [], []
+        if not self.has_nonplanned_tops():
+            for default_top in self.protocoltype.default_tops:
+                top = default_top.get_top(self)
+                if default_top.is_at_end():
+                    tops_after.append(top)
+                else:
+                    tops_before.append(top)
+        return tops_before + self.tops + tops_after
+
 @event.listens_for(Protocol, "before_delete")
 def on_protocol_delete(mapper, connection, protocol):
     protocol.delete_orphan_todos()
@@ -295,12 +307,30 @@ class DefaultTOP(DatabaseModel):
     name = db.Column(db.String)
     number = db.Column(db.Integer)
 
+    localtops = relationship("LocalTOP", backref=backref("defaulttop"), cascade="all, delete-orphan")
+
     def get_parent(self):
         return self.protocoltype
 
     def is_at_end(self):
         return self.number > 0
 
+    def get_localtop(self, protocol):
+        localtop = LocalTOP.query.filter_by(defaulttop_id=self.id,
+            protocol_id=protocol.id).first()
+        if localtop is None:
+            localtop = LocalTOP(protocol_id=protocol.id, defaulttop_id=self.id,
+                description="")
+            db.session.add(localtop)
+            db.session.commit()
+        return localtop
+
+    def get_top(self, protocol):
+        localtop = self.get_localtop(protocol)
+        top = TOP(protocol_id=protocol.id, name=self.name,
+            description=localtop.description)
+        return top
+
 class TOP(DatabaseModel):
     __tablename__ = "tops"
     __model_name__ = "top"
@@ -314,6 +344,17 @@ class TOP(DatabaseModel):
     def get_parent(self):
         return self.protocol
 
+class LocalTOP(DatabaseModel):
+    __tablename__ = "localtops"
+    __model_name__ = "localtop"
+    id = db.Column(db.Integer, primary_key=True)
+    protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
+    defaulttop_id = db.Column(db.Integer, db.ForeignKey("defaulttops.id"))
+    description = db.Column(db.String)
+
+    def get_parent(self):
+        return self.protocol
+
 class Document(DatabaseModel):
     __tablename__ = "documents"
     __model_name__ = "document"
diff --git a/server.py b/server.py
index 6a16991..5f92e8e 100755
--- a/server.py
+++ b/server.py
@@ -23,8 +23,8 @@ import config
 from shared import db, date_filter, datetime_filter, date_filter_long, date_filter_short, time_filter, time_filter_short, 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 decorators import db_lookup, require_public_view_right, require_private_view_right, require_modify_right, require_admin_right
-from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta, DecisionCategory
-from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, SearchForm, DecisionSearchForm, ProtocolSearchForm, TodoSearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm, MergeTodosForm, DecisionCategoryForm
+from models.database import ProtocolType, Protocol, DefaultTOP, TOP, LocalTOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState, Meta, DefaultMeta, DecisionCategory
+from views.forms import LoginForm, ProtocolTypeForm, DefaultTopForm, MeetingReminderForm, NewProtocolForm, DocumentUploadForm, KnownProtocolSourceUploadForm, NewProtocolSourceUploadForm, ProtocolForm, TopForm, LocalTopForm, SearchForm, DecisionSearchForm, ProtocolSearchForm, TodoSearchForm, NewProtocolFileUploadForm, NewTodoForm, TodoForm, TodoMailForm, DefaultMetaForm, MetaForm, MergeTodosForm, DecisionCategoryForm
 from views.tables import ProtocolsTable, ProtocolTypesTable, ProtocolTypeTable, DefaultTOPsTable, MeetingRemindersTable, ErrorsTable, TodosTable, DocumentsTable, DecisionsTable, TodoTable, ErrorTable, TodoMailsTable, DefaultMetasTable, DecisionCategoriesTable
 from legacy import import_old_todos, import_old_protocols, import_old_todomails
 
@@ -760,6 +760,18 @@ def move_top(top, diff):
         flash("Die angegebene Differenz ist keine Zahl.", "alert-error")
     return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=top.protocol.id))
 
+@app.route("/protocol/localtop/edit/<int:localtop_id>", methods=["GET", "POST"])
+@login_required
+@db_lookup(LocalTOP)
+@require_modify_right()
+def edit_localtop(localtop):
+    form = LocalTopForm(obj=localtop)
+    if form.validate_on_submit():
+        form.populate_obj(localtop)
+        db.session.commit()
+        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=localtop.protocol.id))
+    return render_template("localtop-edit.html", form=form, localtop=localtop)
+
 def _get_page():
     try:
         page = request.args.get("page")
diff --git a/templates/calendar-tops.txt b/templates/calendar-tops.txt
index a9d8da6..a2325f0 100644
--- a/templates/calendar-tops.txt
+++ b/templates/calendar-tops.txt
@@ -1,17 +1,3 @@
-{% if not protocol.has_nonplanned_tops() %}
-    {% for default_top in protocol.protocoltype.default_tops %}
-        {% if not default_top.is_at_end() %}
-{{default_top.name}}
-        {% endif %}
-    {% endfor %}
-{% endif %}
-{% for top in protocol.tops %}
+{% for top in protocol.get_tops() %}
 {{top.name}}
 {% endfor %}
-{% if not protocol.has_nonplanned_tops() %}
-    {% for default_top in protocol.protocoltype.default_tops %}
-        {% if default_top.is_at_end() %}
-{{default_top.name}}
-        {% endif %}
-    {% endfor %}
-{% endif %}
diff --git a/templates/localtop-edit.html b/templates/localtop-edit.html
new file mode 100644
index 0000000..c320d72
--- /dev/null
+++ b/templates/localtop-edit.html
@@ -0,0 +1,10 @@
+{% extends "layout.html" %}
+{% from "macros.html" import render_form %}
+{% block title %}TOP-Beschreibung bearbeiten{% endblock %}
+
+{% block content %}
+<div class="container">
+    <h3>{{localtop.defaulttop.name}}</h3>
+    {{render_form(form, action_url=url_for("edit_localtop", localtop_id=localtop.id, next=request.args.get("next") or url_for("show_protocol", protocol_id=localtop.protocol.id)), action_text="Ändern", textarea_rows=5)}}
+</div>
+{% endblock %}
diff --git a/templates/protocol-mail.txt b/templates/protocol-mail.txt
index fa09eb6..4235afc 100644
--- a/templates/protocol-mail.txt
+++ b/templates/protocol-mail.txt
@@ -9,23 +9,9 @@ Zeit: von {{protocol.start_time|timify}} bis {{protocol.end_time|timify}}
 {% endfor %}
 
 Die Tagesordnung ist:
-{% if not protocol.has_nonplanned_tops() %}
-    {% for default_top in protocol.protocoltype.default_tops %}
-        {% if not default_top.is_at_end() %}
-* {{default_top.name}}
-        {% endif %}
-    {% endfor %}
-{% endif %}
-{% for top in protocol.tops %}
-* {{top.name }}
+{% for top in protocol.get_tops() %}
+* {{top.name}}
 {% endfor %}
-{% if not protocol.has_nonplanned_tops() %}
-    {% for default_top in protocol.protocoltype.default_tops %}
-        {% if default_top.is_at_end() %}
-* {{default_top.name}}
-        {% endif %}
-    {% endfor %}
-{% endif %}
 
 Beschlüsse:
 {% if protocol.decisions|length > 0 %}
diff --git a/templates/protocol-template.txt b/templates/protocol-template.txt
index 84309cc..e512056 100644
--- a/templates/protocol-template.txt
+++ b/templates/protocol-template.txt
@@ -32,7 +32,7 @@
 {% if not protocol.has_nonplanned_tops() %}
     {% for default_top in protocol.protocoltype.default_tops %}
         {% if not default_top.is_at_end() %}
-            {{-render_top(default_top)}}
+            {{-render_top(default_top.get_top(protocol), use_description=True)}}
         {% endif %}
     {% endfor %}
 {% endif %}
@@ -42,7 +42,7 @@
 {% 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.get_top(protocol), use_description=True)}}
         {% endif %}
     {% endfor %}
 {% endif %}
diff --git a/templates/protocol-tops-include.html b/templates/protocol-tops-include.html
index 93a962a..b1aefb6 100644
--- a/templates/protocol-tops-include.html
+++ b/templates/protocol-tops-include.html
@@ -2,7 +2,19 @@
     {% if not protocol.has_nonplanned_tops() %}
         {% for default_top in protocol.protocoltype.default_tops %}
             {% if not default_top.is_at_end() %}
-                <li>{{default_top.name}}</li>
+                {% set localtop = default_top.get_localtop(protocol) %}
+                <li{% if has_private_view_right and localtop.description is not none and localtop.description|length > 0 %} class="expansion-button" id="localtop-{{localtop.id}}" title="{{localtop.description}}"{% endif %}>
+                    {{default_top.name}}
+                    {% if not protocol.is_done() and has_modify_right %}
+                        <a href="{{url_for('edit_localtop', localtop_id=localtop.id)}}">Ändern</a>
+                    {% endif %}
+                    {% if has_private_view_right and localtop.description is not none and localtop.description|length > 0 %}
+                        <span class="glyphicon glyphicon-info-sign"></span>
+                        <pre id="localtop-{{localtop.id}}-description" class="expansion-text">
+                            {{-localtop.description-}}
+                        </pre>
+                    {% endif %}
+                </li>
             {% endif %}
         {% endfor %}
     {% endif %}
@@ -29,7 +41,19 @@
     {% if not protocol.has_nonplanned_tops() %}
         {% for default_top in protocol.protocoltype.default_tops %}
             {% if default_top.is_at_end() %}
-                <li>{{default_top.name}}</li>
+                {% set localtop = default_top.get_localtop(protocol) %}
+                <li{% if has_private_view_right and localtop.description is not none and localtop.description|length > 0 %} class="expansion-button" id="localtop-{{localtop.id}}" title="{{localtop.description}}"{% endif %}>
+                    {{default_top.name}}
+                    {% if not protocol.is_done() and has_modify_right %}
+                        <a href="{{url_for('edit_localtop', localtop_id=localtop.id)}}">Ändern</a>
+                    {% endif %}
+                    {% if has_private_view_right and localtop.description is not none and localtop.description|length > 0 %}
+                        <span class="glyphicon glyphicon-info-sign"></span>
+                        <pre id="localtop-{{localtop.id}}-description" class="expansion-text">
+                            {{-localtop.description-}}
+                        </pre>
+                    {% endif %}
+                </li>
             {% endif %}
         {% endfor %}
     {% endif %}
diff --git a/templates/reminder-mail.txt b/templates/reminder-mail.txt
index c78c32d..d0180fb 100644
--- a/templates/reminder-mail.txt
+++ b/templates/reminder-mail.txt
@@ -1,24 +1,10 @@
 Die nächste {{protocol.protocoltype.name}} findet am {{protocol.date|datify}} um {{protocol.protocoltype.usual_time|timify}} statt.
 
 Die vorläufige Tagesordnung ist:
-{% if not protocol.has_nonplanned_tops() %}
-    {% for default_top in protocol.protocoltype.default_tops %}
-        {% if not default_top.is_at_end() %}
-* {{default_top.name}}
-        {% endif %}
-    {% endfor %}
-{% endif %}
-{% for top in protocol.tops %}
+{% for top in protocol.get_tops() %}
 * {{top.name }}
 {% endfor %}
-{% if not protocol.has_nonplanned_tops() %}
-    {% for default_top in protocol.protocoltype.default_tops %}
-        {% if default_top.is_at_end() %}
-* {{default_top.name}}
-        {% endif %}
-    {% endfor %}
-{% endif %}
-
 {% if reminder.additional_text is not none %}
+
 {{reminder.additional_text}}
 {% endif %}
diff --git a/views/forms.py b/views/forms.py
index f499d52..ae4ea0d 100644
--- a/views/forms.py
+++ b/views/forms.py
@@ -190,6 +190,9 @@ class TopForm(FlaskForm):
     number = IntegerField("Sortierung", validators=[InputRequired("Du musst eine Sortierung in der Reihenfolge angebene.")])
     description = TextAreaField("Beschreibung")
 
+class LocalTopForm(FlaskForm):
+    description = TextAreaField("Beschreibung")
+
 class SearchForm(FlaskForm):
     search = StringField("Suchbegriff")
     protocoltype_id = SelectField("Typ", choices=[], coerce=int)
-- 
GitLab