diff --git a/config.py.example b/config.py.example new file mode 100644 index 0000000000000000000000000000000000000000..4d8d389a2785d2541fa4b2c5f41c524d65af04d7 --- /dev/null +++ b/config.py.example @@ -0,0 +1,18 @@ +SQLALCHEMY_DATABASE_URI = "postgresql://proto3:@/proto3" +SQLALCHEMY_TRACK_MODIFICATIONS = False +SECRET_KEY = "abc" +DEBUG = False +MAIL_ACTIVE = True +MAIL_FROM = "protokolle@example.com" +MAIL_HOST = "mail.example.com:465" +MAIL_USER = "user" +MAIL_PASSWORD = "password" +MAIL_PREFIX = "protokolle" +CELERY_BROKER_URL = "redis://localhost:6379/0" +CELERY_TASK_SERIALIZER = "pickle" +CELERY_ACCEPT_CONTENT = ["pickle"] +URL_ROOT = "protokolle.example.com" +URL_PROTO = "https" +URL_PATH = "/" +URL_PARAMS = "" +ERROR_CONTEXT_LINES = 3 diff --git a/models/database.py b/models/database.py new file mode 100644 index 0000000000000000000000000000000000000000..9479ca042f4b306e8cf9cdd1c9b569c1961bb11b --- /dev/null +++ b/models/database.py @@ -0,0 +1,219 @@ +from flask import render_template, send_file, url_for, redirect, flash, request + +from datetime import datetime, date, timedelta +import time +import math + +from shared import db +from utils import random_string, url_manager + +#from models.tables import TexResponsiblesTable, TexSupportersTable + +from sqlalchemy.orm import relationship, backref + +class ProtocolType(db.Model): + __tablename__ = "protocoltypes" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String, unique=True) + short_name = db.Column(db.String, unique=True) + organization = db.Column(db.String) + is_public = db.Column(db.Boolean) + private_group = db.Column(db.String) + public_group = db.Column(db.String) + private_mail = db.Column(db.String) + public_mail = db.Column(db.String) + + protocols = relationship("Protocol", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="Protocol.id") + default_tops = relationship("DefaultTOP", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="DefaultTOP.number") + reminders = relationship("MeetingReminder", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="MeetingReminder.time_before") + + def __init__(self, name, short_name, organization, + is_public, private_group, public_group, private_mail, public_mail): + self.name = name + self.short_name = short_name + self.organization = organization + self.is_public = is_public + self.private_group = private_group + self.public_group = public_group + self.private_mail = private_mail + self.public_mail = public_mail + + def __repr__(self): + return "<ProtocolType(id={}, short_name={}, name={}, organization={})>".format( + self.id, self.short_name, self.name, self.organization) + +class Protocol(db.Model): + __tablename__ = "protocols" + id = db.Column(db.Integer, primary_key=True) + protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id")) + source = db.Column(db.String, nullable=True) + date = db.Column(db.Date) + start_time = db.Column(db.Time) + end_time = db.Column(db.Time) + author = db.Column(db.String) + participants = db.Column(db.String) + location = db.Column(db.String) + + tops = relationship("TOP", backref=backref("protocol"), cascade="all, delete-orphan", order_by="TOP.number") + decisions = relationship("Decision", backref=backref("protocol"), cascade="all, delete-orphan", order_by="Decision.id") + 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") + + def __init__(self, protocoltype_id, date, source=None, start_time=None, end_time=None, author=None, participants=None, location=None): + self.protocoltype_id = protocoltype_id + self.date = date + self.source = source + self.start_time = start_time + self.end_time = end_time + self.author = author + self.participants = participants + self.location = location + + def __repr__(self): + return "<Protocol(id={}, protocoltype_id={})>".format( + self.id, self.protocoltype_id) + + def create_error(self, action, name, description): + now = datetime.now() + return Error(self.id, action, name, now, description) + + def fill_from_remarks(self, remarks): + self.date = datetime.strptime(remarks["Datum"].value, "%d.%m.%Y") + self.start_time = time.strptime(remarks["Beginn"].value, "%H:%M") + self.end_time = time.strptime(remarks["Ende"].value, "%H:%M") + self.author = remarks["Autor"].value + self.participants = remarks["Anwesende"].value + self.location = remarks["Ort"].value + + +class DefaultTOP(db.Model): + __tablename__ = "defaulttops" + id = db.Column(db.Integer, primary_key=True) + protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id")) + name = db.Column(db.String) + number = db.Column(db.Integer) + + def __init__(self, protocoltype_id, name, number): + self.protocoltype_id = protocoltype_id + self.name = name + self.number = number + + def __repr__(self): + return "<DefaultTOP(id={}, protocoltype_id={}, name={}, number={})>".format( + self.id, self.protocoltype_id, self.name, self.number) + + def is_at_end(self): + return self.number < 0 + +class TOP(db.Model): + __tablename__ = "tops" + id = db.Column(db.Integer, primary_key=True) + protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id")) + name = db.Column(db.String) + number = db.Column(db.String) + planned = db.Column(db.Boolean) + + def __init__(self, protocol_id, name, number, planned): + self.protocol_id = protocol_id + self.name = name + self.number = number + self.planned = planned + + def __repr__(self): + return "<TOP(id={}, protocol_id={}, name={}, number={}, planned={})>".format( + self.id, self.protocol_id, self.name, self.number, self.planned) + +class Document(db.Model): + __tablename__ = "documents" + id = db.Column(db.Integer, primary_key=True) + protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id")) + name = db.Column(db.String) + filename = db.Column(db.String, unique=True) + is_compiled = db.Column(db.Boolean) + + def __init__(self, protocol_id, name, filename, is_compiled): + self.protocol_id = protocol_id + self.name = name + self.filename = filename + self.is_compiled = is_compiled + + def __repr__(self): + return "<Document(id={}, protocol_id={}, name={}, filename={}, is_compiled={})>".format( + self.id, self.protocol_id, self.name, self.filename, self.is_compiled) + +class Todo(db.Model): + __tablename__ = "todos" + id = db.Column(db.Integer, primary_key=True) + who = db.Column(db.String) + description = db.Column(db.String) + tags = db.Column(db.String) + done = db.Column(db.Boolean) + + protocols = relationship("Protocol", secondary="todoprotocolassociations", backref="todos") + + def __init__(self, who, description, tags, done): + self.who = who + self.description = description + self.tags = tags + self.done = done + + def __repr__(self): + return "<Todo(id={}, who={}, description={}, tags={}, done={})>".format( + self.id, self.who, self.description, self.tags, self.done) + +class TodoProtocolAssociation(db.Model): + __tablename__ = "todoprotocolassociations" + todo_id = db.Column(db.Integer, db.ForeignKey("todos.id"), primary_key=True) + protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id"), primary_key=True) + +class Decision(db.Model): + __tablename__ = "decisions" + id = db.Column(db.Integer, primary_key=True) + protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id")) + content = db.Column(db.String) + + def __init__(self, protocol_id, content): + self.protocol_id = protocol_id + self.content = content + + def __repr__(self): + return "<Decision(id={}, protocol_id={}, content='{}')>".format( + self.id, self.protocol_id, self.content) + +class MeetingReminder(db.Model): + __tablename__ = "meetingreminders" + id = db.Column(db.Integer, primary_key=True) + protocoltype_id = db.Column(db.Integer, db.ForeignKey("protocoltypes.id")) + time_before = db.Column(db.Interval) + send_public = db.Column(db.Boolean) + send_private = db.Column(db.Boolean) + + def __init__(self, protocoltype_id, time_before, send_public, send_private): + self.protocoltype_id = protocoltype_id + self.time_before = time_before + self.send_public = send_public + self.send_private = send_private + + def __repr__(self): + return "<MeetingReminder(id={}, protocoltype_id={}, time_before={}, send_public={}, send_private={})>".format( + self.id, self.protocoltype_id, self.time_before, self.send_public, self.send_private) + +class Error(db.Model): + __tablename__ = "errors" + id = db.Column(db.Integer, primary_key=True) + protocol_id = db.Column(db.Integer, db.ForeignKey("protocols.id")) + action = db.Column(db.String) + name = db.Column(db.String) + datetime = db.Column(db.DateTime) + description = db.Column(db.String) + + def __init__(self, protocol_id, action, name, datetime, description): + self.protocol_id = protocol_id + self.action = action + self.name = name + self.datetime = datetime + self.description = description + + def __repr__(self): + return "<Error(id={}, protocol_id={}, action={}, name={}, datetime={})>".format( + self.id, self.protocol_id, self.action, self.name, self.datetime) diff --git a/parser.py b/parser.py index e450c5e6495718bfb059b44b2beeb05ab23cd5c7..26e2a13ddb44991662e04566d3bb77ea67860b87 100644 --- a/parser.py +++ b/parser.py @@ -100,6 +100,10 @@ class Content(Element): for child in self.children: child.dump(level + 1) + def get_tags(self, tags): + tags.extend([child for child in self.children if isinstance(child, Tag)]) + return tags + @staticmethod def parse(match, current, linenumber=None): linenumber = Element.parse_inner(match, current, linenumber) @@ -254,6 +258,13 @@ class Fork(Element): + "\n".join(map(lambda e: r"\item {}".format(e.render()), self.children)) + "\n" + r"\end{itemize}" + "\n") + def get_tags(self, tags=None): + if tags is None: + tags = [] + for child in self.children: + child.get_tags(tags) + return tags + def is_anonymous(self): return self.environment == None diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ea72be555d893bfa30ec83484869df860093bf73 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +alembic +blinker +Flask +Flask-Migrate +Flask-Script +Flask-SQLAlchemy +Flask-WTF +Flask-SocketIO +Jinja2 +Mako +MarkupSafe +psycopg2 +python-editor +SQLAlchemy +Werkzeug +wheel +WTForms +celery[redis] +regex +watchdog diff --git a/server.py b/server.py new file mode 100755 index 0000000000000000000000000000000000000000..54e202d183db1b8ef4b2d6b57ac4bdc0f6544c24 --- /dev/null +++ b/server.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import locale +locale.setlocale(locale.LC_TIME, "de_DE.utf8") + +from flask import Flask, g, current_app, request, session, flash, redirect, url_for, abort, render_template, Response +from flask_script import Manager, prompt +from flask_migrate import Migrate, MigrateCommand +#from flask_socketio import SocketIO +from celery import Celery + +import config +from shared import db, date_filter, datetime_filter +from utils import is_past, mail_manager, url_manager + +app = Flask(__name__) +app.config.from_object(config) +db.init_app(app) +migrate = Migrate(app, db) +manager = Manager(app) +manager.add_command("db", MigrateCommand) + +def make_celery(app, config): + celery = Celery(app.import_name, broker=config.CELERY_BROKER_URL) + celery.conf.update(app.config) + return celery +celery = make_celery(app, config) + +#def make_socketio(app, config): +# socketio = SocketIO(app) +# return socketio +#socketio = make_socketio(app, config) + +app.jinja_env.trim_blocks = True +app.jinja_env.lstrip_blocks = True +app.jinja_env.filters["datify"] = date_filter +app.jinja_env.filters["datetimify"] = datetime_filter +app.jinja_env.filters["url_complete"] = url_manager.complete + +import tasks + +# blueprints here + +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/imprint/") +def imprint(): + return render_template("imprint.html") + +@app.route("/contact/") +def contact(): + return render_template("contact.html") + +@app.route("/privacy/") +def privacy(): + return render_template("privacy.html") + +if __name__ == "__main__": + manager.run() diff --git a/shared.py b/shared.py new file mode 100644 index 0000000000000000000000000000000000000000..d3d79364de14a3a8e18fa8d89aa672be008df95e --- /dev/null +++ b/shared.py @@ -0,0 +1,73 @@ +from flask_sqlalchemy import SQLAlchemy + +import re + +import config + +db = SQLAlchemy() + +# the following code is written by Lars Beckers and not to be published without permission +latex_chars = [ + ("\\", "\\backslash"), # this needs to be first + ("$", "\$"), + ('%', '\\%'), + ('&', '\\&'), + ('#', '\\#'), + ('_', '\\_'), + ('{', '\\{'), + ('}', '\\}'), + #('[', '\\['), + #(']', '\\]'), + #('"', '"\''), + ('~', '$\\sim{}$'), + ('^', '\\textasciicircum{}'), + ('Ë„', '\\textasciicircum{}'), + ('`', '{}`'), + ('-->', '$\longrightarrow$'), + ('->', '$\rightarrow$'), + ('==>', '$\Longrightarrow$'), + ('=>', '$\Rightarrow$'), + ('>=', '$\geq$'), + ('=<', '$\leq$'), + ('<', '$<$'), + ('>', '$>$'), + ('\\backslashin', '$\\in$'), + ('\\backslash', '$\\backslash$') # this needs to be last +] + +def escape_tex(text): + out = text + for old, new in latex_chars: + out = out.replace(old, new) + # beware, the following is carefully crafted code + res = '' + k, l = (0, -1) + while k >= 0: + k = out.find('"', l+1) + if k >= 0: + res += out[l+1:k] + l = out.find('"', k+1) + if l >= 0: + res += '\\enquote{' + out[k+1:l] + '}' + else: + res += '"\'' + out[k+1:] + k = l + else: + res += out[l+1:] + # yes, this is not quite escaping latex chars, but anyway... + res = re.sub('([a-z])\(', '\\1 (', res) + res = re.sub('\)([a-z])', ') \\1', res) + #logging.debug('escape latex ({0}/{1}): {2} --> {3}'.format(len(text), len(res), text.split('\n')[0], res.split('\n')[0])) + return res + +def unhyphen(text): + return " ".join([r"\mbox{" + word + "}" for word in text.split(" ")]) + +def date_filter(date): + return date.strftime("%d. %B %Y") +def datetime_filter(date): + return date.strftime("%d. %B %Y, %H:%M") +def date_filter_long(date): + return date.strftime("%A, %d.%m.%Y, Kalenderwoche %W") +def time_filter(time): + return time.strftime("%H:%m") diff --git a/start_celery.sh b/start_celery.sh new file mode 100755 index 0000000000000000000000000000000000000000..397bab8183fb1252615b6c0a965862f24bcdbfe1 --- /dev/null +++ b/start_celery.sh @@ -0,0 +1,2 @@ +#!/bin/bash +celery -A server.celery worker --loglevel=debug --concurrency=4 diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000000000000000000000000000000000000..c498188af7871246c813cf82381a1fc18e61c337 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,186 @@ +body { + font-family: sans-serif; +} + +.header { + border-bottom: 1px solid black; + padding: 4px; + font-size: 16pt; + margin-bottom: 1pc; +} + +.header > p { + display: inline; +} + +.header-title { + font-weight: bold; +} + +.header-link { + +} + +.footer { + border-top: 1px solid black; + padding: 4px; + font-size: 10pt; + margin-top: 1pc; + width: 100%; +} + +.footer > p { + display: inline; +} + +.footer-text { +} + +.footer-link { +} + +.main { + max-width: 940px; + margin: 0px auto; +} + +.main > p { + margin: 0px; + padding: 0.5ex; + display: inline-block; +} + +.title { + font-size: 16pt; +} + +.form { + background-color: #eee; + padding: 1ex; + border: 1px dashed gray; +} + +.form-title { + font-size: 16pt; + margin-bottom: 3pt; + border-bottom: 1px solid black; +} + +.form-field { + font-size: 12pt; + margin: 2pt; +} + +.form-label { + font-size: 12pt; + display: inline-block; + min-width: 100px; +} + +.form-textareafield > textarea { + width: 50%; + min-width: 350px; + max-width: 100%; + height: 150px; +} + +.form-errors { + font-size: 10pt; + color: darkred; +} + +.form-errors-entry { + margin: 0px; +} + +.section { + margin-top: 1ex; + margin-bottom: 1ex; + background-color: #eee; + padding: 1ex; + border: 1px dashed gray; +} + +.section > p { + display: inline; +} + +.section-title { + font-weight: bold; + font-size: 14pt; +} + +.table { + border-spacing: 0px; + border-collapse: collapse; + text-align: center; +} + +.table-header { + font-size: 13pt; +} + +.table-body > tr > td { + border-right: 1px solid gray; + border-left: 1px solid gray; + padding-left: 5px; + padding-right: 5px; +} + +.table-body > tr:first-child { + border-top: 1px solid gray; +} + +.table-body > tr:last-child { + border-bottom: 1px solid gray; +} + +.table-body > tr:nth-child(odd) { + background-color: #fefefe; +} + +.table-body > tr:nth-child(even) { + background-color: #f0f0f0; +} + +.alert { + font-weight: bold; + text-align: center; +} + +.alert-error { + color: #800000; +} + +.alert-success { + color: #008000; +} + +.alert-warning { + color: #808000; +} + +.mail-content { + white-space: pre; +} + +.imprint-header { + font-weight: bold; +} + +.imprint > div { + +} + +.privacy-title { + font-weight: bold; + font-size: 14pt; +} + +.privacy-content { + +} + +.pad-content { + width: 100%; +} diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..adee236f1cc921815488329beaf3e4a9b3297254 --- /dev/null +++ b/tasks.py @@ -0,0 +1,215 @@ +from flask import render_template + +import os +import subprocess +import shutil + +from models.database import Document, Protocol, Error, Todo, Decision +from server import celery, app +from shared import db, escape_tex, unhyphen, date_filter, datetime_filter, date_filter_long, time_filter +from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs +from parser import parse, ParserException, Element, Content, Text, Tag, Remark, Fork + +import config + +texenv = app.create_jinja_environment() +texenv.block_start_string = r"\ENV{" +texenv.block_end_string = r"}" +texenv.variable_start_string = r"\VAR{" +texenv.variable_end_string = r"}" +texenv.comment_start_string = r"\COMMENT{" +texenv.comment_end_string = r"}" +texenv.filters["escape_tex"] = escape_tex +texenv.filters["unhyphen"] = unhyphen +texenv.trim_blocks = True +texenv.lstrip_blocks = True +texenv.filters["url_complete"] = url_manager.complete +texenv.filters["datify"] = date_filter +texenv.filters["datify_long"] = date_filter_long +texenv.filters["datetimify"] = datetime_filter +texenv.filters["timify"] = time_filter + +mailenv = app.create_jinja_environment() +mailenv.trim_blocks = True +mailenv.lstrip_blocks = True +mailenv.filters["url_complete"] = url_manager.complete +mailenv.filters["datify"] = date_filter +mailenv.filters["datetimify"] = datetime_filter + +ID_FIELD_BEGINNING = "id " + +def parse_protocol(protocol, **kwargs): + parse_protocol_async.delay(protocol.id, encode_kwargs(kwargs)) + +@celery.task +def parse_protocol_async(protocol_id, encoded_kwargs): + with app.app_context(): + with app.test_request_context("/"): + kwargs = decode_kwargs(encoded_kwargs) + protocol = Protocol.query.filter_by(id=protocol_id).first() + if protocol is None: + raise Exception("No protocol given. Aborting parsing.") + if protocol.source is None: + error = protocol.create_error("Parsing", "Protocol source is None", "") + db.session.add(error) + db.session.commit() + return + tree = None + try: + tree = parse(protocol.source) + except ParserException as exc: + context = "" + if exc.linenumber is not None: + source_lines = source.splitlines() + start_index = max(0, exc.linenumber - config.ERROR_CONTEXT_LINES) + end_index = min(len(source_lines) - 1, exc.linenumber + config.ERROR_CONTEXT_LINES) + context = "\n".join(source_lines[start_index:end_index]) + error = protocol.create_error("Parsing", str(exc), context) + db.session.add(error) + db.session.commit() + return + remarks = {element.name: element for element in tree.children is isinstance(element, Remark)} + required_fields = ["Datum", "Anwesende", "Beginn", "Ende", "Autor", "Ort"] + missing_fields = [field for field in required_files if field not in remarks] + if len(missing_fields) > 0: + error = protocol.create_error("Parsing", "Missing fields", ", ".join(missing_fields)) + db.session.add(error) + db.session.commit() + return + try: + protocol.fill_from_remarks(remarks) + except ValueError: + error = protocol.create_error( + "Parsing", "Invalid fields", + "Date or time fields are not '%d.%m.%Y' respectively '%H:%M', " + "but rather {}".format( + ", ".join([remarks["Datum"], remarks["Beginn"], remarks["Ende"]]))) + db.session.add(error) + db.session.commit() + return + old_todos = list(protocol.todos) + for todo in old_todos + protocol.todos.remove(todo) + db.session.commit() + tags = tree.get_tags() + todo_tags = [tag for tag in tags if tag.name == "todo"] + for todo_tag in todo_tags: + if len(todo_tag.values) < 2: + error = protocol.create_error("Parsing", "Invalid todo-tag", + "The todo tag in line {} needs at least " + "information on who and what, " + "but has less than that.".format(todo_tag.linenumber)) + db.session.add(error) + db.session.commit() + return + who = todo_tag.values[0] + what = todo_tag.values[1] + todo = None + for other_field in todo_tag.values[2:]: + if other_field.startswith(ID_FIELD_BEGINNING): + field_id = 0 + try: + field_id = int(other_field[len(ID_FIELD_BEGINNING):]) + except ValueError: + error = protocol.create_error("Parsing", "Non-numerical todo ID", + "The todo in line {} has a nonnumerical ID, but needs " + "something like \"id 1234\"".format(todo_tag.linenumber)) + db.session.add(error) + db.session.commit() + return + todo = Todo.query.filter_by(id=field_id).first() + if todo is None: + todo = Todo(who=who, description=what, tags="", done=False) + db.session.add(todo) + todo.protocols.append(protocol) + todo_tags_internal = todo.tags.split(";") + for other_field in todo_tag.values[2:]: + if other_field.startswith(ID_FIELD_BEGINNING): + continue + elif other_field == "done": + todo.done = True + elif other_field not in todo_tags_internal: + todo_tags_internal.append(other_field) + todo.tags = ";".join(todo_tags_internal) + db.session.commit() + old_decisions = list(protocol.decisions) + for decision in old_decisions + protocol.decisions.remove(decision) + db.session.commit() + decision_tags = [tag for tag in tags if tag.name == "beschluss"] + for decision_tag in decision_tags: + if len(decision_tag.values) == 0: + error = protocol.create_error("Parsing", "Empty decision found.", + "The decision in line {} is empty.".format(decision_tag.linenumber)) + db.session.add(error) + db.session.commit() + return + decision = Decision(protocol_id=protocol.id, content=decision_tag.values[0]) + db.session.add(decision) + db.session.commit() + + + + + +@celery.task +def compile_async(name, document_id): + is_error = False + try: + current = os.getcwd() + os.chdir("latex") + os.makedirs("bin", exist_ok=True) + command = [ + "/usr/bin/xelatex", + "-halt-on-error", + "-file-line-error", + "-output-directory", "bin", + "{}.tex".format(name) + ] + 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) + os.makedirs("documents/pdf", exist_ok=True) + shutil.copy("latex/bin/{}.pdf".format(name), "documents/pdf/") + for typ in ["pdf", "log", "aux", "out"]: + silent_remove("latex/bin/{}.{}".format(name, typ)) + silent_remove("latex/{}.tex".format(name)) + except subprocess.SubprocessError: + is_error = True + with app.app_context(): + document = Document.query.filter_by(id=document_id).first() + if document is not None: + document.ready = False + document.error = True + db.session.commit() + finally: + # TODO: activate deleting files in error case too + #for typ in ["pdf", "log", "aux", "out"]: + # silent_remove("latex/bin/{}.{}".format(name, typ)) + #silent_remove("latex/{}.tex".format(name)) + os.chdir(current) + if not is_error: + with app.app_context(): + document = Document.query.filter_by(id=document_id).first() + if document is not None: + document.ready = True + db.session.commit() + +def send_mail(mail): + send_mail_async.delay(mail.id) + +@celery.task +def send_mail_async(mail_id): + with app.app_context(): + mail = Mail.query.filter_by(id=mail_id).first() + if mail is None: + return False + mail.ready = False + mail.error = False + db.session.commit() + result = mail_manager.send(mail.to_addr, mail.subject, mail.content) + mail.ready = True + mail.error = not result + db.session.commit() + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..edf0aa3a6d7dcfd4bb283866974baea0496d1b4d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,6 @@ +{% extends "layout.html" %} +{% block title %}Hauptseite{% endblock %} + +{% block content %} + Dies ist die Startseite +{% endblock %} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000000000000000000000000000000000000..ff0ec5a2e0530ea282f1fa2011d727fb6c8f5f29 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,41 @@ +<!doctype html> +<html> +<head> + {% block head %} + <meta charset="utf-8" /> + <meta name="description" content="Protokollsystem" /> + <link rel="stylesheet" href="{{ url_for("static", filename="css/style.css") }}" /> + {% block additional_js %} + {% endblock %} + <title>{% block title %}Unbenannte Seite{% endblock %} - Protokollsystem</title> + {% endblock %} +</head> +<body> +<div class="header"> + <p class="header-title"><a href="{{ url_for("index") }}">Protokollsystem</a></p> + {% block additional_links %} + {% endblock %} +</div> +<div class="main"> +{% with messages = get_flashed_messages(with_categories=true) %} +{% if messages %} +{% for category, message in messages %} + <div class="section alert {{ category }}"> + {{ message }} + </div> +{% endfor %} +{% endif %} +{% endwith %} +{% block content %} +Diese Seite ist leer. +{% endblock %} +{% block footer %} +<div class="footer"> + <p class="footer-text">System zur Erstellung und Verwaltung von Sitzungsprotokollen, entwickelt von Robin Sonnabend</p> + <p class="footer-link"><a href="{{ url_for("imprint") }}">Impressum</a></p> + <p class="footer-link"><a href="{{ url_for("contact") }}">Kontakt</a></p> + <p class="footer-link"><a href="{{ url_for("privacy") }}">Datenschutzerklärung</a></p> +</div> +{% endblock %} +</div> +</body> diff --git a/templates/layout_simple.html b/templates/layout_simple.html new file mode 100644 index 0000000000000000000000000000000000000000..31b0c14ded2937201b3c0990a6f1c9b48bd76fcf --- /dev/null +++ b/templates/layout_simple.html @@ -0,0 +1,28 @@ +<!doctype html> +<html> +<head> + {% block head %} + <meta charset="utf-8" /> + <meta name="description" content="Protokollsystem" /> + <link rel="stylesheet" href="{{ url_for("static", filename="css/style.css") }}" /> + {% block additional_js %} + {% endblock %} + <title>{% block title %}Unbenannte Seite{% endblock %} - Protokollsystem</title> + {% endblock %} +</head> +<body> +{# +{% with messages = get_flashed_messages(with_categories=true) %} +{% if messages %} +{% for category, message in messages %} + <div class="section alert {{ category }}"> + {{ message }} + </div> +{% endfor %} +{% endif %} +{% endwith %} +#} +{% block content %} +Diese Seite ist leer. +{% endblock %} +</body> diff --git a/templates/pad_index.html b/templates/pad_index.html new file mode 100644 index 0000000000000000000000000000000000000000..17c580f13a8a2210c40737da3e9fea8a1f817f3d --- /dev/null +++ b/templates/pad_index.html @@ -0,0 +1,12 @@ +{% extends "layout_simple.html" %} +{% block title %}Pad - Padname?{% endblock %} +{% block additional_js %} +<script src="{{ url_for("static", filename="js/socket_io.js") }}"></script> +<script src="{{ url_for("static", filename="js/pad.js") }}"></script> +{% endblock %} + +{% block content %} + <div id="padarea" class="pad-content"> + + </div> +{% endblock %} diff --git a/templates/protokoll.tex b/templates/protokoll.tex new file mode 100644 index 0000000000000000000000000000000000000000..5616e1b832205d21bda09d1070dfac2bce627e7e --- /dev/null +++ b/templates/protokoll.tex @@ -0,0 +1,52 @@ +\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{15.5cm}} +{\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|join(", ")|escape_tex}\\ +\end{tabular} +\normalsize + +\section*{Beschlüsse} +\begin{itemize} +\ENV{if protocol.decisions|length > 0} + \ENV{for decision in protocol.decisions} + \item \VAR{decisions.content|escape_tex} + \ENV{endfor} +\ENV{else} + \item Keine Beschlüsse +\ENV{endif} +\end{itemize} + +Beginn der Sitzung: \VAR{protocol.start_time|timify} + +\ENV{for top in tree.children} + \TOP{\VAR{top.name}} % here we probably have information doubly + \VAR{top.render()} +\ENV{endfor} + +Ende der Sitzung: \VAR{protocol.end_time|timify} + +\end{document} diff --git a/utils.py b/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b1cd9f733acd060f659a27390f3059b5a70f23ec --- /dev/null +++ b/utils.py @@ -0,0 +1,92 @@ +from flask import render_template + +import random +import string +import regex +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from datetime import datetime, date, timedelta + +import config + +def random_string(length): + return "".join((random.choice(string.ascii_letters) for i in range(length))) + +def is_past(some_date): + return (datetime.now() - some_date).total_seconds() > 0 + +def encode_kwargs(kwargs): + encoded_kwargs = {} + for key in kwargs: + value = kwargs[key] + if hasattr(value, "id"): + encoded_kwargs[key] = (type(value), value.id, True) + else: + encoded_kwargs[key] = (type(value), value, False) + return encoded_kwargs + +def decode_kwargs(encoded_kwargs): + kwargs = {} + for name in encoded_kwargs: + kind, id, from_db = encoded_kwargs[name] + if from_db: + kwargs[name] = kind.query.filter_by(id=id).first() + else: + kwargs[name] = id + return kwargs + + +class UrlManager: + def __init__(self, config): + self.pattern = regex.compile(r"(?:(?<proto>https?):\/\/)?(?<hostname>[[:alnum:]_.]+(?:\:[[:digit:]]+)?)?(?<path>(?:\/[[:alnum:]_#]*)+)?(?:\?(?<params>.*))?") + self.base = "{}://{}{}{}" + self.proto = getattr(config, "URL_PROTO", "https") + self.root = getattr(config, "URL_ROOT", "example.com") + self.path = getattr(config, "URL_PATH", "/") + self.params = getattr(config, "URL_PARAMS", "") + + def complete(self, url): + match = self.pattern.match(url) + if match is None: + return None + proto = match.group("proto") or self.proto + root = match.group("hostname") or self.root + path = match.group("path") or self.path + params = match.group("params") or self.params + return self.base.format(proto, root, path, "?" + params if len(params) > 0 else "") + +url_manager = UrlManager(config) + +class MailManager: + def __init__(self, config): + self.active = getattr(config, "MAIL_ACTIVE", False) + self.from_addr = getattr(config, "MAIL_FROM", "") + self.hostname = getattr(config, "MAIL_HOST", "") + self.username = getattr(config, "MAIL_USER", "") + self.password = getattr(config, "MAIL_PASSWORD", "") + self.prefix = getattr(config, "MAIL_PREFIX", "") + + def send(self, to_addr, subject, content): + if (not self.active + or not self.hostname + or not self.username + or not self.password + or not self.from_addr): + return True + try: + msg = MIMEMultipart("alternative") + msg["From"] = self.from_addr + msg["To"] = to_addr + msg["Subject"] = "[{}] {}".format(self.prefix, subject) if self.prefix else subject + msg.attach(MIMEText(content, _charset="utf-8")) + server = smtplib.SMTP_SSL(self.hostname) + server.login(self.username, self.password) + server.sendmail(self.from_addr, to_addr, msg.as_string()) + server.quit() + except Exception as e: + print(e) + return False + return True + +mail_manager = MailManager(config)