From 2cdf562ee6c7dba54783d278779dc381011c0b68 Mon Sep 17 00:00:00 2001 From: Robin Sonnabend <robin@fsmpi.rwth-aachen.de> Date: Wed, 1 Mar 2017 01:48:29 +0100 Subject: [PATCH] More legacy, can now import every old protocol --- calendarpush.py | 36 ++- config.py.example | 8 +- legacy.py | 67 +++++- models/database.py | 44 +++- parser.py | 86 ++++--- server.py | 49 ++-- start_celery.sh | 2 +- tasks.py | 420 +++++++++++++++++++---------------- templates/decision.tex | 10 +- templates/index.html | 2 +- templates/protocol-mail.txt | 2 +- templates/protocol-show.html | 6 +- templates/protocol.tex | 2 +- templates/protokoll2.cls | 7 + templates/top-new.html | 2 +- todostates.py | 1 + utils.py | 12 + views/forms.py | 34 ++- views/tables.py | 13 +- 19 files changed, 519 insertions(+), 284 deletions(-) diff --git a/calendarpush.py b/calendarpush.py index 260aece..331f82a 100644 --- a/calendarpush.py +++ b/calendarpush.py @@ -3,6 +3,7 @@ import random import quopri from caldav import DAVClient, Principal, Calendar, Event +from caldav.lib.error import PropfindError from vobject.base import ContentLine import config @@ -14,17 +15,33 @@ class Client: def __init__(self, calendar=None, url=None): self.url = url if url is not None else config.CALENDAR_URL self.client = DAVClient(self.url) - self.principal = self.client.principal() + self.principal = None + for _ in range(config.CALENDAR_MAX_REQUESTS): + try: + self.principal = self.client.principal() + break + except PropfindError as exc: + print(exc) + if self.principal is None: + raise CalendarException("Got {} PropfindErrors from the CalDAV server.".format(config.CALENDAR_MAX_REQUESTS)) if calendar is not None: self.calendar = self.get_calendar(calendar) else: self.calendar = calendar def get_calendars(self): - return [ - calendar.name - for calendar in self.principal.calendars() - ] + if not config.CALENDAR_ACTIVE: + return + for _ in range(config.CALENDAR_MAX_REQUESTS): + try: + return [ + calendar.name + for calendar in self.principal.calendars() + ] + except PropfindError as exc: + print(exc) + raise CalendarException("Got {} PropfindErrors from the CalDAV server.".format(config.CALENDAR_MAX_REQUESTS)) + def get_calendar(self, calendar_name): candidates = self.principal.calendars() @@ -34,6 +51,8 @@ class Client: raise CalendarException("No calendar named {}.".format(calendar_name)) def set_event_at(self, begin, name, description): + if not config.CALENDAR_ACTIVE: + return candidates = [ Event.from_raw_event(raw_event) for raw_event in self.calendar.date_search(begin) @@ -126,10 +145,3 @@ def get_timezone_offset(): def encode_quopri(text): return quopri.encodestring(text.encode("utf-8")).replace(b"\n", b"=0D=0A").decode("utf-8") -def main(): - client = Client("Protokolltest") - client.set_event_at(datetime(2017, 2, 27, 19, 0), "FSS", "Tagesordnung\nTOP 1") - -if __name__ == "__main__": - if config.CALENDAR_ACTIVE: - main() diff --git a/config.py.example b/config.py.example index 0f0399a..fd0c4e3 100644 --- a/config.py.example +++ b/config.py.example @@ -85,7 +85,7 @@ ADMIN_MAIL = "admin@example.com" PARSER_LAZY = False # minimum similarity (0-100) todos need to have to be considered equal -FUZZY_MIN_SCORE = 50 +FUZZY_MIN_SCORE = 90 # choose something nice from fc-list # Nimbus Sans looks very much like Computer Modern @@ -122,3 +122,9 @@ DOCUMENTS_PATH = "documents" # keywords indicating private protocol parts PRIVATE_KEYWORDS = ["private", "internal", "privat", "intern"] +LATEX_BULLETPOINTS = [ + r"\textbullet", + r"\normalfont \bfseries \textendash", + r"\textasteriskcentered", + r"\textperiodcentered" +] diff --git a/legacy.py b/legacy.py index 7374836..7cf650e 100644 --- a/legacy.py +++ b/legacy.py @@ -1,10 +1,17 @@ -from models.database import Todo, OldTodo +from datetime import datetime from fuzzywuzzy import fuzz, process +import tempfile +from models.database import Todo, OldTodo, Protocol, ProtocolType from shared import db import config +def log_fuzzy(text): + with tempfile.NamedTemporaryFile(delete=False, mode="w") as tmpfile: + tmpfile.write(text + "\n\n") + print(text) + def lookup_todo_id(old_candidates, new_who, new_description): # Check for perfect matches for candidate in old_candidates: @@ -22,25 +29,62 @@ def lookup_todo_id(old_candidates, new_who, new_description): best_match, best_match_score = process.extractOne( new_description, content_to_number.keys()) if best_match_score >= config.FUZZY_MIN_SCORE: - print("Used fuzzy matching on '{}', got '{}' with score {}.".format( + log_fuzzy("Used fuzzy matching on '{}', got '{}' with score {}.".format( new_description, best_match, best_match_score)) return content_to_number[best_match] else: - print("Best match for '{}' is '{}' with score {}, rejecting.".format( + log_fuzzy("Best match for '{}' is '{}' with score {}, rejecting.".format( new_description, best_match, best_match_score)) return None +INSERT_PROTOCOLTYPE = "INSERT INTO `protocolManager_protocoltype`" +INSERT_PROTOCOL = "INSERT INTO `protocolManager_protocol`" +INSERT_TODO = "INSERT INTO `protocolManager_todo`" + +def import_old_protocols(sql_text): + protocoltype_lines = [] + protocol_lines = [] + for line in sql_text.splitlines(): + if line.startswith(INSERT_PROTOCOLTYPE): + protocoltype_lines.append(line) + elif line.startswith(INSERT_PROTOCOL): + protocol_lines.append(line) + if (len(protocoltype_lines) == 0 + or len(protocol_lines) == 0): + raise ValueError("Necessary lines not found.") + type_id_to_handle = {} + for type_line in protocoltype_lines: + for id, handle, name, mail, protocol_id in _split_insert_line(type_line): + type_id_to_handle[int(id)] = handle.lower() + protocols = [] + for protocol_line in protocol_lines: + for (protocol_id, old_type_id, date, source, textsummary, htmlsummary, + deleted, sent, document_id) in _split_insert_line(protocol_line): + date = datetime.strptime(date, "%Y-%m-%d") + handle = type_id_to_handle[int(old_type_id)] + type = ProtocolType.query.filter(ProtocolType.short_name.ilike(handle)).first() + if type is None: + raise KeyError("No protocoltype for handle '{}'.".format(handle)) + protocol = Protocol(type.id, date, source=source) + db.session.add(protocol) + db.session.commit() + import tasks + protocols.append(protocol) + for protocol in sorted(protocols, key=lambda p: p.date): + print(protocol.date) + tasks.parse_protocol(protocol) + def import_old_todos(sql_text): protocoltype_lines = [] protocol_lines = [] todo_lines = [] for line in sql_text.splitlines(): - if line.startswith("INSERT INTO `protocolManager_protocoltype`"): + if line.startswith(INSERT_PROTOCOLTYPE): protocoltype_lines.append(line) - elif line.startswith("INSERT INTO `protocolManager_protocol`"): + elif line.startswith(INSERT_PROTOCOL): protocol_lines.append(line) - elif line.startswith("INSERT INTO `protocolManager_todo`"): + elif line.startswith(INSERT_TODO): todo_lines.append(line) if (len(protocoltype_lines) == 0 or len(protocol_lines) == 0 @@ -125,7 +169,16 @@ def _split_base_level(text, begin="(", end=")", separator=",", string_terminator escaped = False for char in part: if escaped: - current_field += char + if char == "n": + current_field += "\n" + elif char == "r": + current_field += "\r" + elif char == "t": + current_field += "\t" + else: + if char not in "\"'\\": + print("escaped char: '{}'".format(char)) + current_field += char escaped = False elif in_string: if char == escape: diff --git a/models/database.py b/models/database.py index baa6790..1fe696c 100644 --- a/models/database.py +++ b/models/database.py @@ -43,7 +43,7 @@ class ProtocolType(db.Model): def __init__(self, name, short_name, organization, usual_time, is_public, private_group, public_group, private_mail, public_mail, - use_wiki, wiki_category, wiki_only_public, printer): + use_wiki, wiki_category, wiki_only_public, printer, calendar): self.name = name self.short_name = short_name self.organization = organization @@ -416,6 +416,44 @@ class TodoState(Enum): raise ValueError("Unknown state: '{}'".format(name)) return NAME_TO_STATE[name] + @staticmethod + def from_name_lazy(name): + name = name.strip().lower() + STATE_TO_NAME, NAME_TO_STATE = make_states(TodoState) + for key in NAME_TO_STATE: + if name.startswith(key): + return NAME_TO_STATE[key] + raise ValueError("{} does not start with a state.".format(name)) + + @staticmethod + def from_name_with_date(name, protocol=None): + name = name.strip().lower() + if not " " in name: + raise ValueError("{} does definitely not contain a state and a date".format(name)) + name_part, date_part = name.split(" ", 1) + state = TodoState.from_name(name_part) + date = None + last_exc = None + formats = [("%d.%m.%Y", False)] + if config.PARSER_LAZY: + formats.extend([("%d.%m.", True), ("%d.%m", True)]) + for format, year_missing in formats: + try: + date = datetime.strptime(date_part.strip(), format).date() + if year_missing: + year = datetime.now().year + if protocol is not None: + year = protocol.date.year + date = datetime(year=year, month=date.month, day=date.day).date() + break + except ValueError as exc: + last_exc = exc + continue + if date is None: + raise last_exc + return state, date + + class Todo(db.Model): __tablename__ = "todos" id = db.Column(db.Integer, primary_key=True) @@ -443,9 +481,9 @@ class Todo(db.Model): def is_done(self): if self.state.needs_date(): if self.state == TodoState.after: - return datetime.now().date() >= self.date - elif self.state == TodoState.before: return datetime.now().date() <= self.date + elif self.state == TodoState.before: + return datetime.now().date() >= self.date return self.state.is_done() def get_id(self): diff --git a/parser.py b/parser.py index d2c5bed..c78ee74 100644 --- a/parser.py +++ b/parser.py @@ -156,7 +156,7 @@ class Content(Element): # v2: does not require the semicolon, but the newline #PATTERN = r"\s*(?<content>(?:[^\[\];\r\n]+)?(?:\[[^\]\r\n]+\][^;\[\]\r\n]*)*);?" # v3: does not allow braces in the content - PATTERN = r"\s*(?<content>(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n]+\][^;\[\]\r\n]*)*);?" + PATTERN = r"\s*(?<content>(?:[^\[\];\r\n{}]+)?(?:\[[^\]\r\n{}]+\][^;\[\]\r\n{}]*)*);?" class Text: def __init__(self, text, linenumber, fork): @@ -188,7 +188,10 @@ class Text: raise ParserException("Text is empty!", linenumber) return Text(content, linenumber, current) - PATTERN = r"(?<text>[^\[]+)(?:(?=\[)|$)" + # v1: does not allow any [, as that is part of a tag + # PATTERN = r"(?<text>[^\[]+)(?:(?=\[)|$)" + # v2: does allow one [ at the beginning, which is used if it did not match a tag + PATTERN = r"(?<text>\[?[^\[{}]+)(?:(?=\[)|$)" class Tag: @@ -206,7 +209,7 @@ class Tag: if not show_private: return "" return self.todo.render_latex(current_protocol=protocol) - return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(self.values[0])) + return r"\textbf{{{}:}} {}".format(escape_tex(self.name.capitalize()), escape_tex(";".join(self.values))) elif render_type == RenderType.plaintext: if self.name == "url": return self.values[0] @@ -214,7 +217,7 @@ class Tag: if not show_private: return "" return self.values[0] - return "{}: {}".format(self.name.capitalize(), self.values[0]) + return "{}: {}".format(self.name.capitalize(), ";".join(self.values)) elif render_type == RenderType.wikitext: if self.name == "url": return "[{0} {0}]".format(self.values[0]) @@ -222,7 +225,7 @@ class Tag: if not show_private: return "" return self.todo.render_wikitext(current_protocol=protocol) - return "'''{}:''' {}".format(self.name.capitalize(), self.values[0]) + return "'''{}:''' {}".format(self.name.capitalize(), ";".join(self.values)) else: raise _not_implemented(self, render_type) @@ -240,8 +243,12 @@ class Tag: raise ParserException("Tag is empty!", linenumber) parts = content.split(";") return Tag(parts[0], parts[1:], linenumber, current) + + # v1: matches [text without semicolons] + #PATTERN = r"\[(?<content>(?:[^;\]]*;)*(?:[^;\]]*))\]" + # v2: needs at least two parts separated by a semicolon + PATTERN = r"\[(?<content>(?:[^;\]]*;)+(?:[^;\]]*))\]" - PATTERN = r"\[(?<content>(?:[^;\]]*;)*(?:[^;\]]*))\]" class Empty(Element): def __init__(self, linenumber): @@ -301,8 +308,8 @@ class Remark(Element): PATTERN = r"\s*\#(?<content>[^\n]+)" class Fork(Element): - def __init__(self, environment, name, parent, linenumber, children=None): - self.environment = environment if environment is None or len(environment) > 0 else None + def __init__(self, is_top, name, parent, linenumber, children=None): + self.is_top = is_top self.name = name.strip() if (name is not None and len(name) > 0) else None self.parent = parent self.linenumber = linenumber @@ -311,22 +318,19 @@ class Fork(Element): def dump(self, level=None): if level is None: level = 0 - result_lines = ["{}fork: {}".format(INDENT_LETTER * level, self.name)] + result_lines = ["{}fork: {}'{}'".format(INDENT_LETTER * level, "TOP " if self.is_top else "", self.name)] for child in self.children: result_lines.append(child.dump(level + 1)) return "\n".join(result_lines) def test_private(self, name): + if name is None: + return False stripped_name = name.replace(":", "").strip() return stripped_name in config.PRIVATE_KEYWORDS def render(self, render_type, show_private, level, protocol=None): - name_parts = [] - if self.environment is not None: - name_parts.append(self.environment) - if self.name is not None: - name_parts.append(self.name) - name_line = " ".join(name_parts) + name_line = escape_tex(self.name if self.name is not None else "") if level == 0 and self.name == "Todos" and not show_private: return "" if render_type == RenderType.latex: @@ -341,6 +345,8 @@ class Fork(Element): part = r"\item {}".format(part) content_parts.append(part) content_lines = "\n".join(content_parts) + if len(content_lines.strip()) == 0: + content_lines = "\\item Nichts\n" if level == 0: return "\n".join([begin_line, content_lines, end_line]) elif self.test_private(self.name): @@ -388,7 +394,7 @@ class Fork(Element): return tags def is_anonymous(self): - return self.environment == None + return self.name == None def is_root(self): return self.parent is None @@ -398,6 +404,17 @@ class Fork(Element): return self return self.parent.get_top() + def get_maxdepth(self): + child_depths = [ + child.get_maxdepth() + for child in self.children + if isinstance(child, Fork) + ] + if len(child_depths) > 0: + return max(child_depths) + 1 + else: + return 1 + @staticmethod def create_root(): return Fork(None, None, None, 0) @@ -405,18 +422,13 @@ class Fork(Element): @staticmethod def parse(match, current, linenumber=None): linenumber = Element.parse_inner(match, current, linenumber) - environment = match.group("environment") - name1 = match.group("name1") - name2 = match.group("name2") - name = "" - if name1 is not None: - name = name1 - if name2 is not None: - if len(name) > 0: - name += " {}".format(name2) - else: - name = name2 - element = Fork(environment, name, current, linenumber) + topname = match.group("topname") + name = match.group("name") + is_top = False + if topname is not None: + is_top = True + name = topname + element = Fork(is_top, name, current, linenumber) current = Element.parse_outer(element, current) return current, linenumber @@ -434,7 +446,11 @@ class Fork(Element): # v1: has a problem with old protocols that do not use a lot of semicolons #PATTERN = r"\s*(?<name1>[^{};]+)?{(?<environment>\S+)?\h*(?<name2>[^\n]+)?" # v2: do not allow newlines in name1 or semicolons in name2 - PATTERN = r"\s*(?<name1>[^{};\n]+)?{(?<environment>\S+)?\h*(?<name2>[^;\n]+)?" + #PATTERN = r"\s*(?<name1>[^{};\n]+)?{(?<environment>[^\s{};]+)?\h*(?<name2>[^;{}\n]+)?" + # v3: no environment/name2 for normal lists, only for tops + #PATTERN = r"\s*(?<name>[^{};\n]+)?{(?:TOP\h*(?<topname>[^;{}\n]+))?" + # v4: do allow one newline between name and { + PATTERN = r"\s*(?<name>(?:[^{};\n])+)?\n?\s*{(?:TOP\h*(?<topname>[^;{}\n]+))?" END_PATTERN = r"\s*};?" PATTERNS = OrderedDict([ @@ -446,8 +462,8 @@ PATTERNS = OrderedDict([ ]) TEXT_PATTERNS = OrderedDict([ - (re.compile(Text.PATTERN), Text.parse), - (re.compile(Tag.PATTERN), Tag.parse) + (re.compile(Tag.PATTERN), Tag.parse), + (re.compile(Text.PATTERN), Text.parse) ]) def parse(source): @@ -460,11 +476,15 @@ def parse(source): match = pattern.match(source) if match is not None: source = source[len(match.group()):] - current, linenumber = PATTERNS[pattern](match, current, linenumber) + try: + current, linenumber = PATTERNS[pattern](match, current, linenumber) + except ParserException as exc: + exc.tree = tree + raise exc found = True break if not found: - raise ParserException("No matching syntax element found!", linenumber) + raise ParserException("No matching syntax element found!", linenumber, tree=tree) if current is not tree: raise ParserException("Source ended within fork! (started at line {})".format(current.linenumber), linenumber=current.linenumber, tree=tree) return tree diff --git a/server.py b/server.py index 854dea0..1f8dce4 100755 --- a/server.py +++ b/server.py @@ -24,7 +24,7 @@ from utils import is_past, mail_manager, url_manager, get_first_unused_int, set_ from models.database import ProtocolType, Protocol, DefaultTOP, TOP, Document, Todo, Decision, MeetingReminder, Error, TodoMail, DecisionDocument, TodoState 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 -from legacy import import_old_todos +from legacy import import_old_todos, import_old_protocols app = Flask(__name__) app.config.from_object(config) @@ -81,10 +81,15 @@ app.jinja_env.globals.update(dir=dir) @manager.command def import_legacy(): - """Import the old todos from an sql dump""" + """Import the old todos and protocols from an sql dump""" filename = prompt("SQL-file") + #filename = "legacy.sql" with open(filename, "r") as sqlfile: - import_old_todos(sqlfile.read()) + content = sqlfile.read() + import_old_todos(content) + import_old_protocols(content) + + @app.route("/") def index(): @@ -93,7 +98,7 @@ def index(): protocol for protocol in Protocol.query.all() if protocol.protocoltype.has_public_view_right(user) ] - def _sort_key(protocol): + def _protocol_sort_key(protocol): if protocol.date is not None: return protocol.date return datetime.now().date() @@ -104,7 +109,7 @@ def index(): if not protocol.done and (protocol.date - current_day).days < config.MAX_INDEX_DAYS ], - key=_sort_key + key=_protocol_sort_key ) finished_protocols = sorted( [ @@ -113,7 +118,7 @@ def index(): and (protocol.has_public_view_right(user) or protocol.has_private_view_right(user)) ], - key=_sort_key + key=_protocol_sort_key ) protocol = finished_protocols[0] if len(finished_protocols) > 0 else None todos = None @@ -123,6 +128,10 @@ def index(): if todo.protocoltype.has_private_view_right(user) and not todo.is_done() ] + def _todo_sort_key(todo): + protocol = todo.get_first_protocol() + return protocol.date if protocol.date is not None else datetime.now().date() + todos = sorted(todos, key=_todo_sort_key, reverse=True) todos_table = TodosTable(todos) if todos is not None else None return render_template("index.html", open_protocols=open_protocols, protocol=protocol, todos=todos, todos_table=todos_table) @@ -143,6 +152,7 @@ def list_types(): if (protocoltype.public_group in user.groups or protocoltype.private_group in user.groups or protocoltype.is_public)] + types = sorted(types, key=lambda t: t.short_name) types_table = ProtocolTypesTable(types) return render_template("types-list.html", types=types, types_table=types_table) @@ -160,7 +170,8 @@ def new_type(): form.private_group.data, form.public_group.data, form.private_mail.data, form.public_mail.data, form.use_wiki.data, form.wiki_category.data, - form.wiki_only_public.data, form.printer.data) + form.wiki_only_public.data, form.printer.data, + form.calendar.data) db.session.add(protocoltype) db.session.commit() flash("Der Protokolltyp {} wurde angelegt.".format(protocoltype.name), "alert-success") @@ -204,6 +215,20 @@ def show_type(type_id): reminders_table = MeetingRemindersTable(protocoltype.reminders, protocoltype) return render_template("type-show.html", protocoltype=protocoltype, protocoltype_table=protocoltype_table, default_tops_table=default_tops_table, reminders_table=reminders_table, mail_active=config.MAIL_ACTIVE) +@app.route("/type/delete/<int:type_id>") +@login_required +def delete_type(type_id): + user = current_user() + protocoltype = ProtocolType.query.filter_by(id=type_id).first() + if protocoltype is None or not protocoltype.has_modify_right(user): + flash("Invalider Protokolltyp oder fehlende Zugriffsrechte.", "alert-error") + return redirect(request.args.get("next") or url_for("index")) + name = protocoltype.name + db.session.delete(protocoltype) + db.session.commit() + flash("Der Protokolltype {} wurde gelöscht.".format(name), "alert-success") + return redirect(request.args.get("next") or url_for("list_types")) + @app.route("/type/reminders/new/<int:type_id>", methods=["GET", "POST"]) @login_required def new_reminder(type_id): @@ -436,7 +461,7 @@ def list_protocols(): search_results[protocol] = "<br />\n".join(formatted_lines) protocols = sorted(protocols, key=lambda protocol: protocol.date, reverse=True) page = _get_page() - page_count = int(math.ceil(len(protocols)) / config.PAGE_LENGTH) + page_count = int(math.ceil(len(protocols) / config.PAGE_LENGTH)) if page >= page_count: page = 0 begin_index = page * config.PAGE_LENGTH @@ -637,11 +662,9 @@ def etherpush_protocol(protocol_id): if not config.ETHERPAD_ACTIVE: flash("Die Etherpadfunktion ist nicht aktiviert.", "alert-error") return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol_id)) - if set_etherpad_text(protocol.get_identifier(), protocol.get_template()): - flash("Vorlage von {} in Etherpad hochgeladen.".format(protocol.get_identifier()), "alert-success") - else: - flash("Das Etherpad wurde bereits bearbeitet.", "alert-error") - return redirect(request.args.get("next") or url_for("show_protocol", protocol_id=protocol.id)) + if not protocol.is_done(): + tasks.set_etherpad_content(protocol) + return redirect(request.args.get("next") or protocol.get_etherpad_link()) @app.route("/protocol/update/<int:protocol_id>", methods=["GET", "POST"]) @login_required diff --git a/start_celery.sh b/start_celery.sh index 397bab8..027f65a 100755 --- a/start_celery.sh +++ b/start_celery.sh @@ -1,2 +1,2 @@ #!/bin/bash -celery -A server.celery worker --loglevel=debug --concurrency=4 +celery -A server.celery worker --loglevel=debug --concurrency=1 diff --git a/tasks.py b/tasks.py index bea4291..b8822fe 100644 --- a/tasks.py +++ b/tasks.py @@ -10,7 +10,7 @@ from models.database import Document, Protocol, Error, Todo, Decision, TOP, Defa 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, KNOWN_KEYS -from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs +from utils import mail_manager, url_manager, encode_kwargs, decode_kwargs, add_line_numbers, set_etherpad_text, get_etherpad_text from parser import parse, ParserException, Element, Content, Text, Tag, Remark, Fork, RenderType from wiki import WikiClient, WikiException from calendarpush import Client as CalendarClient, CalendarException @@ -53,215 +53,237 @@ def parse_protocol(protocol, **kwargs): 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.") - old_errors = list(protocol.errors) - for error in old_errors: - protocol.errors.remove(error) - db.session.commit() - 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 = protocol.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]) - if exc.tree is not None: - context += "\n\nParsed syntax tree was:\n" + str(exc.tree.dump()) - 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 if isinstance(element, Remark)} - required_fields = KNOWN_KEYS - if not config.PARSER_LAZY: - missing_fields = [field for field in required_fields 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"].value.strip(), - remarks["Beginn"].value.strip(), - remarks["Ende"].value.strip() - ]))) + 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.") + parse_protocol_async_inner(protocol, encoded_kwargs) + except Exception as exc: + error = protocol.create_error("Parsing", "Exception", str(exc)) db.session.add(error) db.session.commit() - return - except DateNotMatchingException as exc: - error = protocol.create_error("Parsing", "Date not matching", - "This protocol's date should be {}, but the protocol source says {}.".format(date_filter(exc.original_date), date_filter(exc.protocol_date))) - db.session.add(error) - db.session.commit() - return - protocol.delete_orphan_todos() + +def parse_protocol_async_inner(protocol, encoded_kwargs): + old_errors = list(protocol.errors) + for error in old_errors: + protocol.errors.remove(error) + db.session.commit() + 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 = protocol.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]) + if exc.tree is not None: + context += "\n\nParsed syntax tree was:\n" + str(exc.tree.dump()) + 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 if isinstance(element, Remark)} + required_fields = KNOWN_KEYS + if not config.PARSER_LAZY: + missing_fields = [field for field in required_fields 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() - old_todos = list(protocol.todos) - for todo in old_todos: - protocol.todos.remove(todo) + 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"].value.strip(), + remarks["Beginn"].value.strip(), + remarks["Ende"].value.strip() + ]))) + db.session.add(error) + db.session.commit() + return + except DateNotMatchingException as exc: + error = protocol.create_error("Parsing", "Date not matching", + "This protocol's date should be {}, but the protocol source says {}.".format(date_filter(exc.original_date) if exc.original_date is not None else "not present", date_filter(exc.protocol_date) if exc.protocol_date is not None else "not present")) + db.session.add(error) + db.session.commit() + return + # todos + protocol.delete_orphan_todos() + db.session.commit() + 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() - tags = tree.get_tags() - # todos - 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)) + return + who = todo_tag.values[0] + what = todo_tag.values[1] + field_id = None + field_state = None + field_date = None + for other_field in todo_tag.values[2:]: + other_field = other_field.strip() + if len(other_field) == 0: + continue + if other_field.startswith(ID_FIELD_BEGINNING): + 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 - who = todo_tag.values[0] - what = todo_tag.values[1] - field_id = None - field_state = None - field_date = None - for other_field in todo_tag.values[2:]: - if len(other_field) == 0: - continue - if other_field.startswith(ID_FIELD_BEGINNING): - 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 - else: + else: + try: + field_state = TodoState.from_name(other_field) + except ValueError: + try: + field_date = datetime.strptime(other_field, "%d.%m.%Y") + except ValueError: try: - field_state = TodoState.from_name(other_field.strip()) + field_state, field_date = TodoState.from_name_with_date(other_field.strip(), protocol=protocol) except ValueError: try: - field_date = datetime.strptime(other_field.strip(), "%d.%m.%Y") + field_state = TodoState.from_name_lazy(other_field) except ValueError: error = protocol.create_error("Parsing", "Invalid field", - "The todo in line {} has the field '{}', but" - "this does neither match a date (\"%d.%m.%Y\")" + "The todo in line {} has the field '{}', but " + "this does neither match a date (\"%d.%m.%Y\") " "nor a state.".format( todo_tag.linenumber, other_field)) db.session.add(error) db.session.commit() return - if field_state is None: - field_state = TodoState.open - if field_state.needs_date() and field_date is None: - error = protocol.create_error("Parsing", - "Todo missing date", - "The todo in line {} has a state that needs a date, " - "but the todo does not have one.".format(todo_tag.line)) - db.session.add(error) - db.session.commit() - return - who = who.strip() - what = what.strip() - todo = None - if field_id is not None: - todo = Todo.query.filter_by(number=field_id).first() - if todo is None and not config.PARSER_LAZY: - # TODO: add non-strict mode (at least for importing old protocols) - error = protocol.create_error("Parsing", - "Invalid Todo ID", - "The todo in line {} has the ID {}, but there is no " - "Todo with that ID.".format(todo_tag.linenumber, field_id)) - db.session.add(error) - db.session.commit() - return - if todo is None: - protocol_key = protocol.get_identifier() - old_candidates = OldTodo.query.filter( - OldTodo.protocol_key == protocol_key).all() - if len(old_candidates) == 0: - # new protocol - todo = Todo(type_id=protocol.protocoltype.id, - who=who, description=what, state=field_state, - date=field_date) - db.session.add(todo) - db.session.commit() - todo.number = field_id or todo.id - db.session.commit() - else: - # old protocol - number = field_id or lookup_todo_id(old_candidates, who, what) - todo = Todo.query.filter_by(number=number).first() - if todo is None: - todo = Todo(type_id=protocol.protocoltype.id, - who=who, description=what, state=field_state, - date=field_date, number=number) - db.session.add(todo) - db.session.commit() - todo.protocols.append(protocol) - db.session.commit() - todo_tag.todo = todo - # Decisions - old_decisions = list(protocol.decisions) - for decision in old_decisions: - protocol.decisions.remove(decision) + if field_state is None: + field_state = TodoState.open + if field_state.needs_date() and field_date is None: + error = protocol.create_error("Parsing", + "Todo missing date", + "The todo in line {} has a state that needs a date, " + "but the todo does not have one.".format(todo_tag.linenumber)) + db.session.add(error) 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) + return + who = who.strip() + what = what.strip() + todo = None + if field_id is not None: + todo = Todo.query.filter_by(number=field_id).first() + if todo is None and not config.PARSER_LAZY: + # TODO: add non-strict mode (at least for importing old protocols) + error = protocol.create_error("Parsing", + "Invalid Todo ID", + "The todo in line {} has the ID {}, but there is no " + "Todo with that ID.".format(todo_tag.linenumber, field_id)) + db.session.add(error) 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) - compile_decision(decision_content, decision) - old_tops = list(protocol.tops) - for top in old_tops: - protocol.tops.remove(top) - tops = [] - for index, fork in enumerate((child for child in tree.children if isinstance(child, Fork))): - top = TOP(protocol.id, fork.name, index, False) - db.session.add(top) + return + if todo is None: + protocol_key = protocol.get_identifier() + old_candidates = OldTodo.query.filter( + OldTodo.protocol_key == protocol_key).all() + if len(old_candidates) == 0: + # new protocol + todo = Todo(type_id=protocol.protocoltype.id, + who=who, description=what, state=field_state, + date=field_date) + db.session.add(todo) + db.session.commit() + todo.number = field_id or todo.id + db.session.commit() + else: + # old protocol + number = field_id or lookup_todo_id(old_candidates, who, what) + todo = Todo.query.filter_by(number=number).first() + if todo is None: + todo = Todo(type_id=protocol.protocoltype.id, + who=who, description=what, state=field_state, + date=field_date, number=number) + db.session.add(todo) + db.session.commit() + todo.protocols.append(protocol) + todo.state = field_state + todo.date = field_date + db.session.commit() + todo_tag.todo = todo + # Decisions + 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() + decision_top = decision_tag.fork.get_top() + decision_content = texenv.get_template("decision.tex").render( + render_type=RenderType.latex, decision=decision, + protocol=protocol, top=decision_top, show_private=False) + maxdepth = decision_top.get_maxdepth() + compile_decision(decision_content, decision, maxdepth=maxdepth) + old_tops = list(protocol.tops) + for top in old_tops: + protocol.tops.remove(top) + tops = [] + for index, fork in enumerate((child for child in tree.children if isinstance(child, Fork))): + top = TOP(protocol.id, fork.name, index, False) + db.session.add(top) + db.session.commit() - render_kwargs = { - "protocol": protocol, - "tree": tree - } - privacy_states = [False] - content_private = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=True, **render_kwargs) - content_public = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=False, **render_kwargs) - if content_private != content_public: - privacy_states.append(True) - protocol.content_private = content_private - protocol.content_public = content_public + render_kwargs = { + "protocol": protocol, + "tree": tree + } + maxdepth = tree.get_maxdepth() + privacy_states = [False] + content_private = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=True, **render_kwargs) + content_public = render_template("protocol.txt", render_type=RenderType.plaintext, show_private=False, **render_kwargs) + if content_private != content_public: + privacy_states.append(True) + protocol.content_private = content_private + protocol.content_public = content_public - for show_private in privacy_states: - latex_source = texenv.get_template("protocol.tex").render(render_type=RenderType.latex, show_private=show_private, **render_kwargs) - compile(latex_source, protocol, show_private=show_private) + for show_private in privacy_states: + latex_source = texenv.get_template("protocol.tex").render(render_type=RenderType.latex, show_private=show_private, **render_kwargs) + compile(latex_source, protocol, show_private=show_private, maxdepth=maxdepth) - if protocol.protocoltype.use_wiki: - wiki_source = render_template("protocol.wiki", render_type=RenderType.wikitext, show_private=not protocol.protocoltype.wiki_only_public, **render_kwargs).replace("\n\n\n", "\n\n") - push_to_wiki(protocol, wiki_source, "Automatisch generiert vom Protokollsystem 3.0") - protocol.done = True - db.session.commit() + if protocol.protocoltype.use_wiki: + wiki_source = render_template("protocol.wiki", render_type=RenderType.wikitext, show_private=not protocol.protocoltype.wiki_only_public, **render_kwargs).replace("\n\n\n", "\n\n") + push_to_wiki(protocol, wiki_source, "Automatisch generiert vom Protokollsystem 3.0") + protocol.done = True + db.session.commit() def push_to_wiki(protocol, content, summary): push_to_wiki_async.delay(protocol.id, content, summary) @@ -278,14 +300,14 @@ def push_to_wiki_async(protocol_id, content, summary): except WikiException as exc: 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=show_private) +def compile(content, protocol, show_private, maxdepth): + compile_async.delay(content, protocol.id, show_private=show_private, maxdepth=maxdepth) -def compile_decision(content, decision): - compile_async.delay(content, decision.id, use_decision=True) +def compile_decision(content, decision, maxdepth): + compile_async.delay(content, decision.id, use_decision=True, maxdepth=maxdepth) @celery.task -def compile_async(content, protocol_id, show_private=False, use_decision=False): +def compile_async(content, protocol_id, show_private=False, use_decision=False, maxdepth=5): with tempfile.TemporaryDirectory() as compile_dir, app.app_context(): decision = None protocol = None @@ -301,7 +323,7 @@ def compile_async(content, protocol_id, show_private=False, use_decision=False): log_filename = "protocol.log" with open(os.path.join(compile_dir, protocol_source_filename), "w") as source_file: source_file.write(content) - protocol2_class_source = texenv.get_template("protokoll2.cls").render(fonts=config.FONTS) + protocol2_class_source = texenv.get_template("protokoll2.cls").render(fonts=config.FONTS, maxdepth=maxdepth, bulletpoints=config.LATEX_BULLETPOINTS) with open(os.path.join(compile_dir, "protokoll2.cls"), "w") as protocol2_class_file: protocol2_class_file.write(protocol2_class_source) os.chdir(compile_dir) @@ -335,9 +357,13 @@ def compile_async(content, protocol_id, show_private=False, use_decision=False): total_log_filename = os.path.join(compile_dir, log_filename) if os.path.isfile(total_log_filename): with open(total_log_filename, "r") as log_file: - log = log_file.read() + log = "Log:\n\n" + add_line_numbers(log_file.read()) else: log = "Logfile not found." + total_source_filename = os.path.join(compile_dir, protocol_source_filename) + if os.path.isfile(total_source_filename): + with open(total_source_filename, "r") as source_file: + log += "\n\nSource:\n\n" + add_line_numbers(source_file.read()) error = protocol.create_error("Compiling", "Compiling LaTeX failed", log) db.session.add(error) db.session.commit() @@ -461,3 +487,13 @@ def push_tops_to_calendar_async(protocol_id): "Pushing TOPs to Calendar failed", str(exc)) db.session.add(error) db.session.commit() + +def set_etherpad_content(protocol): + set_etherpad_content_async.delay(protocol_id) + +@celery.task +def set_etherpad_content_async(protocol_id): + with app.app_context(): + protocol = Protocol.query.filter_by(id=protocol_id).first() + set_etherpad_text(protocol.get_identifier(), protocol.get_template()) + diff --git a/templates/decision.tex b/templates/decision.tex index 8cc7a93..ad042ba 100644 --- a/templates/decision.tex +++ b/templates/decision.tex @@ -23,10 +23,18 @@ \\\normalsize \VAR{protocol.protocoltype.organization|escape_tex} }{} \begin{tabular}{rp{14cm}} +\ENV{if protocol.date is not none} {\bf Datum:} & \VAR{protocol.date|datify_long|escape_tex}\\ +\ENV{endif} +\ENV{if protocol.location is not none} {\bf Ort:} & \VAR{protocol.location|escape_tex}\\ -{\bf Protokollant:} & \VAR{protocol.author|escape_tex}\\ +\ENV{endif} +\ENV{if protocol.author is not none} +{\bf Protokoll:} & \VAR{protocol.author|escape_tex}\\ +\ENV{endif} +\ENV{if protocol.participants is not none} {\bf Anwesend:} & \VAR{protocol.participants|escape_tex}\\ +\ENV{endif} \end{tabular} \normalsize diff --git a/templates/index.html b/templates/index.html index 17a8c51..e32617a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -58,7 +58,7 @@ <p><strong>Ort:</strong> {{protocol.location}}</p> {% endif %} {% if protocol.author is not none %} - <p><strong>Protokollant:</strong> {{protocol.author}}</p> + <p><strong>Protokoll:</strong> {{protocol.author}}</p> {% endif %} {% if protocol.participants is not none %} <p><strong>Anwesende:</strong> {{protocol.participants}}</p> diff --git a/templates/protocol-mail.txt b/templates/protocol-mail.txt index 9c228ea..c1e4a51 100644 --- a/templates/protocol-mail.txt +++ b/templates/protocol-mail.txt @@ -2,7 +2,7 @@ Protocol der {{protocol.name}} vom {{protocol.date|datify}} Datum: {{protocol.date|datify_long}} Zeit: von {{protocol.start_time|timify}} bis {{protocol.end_time|timify}} -Protokollant: {{protocol.author}} +Protokoll: {{protocol.author}} Anwesende: {{protocol.participants}} Die Tagesordnung ist: diff --git a/templates/protocol-show.html b/templates/protocol-show.html index 2a11a9c..3185396 100644 --- a/templates/protocol-show.html +++ b/templates/protocol-show.html @@ -62,10 +62,10 @@ {% if protocol.location is not none %} <p><strong>Ort:</strong> {{protocol.location}}</p> {% endif %} - {% if protocol.author is not none %} - <p><strong>Protokollant:</strong> {{protocol.author}}</p> + {% if protocol.author is not none and has_public_view_right %} + <p><strong>Protokoll:</strong> {{protocol.author}}</p> {% endif %} - {% if protocol.participants is not none %} + {% if protocol.participants is not none and has_public_view_right %} <p><strong>Anwesende:</strong> {{protocol.participants}}</p> {% endif %} {% else %} diff --git a/templates/protocol.tex b/templates/protocol.tex index 76d742e..784684d 100644 --- a/templates/protocol.tex +++ b/templates/protocol.tex @@ -30,7 +30,7 @@ {\bf Ort:} & \VAR{protocol.location|escape_tex}\\ \ENV{endif} \ENV{if protocol.author is not none} - {\bf Protokollant:} & \VAR{protocol.author|escape_tex}\\ + {\bf Protokoll:} & \VAR{protocol.author|escape_tex}\\ \ENV{endif} \ENV{if protocol.participants is not none} {\bf Anwesend:} & \VAR{protocol.participants|escape_tex}\\ diff --git a/templates/protokoll2.cls b/templates/protokoll2.cls index be443f0..2094516 100644 --- a/templates/protokoll2.cls +++ b/templates/protokoll2.cls @@ -22,6 +22,13 @@ \RequirePackage{eurosym} \RequirePackage[babel]{csquotes} \RequirePackage{polyglossia} +\RequirePackage{enumitem} + +\setlistdepth{\VAR{maxdepth}} +\renewlist{itemize}{itemize}{\VAR{maxdepth}} +\ENV{for index in range(maxdepth)} + \setlist[itemize,\VAR{index+1}]{label=\VAR{loop.cycle(*bulletpoints)}} +\ENV{endfor} \setmainlanguage[babelshorthands=true]{german} diff --git a/templates/top-new.html b/templates/top-new.html index b065246..516ca1a 100644 --- a/templates/top-new.html +++ b/templates/top-new.html @@ -4,7 +4,7 @@ {% set loggedin = check_login() %} {% set user = current_user() %} -{% set has_public_type_view_right = protocol.has_public_type_view_right(user) %} +{% set has_public_type_view_right = protocol.protocoltype.has_public_view_right(user) %} {% set has_public_view_right = protocol.has_public_view_right(user) %} {% set has_private_view_right = protocol.has_private_view_right(user) %} {% set has_modify_right = protocol.has_modify_right(user) %} diff --git a/todostates.py b/todostates.py index 443ac00..cd9a7e0 100644 --- a/todostates.py +++ b/todostates.py @@ -33,6 +33,7 @@ def make_states(TodoState): "after": TodoState.after, "not before": TodoState.after, "vor": TodoState.before, + "bis": TodoState.before, "nur vor": TodoState.before, "nicht nach": TodoState.before, "before": TodoState.before, diff --git a/utils.py b/utils.py index efe1316..63c8225 100644 --- a/utils.py +++ b/utils.py @@ -3,6 +3,7 @@ from flask import render_template, request import random import string import regex +import math import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -159,3 +160,14 @@ def optional_int_arg(name): return int(request.args.get(name)) except (ValueError, TypeError): return None + +def add_line_numbers(text): + raw_lines = text.splitlines() + linenumber_length = math.ceil(math.log10(len(raw_lines)) + 1) + lines = [] + for linenumber, line in enumerate(raw_lines): + lines.append("{} {}".format( + str(linenumber+1).rjust(linenumber_length), + line + )) + return "\n".join(lines) diff --git a/views/forms.py b/views/forms.py index 0f8bb87..2a9dcf0 100644 --- a/views/forms.py +++ b/views/forms.py @@ -5,6 +5,7 @@ from wtforms.validators import InputRequired, Optional from models.database import TodoState from validators import CheckTodoDateByState from calendarpush import Client as CalendarClient +from shared import current_user import config @@ -21,10 +22,21 @@ def get_todostate_choices(): ] def get_calendar_choices(): - return [ - (calendar, calendar) - for calendar in CalendarClient().get_calendars() - ] + calendars = CalendarClient().get_calendars() + choices = list(zip(calendars, calendars)) + choices.insert(0, ("", "Kein Kalender")) + return choices + +def get_printer_choices(): + choices = list(zip(config.PRINTING_PRINTERS, config.PRINTING_PRINTERS)) + choices.insert(0, ("", "Nicht drucken")) + return choices + +def get_group_choices(): + user = current_user() + choices = list(zip(user.groups, user.groups)) + choices.insert(0, ("", "Keine Gruppe")) + return choices def coerce_todostate(key): if isinstance(key, str): @@ -42,19 +54,23 @@ class ProtocolTypeForm(FlaskForm): organization = StringField("Organisation", validators=[InputRequired("Du musst eine zugehörige Organisation angeben.")]) usual_time = DateTimeField("Üblicher Beginn", validators=[InputRequired("Bitte gib die Zeit an, zu der die Sitzung beginnt.")], format="%H:%M") is_public = BooleanField("Öffentlich sichtbar") - private_group = StringField("Interne Gruppe") - public_group = StringField("Öffentliche Gruppe") + private_group = SelectField("Interne Gruppe", choices=[]) + public_group = SelectField("Öffentliche Gruppe", choices=[]) private_mail = StringField("Interner Verteiler") public_mail = StringField("Öffentlicher Verteiler") wiki_category = StringField("Wiki-Kategorie") use_wiki = BooleanField("Wiki benutzen") wiki_only_public = BooleanField("Wiki ist öffentlich") - printer = SelectField("Drucker", choices=list(zip(config.PRINTING_PRINTERS, config.PRINTING_PRINTERS))) + printer = SelectField("Drucker", choices=[]) calendar = SelectField("Kalender", choices=[]) def __init__(self, **kwargs): - super().__init__(self, **kwargs) + super().__init__(**kwargs) self.calendar.choices = get_calendar_choices() + self.printer.choices = get_printer_choices() + group_choices = get_group_choices() + self.private_group.choices = group_choices + self.public_group.choices = group_choices class DefaultTopForm(FlaskForm): name = StringField("Name", validators=[InputRequired("Du musst einen Namen angeben.")]) @@ -103,7 +119,7 @@ class ProtocolForm(FlaskForm): start_time = DateTimeField("Beginn", format="%H:%M", validators=[Optional()]) end_time = DateTimeField("Ende", format="%H:%M", validators=[Optional()]) location = StringField("Ort") - author = StringField("Protokollant") + author = StringField("Protokoll") participants = StringField("Anwesende") done = BooleanField("Fertig") public = BooleanField("Veröffentlicht") diff --git a/views/tables.py b/views/tables.py index 89d2c03..e47a75d 100644 --- a/views/tables.py +++ b/views/tables.py @@ -121,8 +121,9 @@ class ProtocolTypeTable(SingleValueTable): calendar_headers = ["Kalender"] if not config.CALENDAR_ACTIVE: calendar_headers = [] + action_headers = ["Aktion"] return (general_headers + mail_headers + printing_headers - + wiki_headers + calendar_headers) + + wiki_headers + calendar_headers + action_headers) def row(self): general_part = [ @@ -153,7 +154,8 @@ class ProtocolTypeTable(SingleValueTable): calendar_part = [self.value.calendar if self.value.calendar is not None else ""] if not config.CALENDAR_ACTIVE: calendar_part = [] - return general_part + mail_part + printing_part + wiki_part + calendar_part + action_part = [Table.link(url_for("delete_type", type_id=self.value.id), "Löschen", confirm="Bist du dir sicher, dass du den Protokolltype {} löschen möchtest?".format(self.value.name))] + return general_part + mail_part + printing_part + wiki_part + calendar_part + action_part class DefaultTOPsTable(Table): def __init__(self, tops, protocoltype=None): @@ -276,9 +278,10 @@ class TodoTable(SingleValueTable): row = [ self.value.get_id(), self.value.get_state_plain(), - Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier()) - if protocol is not None - else Table.link(url_for("list_protocols", protocolttype=self.value.protocoltype.id), self.value.protocoltype.short_name), + Table.concat([ + Table.link(url_for("show_protocol", protocol_id=protocol.id), protocol.get_identifier()) + for protocol in self.value.protocols + ]), self.value.who, self.value.description ] -- GitLab