From b03b40da776d30080357b5d4955f352528a33e83 Mon Sep 17 00:00:00 2001
From: Robin Sonnabend <robin@fsmpi.rwth-aachen.de>
Date: Fri, 28 Apr 2017 22:14:26 +0200
Subject: [PATCH] Model decision categories as many to many relation

/close #53
---
 migrations/versions/60a730d37c9a_.py | 30 ++++++++++++++++++++++++
 migrations/versions/ac54a2c29610_.py | 34 ++++++++++++++++++++++++++++
 models/database.py                   | 12 +++++++---
 parser.py                            | 15 +++++++-----
 server.py                            |  3 +--
 tasks.py                             | 13 +++++------
 templates/protocol.tex               |  2 +-
 views/tables.py                      |  4 ++--
 8 files changed, 92 insertions(+), 21 deletions(-)
 create mode 100644 migrations/versions/60a730d37c9a_.py
 create mode 100644 migrations/versions/ac54a2c29610_.py

diff --git a/migrations/versions/60a730d37c9a_.py b/migrations/versions/60a730d37c9a_.py
new file mode 100644
index 0000000..7342b55
--- /dev/null
+++ b/migrations/versions/60a730d37c9a_.py
@@ -0,0 +1,30 @@
+"""empty message
+
+Revision ID: 60a730d37c9a
+Revises: ac54a2c29610
+Create Date: 2017-04-28 22:10:24.694024
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '60a730d37c9a'
+down_revision = 'ac54a2c29610'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_constraint('decisions_category_id_fkey', 'decisions', type_='foreignkey')
+    op.drop_column('decisions', 'category_id')
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('decisions', sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=True))
+    op.create_foreign_key('decisions_category_id_fkey', 'decisions', 'decisioncategories', ['category_id'], ['id'])
+    # ### end Alembic commands ###
diff --git a/migrations/versions/ac54a2c29610_.py b/migrations/versions/ac54a2c29610_.py
new file mode 100644
index 0000000..2916bea
--- /dev/null
+++ b/migrations/versions/ac54a2c29610_.py
@@ -0,0 +1,34 @@
+"""empty message
+
+Revision ID: ac54a2c29610
+Revises: c59998057d39
+Create Date: 2017-04-20 19:30:26.887146
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'ac54a2c29610'
+down_revision = 'c59998057d39'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('decisioncategoryassociations',
+    sa.Column('decision_id', sa.Integer(), nullable=False),
+    sa.Column('decisioncategory_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['decision_id'], ['decisions.id'], ),
+    sa.ForeignKeyConstraint(['decisioncategory_id'], ['decisioncategories.id'], ),
+    sa.PrimaryKeyConstraint('decision_id', 'decisioncategory_id')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('decisioncategoryassociations')
+    # ### end Alembic commands ###
diff --git a/models/database.py b/models/database.py
index 5ecf463..0646ff7 100644
--- a/models/database.py
+++ b/models/database.py
@@ -610,15 +610,18 @@ class Decision(DatabaseModel):
     id = db.Column(db.Integer, primary_key=True)
     protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
     content = db.Column(db.String)
-    category_id = db.Column(db.Integer, db.ForeignKey("decisioncategories.id"), nullable=True)
 
     document = relationship("DecisionDocument", backref=backref("decision"), cascade="all, delete-orphan", uselist=False)
 
+    categories = relationship("DecisionCategory", secondary="decisioncategoryassociations")
     likes = relationship("Like", secondary="likedecisionassociations")
 
     def get_parent(self):
         return self.protocol
 
+    def get_categories_str(self):
+        return ", ".join(map(lambda c: c.name, self.categories))
+
 class DecisionCategory(DatabaseModel):
     __tablename__ = "decisioncategories"
     __model_name__ = "decisioncategory"
@@ -626,11 +629,14 @@ class DecisionCategory(DatabaseModel):
     protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id"))
     name = db.Column(db.String)
 
-    decisions = relationship("Decision", backref=backref("category"), order_by="Decision.id")
-
     def get_parent(self):
         return self.protocoltype
 
+class DecisionCategoryAssociation(DatabaseModel):
+    __tablename__ = "decisioncategoryassociations"
+    decision_id = db.Column(db.Integer, db.ForeignKey("decisions.id"), primary_key=True)
+    decisioncategory_id = db.Column(db.Integer, db.ForeignKey("decisioncategories.id"), primary_key=True)
+
 class MeetingReminder(DatabaseModel):
     __tablename__ = "meetingreminders"
     __model_name__ = "meetingreminder"
diff --git a/parser.py b/parser.py
index 6ac3757..369b302 100644
--- a/parser.py
+++ b/parser.py
@@ -217,10 +217,12 @@ class Tag:
                     return ""
                 return self.todo.render_latex(current_protocol=protocol)
             elif self.name == "beschluss":
-                result = r"\textbf{{Beschluss:}} {}".format(self.decision.content)
-                if self.decision.category is not None:
-                    result = r"{} \textit{{({})}}".format(result, self.decision.category.name)
-                return result
+                parts = [r"\textbf{{Beschluss:}} {}".format(self.decision.content)]
+                if len(self.decision.categories):
+                    parts.append(
+                        r"\textit{{({})}}".format(self.decision.get_categories_str())
+                    )
+                return " ".join(parts)
             return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(";".join(self.values)))
         elif render_type == RenderType.plaintext:
             if self.name == "url":
@@ -251,8 +253,9 @@ class Tag:
             elif self.name == "beschluss":
                 if getattr(self, "decision", None) is not None:
                     parts = ["<b>Beschluss:</b>", self.decision.content]
-                    if self.decision.category is not None:
-                        parts.append("<i>{}</i>".format(self.decision.category.name))
+                    if len(self.decision.categories) > 0:
+                        parts.append("<i>{}</i>".format(
+                            self.decision.get_categories_str()))
                     return " ".join(parts)
                 else:
                     return "<b>Beschluss:</b> {}".format(self.values[0])
diff --git a/server.py b/server.py
index de2fbe8..6e91019 100755
--- a/server.py
+++ b/server.py
@@ -1034,8 +1034,7 @@ def list_decisions():
     if decisioncategory_id is not None and decisioncategory_id != -1:
         decisions = [
             decision for decision in decisions
-            if decision.category is not None
-            and decision.category.id == decisioncategory_id
+            if decisioncategory_id in map(lambda d: d.id, decision.categories)
         ]
     if search_term is not None and len(search_term.strip()) > 0:
         decisions = [
diff --git a/tasks.py b/tasks.py
index d1eff0d..b9ba248 100644
--- a/tasks.py
+++ b/tasks.py
@@ -50,8 +50,6 @@ if raw_additional_packages is not None:
         if "{" not in package:
             package = "{{{}}}".format(package)
         additional_packages.append(package)
-print(raw_additional_packages)
-print(additional_packages)
 texenv.globals["additional_packages"] = additional_packages
 latex_pagestyle = getattr(config, "LATEX_PAGESTYLE", None)
 if latex_pagestyle is not None:
@@ -314,9 +312,8 @@ def parse_protocol_async_inner(protocol, encoded_kwargs):
             db.session.commit()
             return
         decision_content = decision_tag.values[0]
-        decision_category_id = None
-        if len(decision_tag.values) > 1:
-            decision_category_name = decision_tag.values[1]
+        decision_categories = []
+        for decision_category_name in decision_tag.values[1:]:
             decision_category = DecisionCategory.query.filter_by(protocoltype_id=protocol.protocoltype.id, name=decision_category_name).first()
             if decision_category is None:
                 category_candidates = DecisionCategory.query.filter_by(protocoltype_id=protocol.protocoltype.id).all()
@@ -336,11 +333,13 @@ def parse_protocol_async_inner(protocol, encoded_kwargs):
                 db.session.commit()
                 return
             else:
-                decision_category_id = decision_category.id
+                decision_categories.append(decision_category)
         decision = Decision(protocol_id=protocol.id,
-            content=decision_content, category_id=decision_category_id)
+            content=decision_content)
         db.session.add(decision)
         db.session.commit()
+        for decision_category in decision_categories:
+            decision.categories.append(decision_category)
         decision_tag.decision = decision
         decisions_to_render.append((decision, decision_tag))
     for decision, decision_tag in decisions_to_render:
diff --git a/templates/protocol.tex b/templates/protocol.tex
index f29e5a2..c433c9f 100644
--- a/templates/protocol.tex
+++ b/templates/protocol.tex
@@ -41,7 +41,7 @@
 \begin{itemize}
 \ENV{if protocol.decisions|length > 0}
     \ENV{for decision in protocol.decisions}
-        \item \VAR{decision.content|escape_tex}\ENV{if decision.category is not none and show_private} \textit{(\VAR{decision.category.name})}\ENV{endif}
+        \item \VAR{decision.content|escape_tex}\ENV{if decision.categories|length > 0 and show_private} \textit{(\VAR{decision.get_categories_str()})}\ENV{endif}
     \ENV{endfor}
 \ENV{else}
 	\item Keine Beschlüsse
diff --git a/views/tables.py b/views/tables.py
index 02b71d9..9f3054d 100644
--- a/views/tables.py
+++ b/views/tables.py
@@ -384,7 +384,7 @@ class DecisionsTable(Table):
         super().__init__("Beschlüsse", decisions)
         self.category_present = len([
             decision for decision in decisions
-            if decision.category is not None
+            if len(decision.categories) > 0
         ]) > 0
 
     def headers(self):
@@ -401,7 +401,7 @@ class DecisionsTable(Table):
             Table.link(url_for("show_protocol", protocol_id=decision.protocol.id), decision.protocol.get_short_identifier()),
             decision.content
         ]
-        category_part = [decision.category.name if decision.category is not None else ""]
+        category_part = [decision.get_categories_str()]
         if not self.category_present:
             category_part = []
         action_part = [
-- 
GitLab