diff --git a/migrations/versions/0e5220a9f169_.py b/migrations/versions/0e5220a9f169_.py new file mode 100644 index 0000000000000000000000000000000000000000..027be458abc7cfc6424dad5bfa31bea87ffbbf9b --- /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 e91c3f4531e35a05909654f60e9ff4f34d794d21..b041a2f0bf1a61f06345ae5dfe00530ca8a76c4b 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 ec8dd56539ab33af557e7ed4cc752de4722ab4f7..069aa807b4a5c7fa9c8be3a1e27bf255762fc8ea 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 2caa76e28cd24903f903aff17c11ee56e22f6d7c..da4f0c2b121654fc760e4f2c14e5e0cab8166d21 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 5ea7a53cb7adf66e9a084e857b6e727c87431f30..4bfdca1fc4ab92b2555f63b3724248825dbed041 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 0000000000000000000000000000000000000000..8cc7a93f2e205041ab99b7392e7f64c730f80aca --- /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 79da04135f1aa22c2a5a832d5fbf3bd26d56d3f1..8ff11867f3a0ad3db1f1732ffc72fd8ec2002a03 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 f7ab1f9ae83e572e04f471b4511cb38bb5875bda..288d1b9040e8aee81909c6d33706dddb1e1ffa74 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):