From 5ed297b070f71ead4f8b18c7c61a1ac87ba11335 Mon Sep 17 00:00:00 2001
From: Robin Sonnabend <robin@fsmpi.rwth-aachen.de>
Date: Sun, 26 Feb 2017 17:12:33 +0100
Subject: [PATCH] Printing decisions

---
 migrations/versions/0e5220a9f169_.py | 37 +++++++++++++++++++++++++
 models/database.py                   | 38 ++++++++++++++++++++++++--
 parser.py                            | 29 +++++++++++++-------
 server.py                            | 25 +++++++++++++++--
 tasks.py                             | 35 ++++++++++++++++++------
 templates/decision.tex               | 41 ++++++++++++++++++++++++++++
 templates/protocol-show.html         |  7 ++++-
 views/tables.py                      | 12 ++++++--
 8 files changed, 196 insertions(+), 28 deletions(-)
 create mode 100644 migrations/versions/0e5220a9f169_.py
 create mode 100644 templates/decision.tex

diff --git a/migrations/versions/0e5220a9f169_.py b/migrations/versions/0e5220a9f169_.py
new file mode 100644
index 0000000..027be45
--- /dev/null
+++ b/migrations/versions/0e5220a9f169_.py
@@ -0,0 +1,37 @@
+"""empty message
+
+Revision ID: 0e5220a9f169
+Revises: 188f389b2286
+Create Date: 2017-02-26 15:53:41.410353
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '0e5220a9f169'
+down_revision = '188f389b2286'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('decisiondocuments',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('decision_id', sa.Integer(), nullable=True),
+    sa.Column('name', sa.String(), nullable=True),
+    sa.Column('filename', sa.String(), nullable=True),
+    sa.Column('is_compiled', sa.Boolean(), nullable=True),
+    sa.Column('is_private', sa.Boolean(), nullable=True),
+    sa.ForeignKeyConstraint(['decision_id'], ['decisions.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('decisiondocuments')
+    # ### end Alembic commands ###
diff --git a/models/database.py b/models/database.py
index e91c3f4..b041a2f 100644
--- a/models/database.py
+++ b/models/database.py
@@ -289,10 +289,42 @@ class Document(db.Model):
 @event.listens_for(Document, "before_delete")
 def on_document_delete(mapper, connection, document):
     if document.filename is not None:
-        document_path = os.path.join(config.DOCUMENTS_PATH, document.filename)
+        document_path = document.get_filename()
         if os.path.isfile(document_path):
             os.remove(document_path)
 
+class DecisionDocument(db.Model):
+    __tablename__ = "decisiondocuments"
+    id = db.Column(db.Integer, primary_key=True)
+    decision_id = db.Column(db.Integer, db.ForeignKey("decisions.id"))
+    name = db.Column(db.String)
+    filename = db.Column(db.String)
+
+    def __init__(self, decision_id, name, filename):
+        self.decision_id = decision_id
+        self.name = name
+        self.filename = filename
+
+    def __repr__(self):
+        return "<DecisionDocument(id={}, decision_id={}, name={}, filename={})>".format(
+            self.id, self.decision_id, self.name, self.filename)
+
+    def get_filename(self):
+        return os.path.join(config.DOCUMENTS_PATH, self.filename)
+
+    def as_file_like(self):
+        with open(self.get_filename(), "rb") as file:
+            return BytesIO(file.read())
+
+@event.listens_for(DecisionDocument, "before_delete")
+def on_decisions_document_delete(mapper, connection, document):
+    if document.filename is not None:
+        document_path = document.get_filename()
+        if os.path.isfile(document_path):
+            os.remove(document_path)
+
+
+
 class Todo(db.Model):
     __tablename__ = "todos"
     id = db.Column(db.Integer, primary_key=True)
@@ -381,6 +413,8 @@ class Decision(db.Model):
     protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"))
     content = db.Column(db.String)
 
+    document = relationship("DecisionDocument", backref=backref("decision"), cascade="all, delete-orphan", uselist=False)
+
     def __init__(self, protocol_id, content):
         self.protocol_id = protocol_id
         self.content = content
@@ -433,7 +467,7 @@ class Error(db.Model):
         lines = self.description.splitlines()
         if len(lines) <= 4:
             return "\n".join(lines)
-        return "\n".join(lines[:2], "…", lines[-2:])
+        return "\n".join([*lines[:2], "…", *lines[-2:]])
 
 class TodoMail(db.Model):
     __tablename__ = "todomails"
diff --git a/parser.py b/parser.py
index ec8dd56..069aa80 100644
--- a/parser.py
+++ b/parser.py
@@ -94,7 +94,9 @@ class Element:
         current.append(element)
         if isinstance(element, Fork):
             return element
-        return current
+        else:
+            element.fork = current
+            return current
 
     PATTERN = r"x(?<!x)" # yes, a master piece, but it should never be called
 
@@ -123,14 +125,14 @@ class Content(Element):
         if match.group("content") is None:
             raise ParserException("Content is missing its content!", linenumber)
         content = match.group("content")
-        element = Content.from_content(content, linenumber)
+        element = Content.from_content(content, current, linenumber)
         if len(content) == 0:
             return current, linenumber
         current = Element.parse_outer(element, current)
         return current, linenumber
 
     @staticmethod
-    def from_content(content, linenumber):
+    def from_content(content, current, linenumber):
         children = []
         while len(content) > 0:
             matched = False
@@ -138,7 +140,7 @@ class Content(Element):
                 match = pattern.match(content)
                 if match is not None:
                     matched = True
-                    children.append(TEXT_PATTERNS[pattern](match, linenumber))
+                    children.append(TEXT_PATTERNS[pattern](match, current, linenumber))
                     content = content[len(match.group()):]
                     break
             if not matched:
@@ -151,9 +153,10 @@ class Content(Element):
     PATTERN = r"\s*(?<content>(?:[^\[\];\r\n]+)?(?:\[[^\]\r\n]+\][^;\[\]\r\n]*)*);?"
 
 class Text:
-    def __init__(self, text, linenumber):
+    def __init__(self, text, linenumber, fork):
         self.text = text
         self.linenumber = linenumber
+        self.fork = fork
 
     def render(self, render_type, show_private, level=None, protocol=None):
         if render_type == RenderType.latex:
@@ -171,22 +174,23 @@ class Text:
         print("{}text: {}".format(" " * level, self.text))
 
     @staticmethod
-    def parse(match, linenumber):
+    def parse(match, current, linenumber):
         if match is None:
             raise ParserException("Text is not actually a text!", linenumber)
         content = match.group("text")
         if content is None:
             raise ParserException("Text is empty!", linenumber)
-        return Text(content, linenumber)
+        return Text(content, linenumber, current)
 
     PATTERN = r"(?<text>[^\[]+)(?:(?=\[)|$)"
 
 
 class Tag:
-    def __init__(self, name, values, linenumber):
+    def __init__(self, name, values, linenumber, fork):
         self.name = name
         self.values = values
         self.linenumber = linenumber
+        self.fork = fork
 
     def render(self, render_type, show_private, level=None, protocol=None):
         if render_type == RenderType.latex:
@@ -222,14 +226,14 @@ class Tag:
         print("{}tag: {}: {}".format(" " * level, self.name, "; ".join(self.values)))
 
     @staticmethod
-    def parse(match, linenumber):
+    def parse(match, current, linenumber):
         if match is None:
             raise ParserException("Tag is not actually a tag!", linenumber)
         content = match.group("content")
         if content is None:
             raise ParserException("Tag is empty!", linenumber)
         parts = content.split(";")
-        return Tag(parts[0], parts[1:], linenumber)
+        return Tag(parts[0], parts[1:], linenumber, current)
 
     PATTERN = r"\[(?<content>(?:[^;\]]*;)*(?:[^;\]]*))\]"
 
@@ -377,6 +381,11 @@ class Fork(Element):
     def is_root(self):
         return self.parent is None
 
+    def get_top(self):
+        if self.is_root() or self.parent.is_root():
+            return self
+        return self.parent.get_top()
+
     @staticmethod
     def create_root():
         return Fork(None, None, None, 0)
diff --git a/server.py b/server.py
index 2caa76e..da4f0c2 100755
--- a/server.py
+++ b/server.py
@@ -21,7 +21,7 @@ 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
 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
+from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument
 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
 
@@ -86,8 +86,13 @@ def index():
         if protocol.date is not None:
             return protocol.date
         return datetime.now().date()
+    current_day = datetime.now().date()
     open_protocols = sorted(
-        [protocol for protocol in protocols if not protocol.done],
+        [
+            protocol for protocol in protocols
+            if not protocol.done
+            and (protocol.date - current_day).days < config.MAX_INDEX_DAYS
+        ],
         key=_sort_key
     )
     finished_protocols = sorted(
@@ -660,7 +665,6 @@ def new_top(protocol_id):
         db.session.commit()
         return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id))
     else:
-        print(form.number.data)
         current_numbers = list(map(lambda t: t.number, protocol.tops))
         suggested_number = get_first_unused_int(current_numbers)
         form.number.data = suggested_number
@@ -967,6 +971,21 @@ def print_document(document_id):
     flash("Das Dokument {} wird gedruckt.".format(document.name), "alert-success")
     return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=document.protocol.id))
 
+@app.route("/decision/print/<int:document_id>")
+@login_required
+def print_decision(document_id):
+    user = current_user()
+    document = DecisionDocument.query.filter_by(id=document_id).first()
+    if document is None or not document.decision.protocol.protocoltype.has_modify_right(user):
+        flash("Invalides Dokument oder keine Berechtigung.", "alert-error")
+        return redirect(request.args.get("next") or url_for("index"))
+    if not config.PRINTING_ACTIVE:
+        flash("Die Druckfunktion ist nicht aktiviert.", "alert-error")
+        return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=document.decision.protocol.id))
+    tasks.print_file(document.get_filename(), document.decision.protocol)
+    flash("Das Dokument {} wird gedruckt.".format(document.name), "alert-success")
+    return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=document.decision.protocol.id))
+
 @app.route("/errors/list")
 @login_required
 def list_errors():
diff --git a/tasks.py b/tasks.py
index 5ea7a53..4bfdca1 100644
--- a/tasks.py
+++ b/tasks.py
@@ -5,7 +5,7 @@ import subprocess
 import shutil
 import tempfile
 
-from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP, MeetingReminder, TodoMail
+from models.database import Document, Protocol, Error, Todo, Decision, TOP, DefaultTOP, MeetingReminder, TodoMail, DecisionDocument
 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
@@ -182,6 +182,9 @@ def parse_protocol_async(protocol_id, encoded_kwargs):
                 decision = Decision(protocol_id=protocol.id, content=decision_tag.values[0])
                 db.session.add(decision)
                 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)
+                print(decision_content)
+                compile_decision(decision_content, decision)
             old_tops = list(protocol.tops)
             for top in old_tops:
                 protocol.tops.remove(top)
@@ -229,12 +232,21 @@ def push_to_wiki_async(protocol_id, content, summary):
             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)
+   compile_async.delay(content, protocol.id, show_private=show_private)
+
+def compile_decision(content, decision):
+    compile_async.delay(content, decision.id, use_decision=True)
 
 @celery.task
-def compile_async(content, protocol_id, show_private):
+def compile_async(content, protocol_id, show_private=False, use_decision=False):
     with tempfile.TemporaryDirectory() as compile_dir, app.app_context():
-        protocol = Protocol.query.filter_by(id=protocol_id).first()
+        decision = None
+        protocol = None
+        if use_decision:
+            decision = Decision.query.filter_by(id=protocol_id).first()
+            protocol = decision.protocol
+        else:
+            protocol = Protocol.query.filter_by(id=protocol_id).first()
         try:
             current = os.getcwd()
             protocol_source_filename = "protocol.tex"
@@ -255,17 +267,22 @@ def compile_async(content, protocol_id, show_private):
             subprocess.check_call(command, universal_newlines=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
             subprocess.check_call(command, universal_newlines=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
             os.chdir(current)
-            for old_document in [document for document in protocol.documents if document.is_compiled and document.is_private == show_private]:
-                protocol.documents.remove(old_document)
-            db.session.commit()
-            document = Document(protocol.id, name="protokoll{}_{}_{}.pdf".format("_intern" if show_private else "", protocol.protocoltype.short_name, date_filter_short(protocol.date)), filename="", is_compiled=True, is_private=show_private)
+            document = None
+            if not use_decision:
+                for old_document in [document for document in protocol.documents if document.is_compiled and document.is_private == show_private]:
+                    protocol.documents.remove(old_document)
+                db.session.commit()
+                document = Document(protocol.id, name="protokoll{}_{}_{}.pdf".format("_intern" if show_private else "", protocol.protocoltype.short_name, date_filter_short(protocol.date)), filename="", is_compiled=True, is_private=show_private)
+            else:
+                document = DecisionDocument(decision.id, name="beschluss_{}_{}_{}.pdf".format(protocol.protocoltype.short_name, date_filter_short(protocol.date), decision.id), filename="")
             db.session.add(document)
             db.session.commit()
             target_filename = "compiled-{}-{}.pdf".format(document.id, "internal" if show_private else "public")
+            if use_decision:
+                target_filename = "decision-{}-{}-{}.pdf".format(protocol.id, decision.id, document.id)
             document.filename = target_filename
             shutil.copy(os.path.join(compile_dir, protocol_target_filename), os.path.join(config.DOCUMENTS_PATH, target_filename))
             db.session.commit()
-            #shutil.copy(os.path.join(compile_dir, log_filename), "/tmp")
         except subprocess.SubprocessError:
             log = ""
             total_log_filename = os.path.join(compile_dir, log_filename)
diff --git a/templates/decision.tex b/templates/decision.tex
new file mode 100644
index 0000000..8cc7a93
--- /dev/null
+++ b/templates/decision.tex
@@ -0,0 +1,41 @@
+\documentclass[11pt,twoside]{protokoll2}
+%\usepackage{bookman}
+%\usepackage{newcent}
+%\usepackage{palatino}
+\usepackage{pdfpages}
+\usepackage{eurosym}
+%\usepackage[utf8]{inputenc}
+\usepackage[pdfborder={0 0 0}]{hyperref}
+%\usepackage{ngerman}
+% \usepackage[left]{lineno}
+%\usepackage{footnote}
+%\usepackage{times}
+\renewcommand{\thefootnote}{\fnsymbol{footnote}}
+\renewcommand{\thempfootnote}{\fnsymbol{mpfootnote}}
+%\renewcommand{\familydefault}{\sfdefault}
+\newcommand{\einrueck}[1]{\hfill\begin{minipage}{0.95\linewidth}#1\end{minipage}}
+
+
+\begin{document}
+%\thispagestyle{plain}   %ggf kommentarzeichen entfernen
+\Titel{
+\large Protokoll: \VAR{protocol.protocoltype.name|escape_tex}
+\\\normalsize \VAR{protocol.protocoltype.organization|escape_tex}
+}{}
+\begin{tabular}{rp{14cm}}
+{\bf Datum:} & \VAR{protocol.date|datify_long|escape_tex}\\
+{\bf Ort:} & \VAR{protocol.location|escape_tex}\\
+{\bf Protokollant:} & \VAR{protocol.author|escape_tex}\\
+{\bf Anwesend:} & \VAR{protocol.participants|escape_tex}\\
+\end{tabular}
+\normalsize
+
+\section*{Beschluss}
+\begin{itemize}
+    \item \VAR{decision.content|escape_tex}
+\end{itemize}
+
+\TOP{\VAR{top.name|escape_tex}}
+\VAR{top.render(render_type=render_type, level=0, show_private=show_private, protocol=protocol)}
+
+\end{document}
diff --git a/templates/protocol-show.html b/templates/protocol-show.html
index 79da041..8ff1186 100644
--- a/templates/protocol-show.html
+++ b/templates/protocol-show.html
@@ -74,7 +74,12 @@
             <ul>
                 {% if protocol.decisions|length > 0 %}
                     {% for decision in protocol.decisions %}
-                        <li>{{decision.content}}</li>
+                        <li>
+                            {{decision.content}}
+                            {% if config.PRINTING_ACTIVE and has_private_view_right and decision.document is not none %}
+                                <a href="{{url_for("print_decision", document_id=decision.document.id)}}">Drucken</a>
+                            {% endif %}
+                        </li>
                     {% endfor %}
                 {% else %}
                     <li>Keine Beschlüsse</li>
diff --git a/views/tables.py b/views/tables.py
index f7ab1f9..288d1b9 100644
--- a/views/tables.py
+++ b/views/tables.py
@@ -209,7 +209,7 @@ class ErrorsTable(Table):
             Table.link(url_for("show_error", error_id=error.id), error.name),
             datetime_filter(error.datetime),
             error.get_short_description(),
-            Table.link(url_for("delete_error", error_id=error.id), "Löschen", confirm="Bist du dir sicher, dass du den Fehler löschen möchtest?")
+            Table.link(url_for("delete_error", error_id=error.id, next=request.path), "Löschen", confirm="Bist du dir sicher, dass du den Fehler löschen möchtest?")
         ]
 
 class ErrorTable(SingleValueTable):
@@ -287,12 +287,18 @@ class DecisionsTable(Table):
         super().__init__("Beschlüsse", decisions)
 
     def headers(self):
-        return ["Sitzung", "Beschluss"]
+        return ["Sitzung", "Beschluss", ""]
 
     def row(self, decision):
+        user = current_user()
         return [
             Table.link(url_for("show_protocol", protocol_id=decision.protocol.id), decision.protocol.get_identifier()),
-            decision.content
+            decision.content,
+            Table.link(url_for("print_decision", document_id=decision.document.id), "Drucken")
+                if config.PRINTING_ACTIVE
+                and decision.protocol.protocoltype.has_modify_right(user)
+                and decision.document is not None 
+                else ""
         ]
 
 class DocumentsTable(Table):
-- 
GitLab