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