From 19b122b9186942899d0ecf208f5d5c04c97e6fdb Mon Sep 17 00:00:00 2001 From: veni-vidi-code <tom.mucke@web.de> Date: Fri, 18 Apr 2025 03:04:42 +0200 Subject: [PATCH 1/4] Improves etherpad opening by removing unnecessary template call Moves the call of protocol.get_template after the check if the etherpad is default and needs this. This removes unnecessary overhead each time someone clicks the etherpad button when the pad is already set up. --- tasks.py | 2 +- utils.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index d894769..909d51a 100644 --- a/tasks.py +++ b/tasks.py @@ -1012,4 +1012,4 @@ def set_etherpad_content_async(protocol_id): with app.app_context(): protocol = Protocol.query.filter_by(id=protocol_id).first() identifier = protocol.get_identifier() - return set_etherpad_text(identifier, protocol.get_template()) + return set_etherpad_text(identifier, protocol.get_template) diff --git a/utils.py b/utils.py index e3d68e4..3dd7fcb 100644 --- a/utils.py +++ b/utils.py @@ -163,6 +163,8 @@ def set_etherpad_text(pad, text, only_if_default=True): and len(current_text) > 0): return False client = get_etherpad_api_client() + if callable(text): + text = text() # This ensures that the text is only generated if needed and not always client.setText(padID=pad, text=text) return True -- GitLab From 568b29cae09bf6d497c5fa9200ff7f1f57cb6468 Mon Sep 17 00:00:00 2001 From: veni-vidi-code <tom.mucke@web.de> Date: Fri, 18 Apr 2025 03:12:25 +0200 Subject: [PATCH 2/4] Fixes #224 OpenXchange caldav Implements the first of the two in #224 proposed fixes to enable caldav with our openxchange server --- calendarpush.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/calendarpush.py b/calendarpush.py index f69e7d0..24930a5 100644 --- a/calendarpush.py +++ b/calendarpush.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta import random import quopri -from caldav import DAVClient +import caldav from vobject.base import ContentLine from shared import config @@ -17,11 +17,15 @@ class Client: if not config.CALENDAR_ACTIVE: return self.url = url if url is not None else config.CALENDAR_URL - self.client = DAVClient(self.url) + self.client = caldav.DAVClient(self.url) self.principal = None for _ in range(config.CALENDAR_MAX_REQUESTS): try: self.principal = self.client.principal() + try: + self.principal.calendars() + except Exception: + self.principal = caldav.Principal(self.client, self.url) # workaround for openxchange break except Exception as exc: print("Got exception {} from caldav, retrying".format( -- GitLab From c3a35242b9b319da660f7c3a64749ebc0c7bc4cc Mon Sep 17 00:00:00 2001 From: veni-vidi-code <tom.mucke@web.de> Date: Fri, 18 Apr 2025 03:17:48 +0200 Subject: [PATCH 3/4] Adds Termin Tag Adds extended calendar support by a) smaller changes to the sitzungstag (e.g. LaTeX) b) Adding a new Tag "Termin" which automatically gets pushed to the caldav server c) Fixing #222 The new Termin Tag also supports Kategories to allow multiple protocol types to share one calender without interference. --- calendarpush.py | 109 +++++++++++++++--- ...c0610e845e_adds_lookahead_calendar_days.py | 30 +++++ models/database.py | 15 +++ protoparser.py | 37 +++++- requirements.txt | 2 +- tasks.py | 76 +++++++++++- templates/documentation-syntax-tags.html | 14 ++- templates/protocol-template.txt | 7 ++ templates/protokoll2.cls | 10 ++ templates/top.tex | 10 ++ views/forms.py | 2 + views/tables.py | 4 +- 12 files changed, 291 insertions(+), 25 deletions(-) create mode 100644 migrations/versions/94c0610e845e_adds_lookahead_calendar_days.py diff --git a/calendarpush.py b/calendarpush.py index 24930a5..d9b7f6b 100644 --- a/calendarpush.py +++ b/calendarpush.py @@ -1,11 +1,11 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, time import random import quopri import caldav from vobject.base import ContentLine -from shared import config +from shared import config, date_filter_short, time_filter_short class CalendarException(Exception): @@ -62,32 +62,90 @@ class Client: return calendar raise CalendarException("No calendar named {}.".format(calendar_name)) - def set_event_at(self, begin, name, description): + def set_event_at(self, begin, name, description=None, categories=None, append_categories=True, end=None): if not config.CALENDAR_ACTIVE: return + if end is None: + end = begin + timedelta(hours=config.CALENDAR_DEFAULT_DURATION) + + if isinstance(end, time): + end = datetime.combine(date=begin.date(), time=end, tzinfo=begin.tzinfo) + candidates = [ Event.from_raw_event(raw_event) - for raw_event in self.calendar.date_search( - begin, begin + timedelta(hours=1)) + for raw_event in self.calendar.search( + start=begin, + end=end + timedelta(hours=1), + comp_class=caldav.objects.Event, + expand=True, + ) ] candidates = [event for event in candidates if event.name == name] event = None + if len(candidates) == 0: - event = Event( - None, name, description, begin, - begin + timedelta(hours=config.CALENDAR_DEFAULT_DURATION)) + event = Event(None, name, description or '', begin, end, categories or []) vevent = self.calendar.add_event(event.to_vcal()) event.vevent = vevent else: event = candidates[0] - event.set_description(description) + if append_categories: + # make sure that the categories are not duplicated + for category in categories: + if category not in event.categories: + event.categories.append(category) + if description is not None: + event.set_description(description) event.vevent.save() + def get_events(self, start, lookahead=14, included_categories=None, protocoltype_id=None): + if not config.CALENDAR_ACTIVE: + return [] + events = [] + if lookahead <= 0: + return events + if not included_categories: + included_categories = None + else: + included_categories = set(included_categories) + end = start + timedelta(days=lookahead) + if protocoltype_id is not None: + protocol_category = "Protokoll_{}".format(protocoltype_id) + + for i in range(config.CALENDAR_MAX_REQUESTS): + try: + raw_events = self.calendar.search( + start=start, + end=end, + comp_class=caldav.objects.Event, + expand=True, + # split_expanded=True + ) + for raw_event in raw_events: + event = Event.from_raw_event(raw_event) + if protocoltype_id is not None and protocol_category in event.categories: + if event.begin.date() != start.date(): + events.append(event) + elif included_categories is None or (not event.categories) or any(category in included_categories + for category in event.categories): + events.append(event) + else: + print("Event {} not in categories {}".format( + event.categories, included_categories)) + break + except Exception as exc: + if i == config.CALENDAR_MAX_REQUESTS - 1: + raise + print("Got exception {} from caldav, retrying".format( + str(exc))) + return events + NAME_KEY = "summary" DESCRIPTION_KEY = "description" BEGIN_KEY = "dtstart" END_KEY = "dtend" +CATEGORIES_KEY = "categories" def _get_item(content, key): @@ -97,12 +155,14 @@ def _get_item(content, key): class Event: - def __init__(self, vevent, name, description, begin, end): + def __init__(self, vevent, name, description, begin, end, category: list, uid=None): self.vevent = vevent self.name = name self.description = description self.begin = begin self.end = end + self.categories = category + self.uid = uid if uid is not None else create_uid() @staticmethod def from_raw_event(vevent): @@ -110,11 +170,14 @@ class Event: content = raw_event.contents name = _get_item(content, NAME_KEY) description = _get_item(content, DESCRIPTION_KEY) - begin = _get_item(content, BEGIN_KEY) - end = _get_item(content, END_KEY) + offset = get_timezone_offset() + begin = _get_item(content, BEGIN_KEY) + offset + end = _get_item(content, END_KEY) + offset + category = _get_item(content, CATEGORIES_KEY) or [] + uid = _get_item(content, "uid") return Event( vevent=vevent, name=name, description=description, - begin=begin, end=end) + begin=begin, end=end, category=category, uid=uid) def set_description(self, description): raw_event = self.vevent.instance.contents["vevent"][0] @@ -129,8 +192,8 @@ class Event: content_line.params["ENCODING"] = ["QUOTED-PRINTABLE"] def __repr__(self): - return "<Event(name='{}', description='{}', begin={}, end={})>".format( - self.name, self.description, self.begin, self.end) + return "<Event(uid='{}', name='{}', description='{}', begin={}, end={}, categories={})>".format( + self.uid, self.name, self.description, self.begin, self.end, self.categories) def to_vcal(self): offset = get_timezone_offset() @@ -144,14 +207,26 @@ DTSTART:{begin} DTEND:{end} SUMMARY:{summary} DESCRIPTION;ENCODING=QUOTED-PRINTABLE:{description} +CATEGORIES:{categories} END:VEVENT END:VCALENDAR""".format( - uid=create_uid(), + uid=self.uid, now=date_format(datetime.now() - offset), begin=date_format(self.begin - offset), end=date_format(self.end - offset), summary=self.name, - description=encode_quopri(self.description)) + description=encode_quopri(self.description), + categories=','.join(self.categories) + ) + + def render_template(self): + """parts = ["termin", self.name or '', self.description or '', date_filter_short(self.begin), + time_filter_short(self.begin), date_filter_short(self.end), time_filter_short(self.end), + ','.join(self.categories or tuple()), self.uid]""" # This would be more complete but no one wants to read that in etherpad + parts = ["termin", date_filter_short(self.begin), + time_filter_short(self.begin), time_filter_short(self.end), self.name or '', + ','.join(self.categories or tuple())] + return "[{}]".format(";".join(parts)) def create_uid(): diff --git a/migrations/versions/94c0610e845e_adds_lookahead_calendar_days.py b/migrations/versions/94c0610e845e_adds_lookahead_calendar_days.py new file mode 100644 index 0000000..e365f13 --- /dev/null +++ b/migrations/versions/94c0610e845e_adds_lookahead_calendar_days.py @@ -0,0 +1,30 @@ +"""Adds lookahead calendar days + +Revision ID: 94c0610e845e +Revises: da846db78ff9 +Create Date: 2024-05-17 12:06:29.723797 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '94c0610e845e' +down_revision = 'da846db78ff9' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('protocoltypes', sa.Column('calendar_lookahead', sa.Integer(), nullable=True)) + op.add_column('protocoltypes', sa.Column('calendar_categories', sa.Text(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('protocoltypes', 'calendar_categories') + op.drop_column('protocoltypes', 'calendar_lookahead') + # ### end Alembic commands ### diff --git a/models/database.py b/models/database.py index 6d98e43..b209003 100644 --- a/models/database.py +++ b/models/database.py @@ -20,6 +20,7 @@ from sqlalchemy.orm import relationship, backref from todostates import make_states, make_state_glyphes +from calendarpush import Client as CalendarClient class DatabaseModel(db.Model): __abstract__ = True @@ -75,6 +76,8 @@ class ProtocolType(DatabaseModel): wiki_only_public = db.Column(db.Boolean) printer = db.Column(db.Text) calendar = db.Column(db.Text) + calendar_lookahead = db.Column(db.Integer) + calendar_categories = db.Column(db.Text) restrict_networks = db.Column(db.Boolean) allowed_networks = db.Column(db.Text) latex_template = db.Column(db.Text) @@ -380,6 +383,18 @@ class Protocol(DatabaseModel): if not todo.is_done() ] + def get_calendar_categories(self): + cats = self.protocoltype.calendar_categories.replace(" ", ",").split(",") + return {cat for cat in cats if cat != ""} + + def get_events(self): + if self.protocoltype.calendar is None or self.protocoltype.calendar == "": + return [] + client = CalendarClient(self.protocoltype.calendar) + start = self.get_timezone_aware_start_date() + return client.get_events(start=start, lookahead=self.protocoltype.calendar_lookahead, + included_categories=self.get_calendar_categories(), protocoltype_id=self.protocoltype.id) + def has_compiled_document(self, private=None): candidates = [ document for document in self.documents diff --git a/protoparser.py b/protoparser.py index c755c59..9551385 100644 --- a/protoparser.py +++ b/protoparser.py @@ -1,10 +1,11 @@ +import flask import regex as re import sys from collections import OrderedDict from enum import Enum from shared import escape_tex -from utils import footnote_hash +from utils import footnote_hash, parse_datetime_from_string from shared import config @@ -225,6 +226,13 @@ class Tag: self.fork = fork def render(self, render_type, show_private, level=None, protocol=None, decision_render=False, top_render=False): + if self.name == "sitzung": + new_protocol_date = parse_datetime_from_string(self.values[0]).date() + protocols = protocol.protocoltype.get_protocols_on_date(new_protocol_date) + for protocol in protocols: + if protocol.get_time() == self.values[1]: + break + if render_type == RenderType.latex and not top_render: if self.name == "url": return r"\url{{{}}}".format(self.values[0]) @@ -241,6 +249,14 @@ class Tag: return r"\Beschluss{{{}}}".format(self.decision.content) elif self.name == "footnote": return r"\footnote{{{}}}".format(self.values[0]) + elif self.name == "sitzung": + if protocol is not None: + return r"\Sitzung[{0}/protocol/show/{1}]{{{2}}}{{{3}}}".format(config.SERVER_NAME, protocol.id, protocol.date, protocol.start_time) + else: + return r"\Sitzung{{{1}}}{{{2}}}".format(self.values[0], self.values[1]) + elif self.name == "termin": + return r"\Termin{{{0}}}{{{1}}}{{{2}}}{{{3}}}".format( + self.values[0], self.values[1], self.values[2], escape_tex(self.values[3])) return r"\textbf{{{}:}} {}".format( escape_tex(self.name.capitalize()), escape_tex(";".join(self.values))) @@ -257,6 +273,14 @@ class Tag: + r"\end{itemize} \end{tcolorbox}") elif self.name == "footnote": return r"\footnote{{{}}}".format(self.values[0]) + elif self.name == "sitzung": + if protocol is not None: + return r"\Sitzung[{0}/protocol/show/{1}]{{{2}}}{{{3}}}".format(config.SERVER_NAME, protocol.id, protocol.date, protocol.start_time) + else: + return r"\Sitzung{{{1}}}{{{2}}}".format(self.values[0], self.values[1]) + elif self.name == "termin": + return r"\Termin{{{0}}}{{{1}}}{{{2}}}{{{3}}}".format( + self.values[0], self.values[1], self.values[2], escape_tex(self.values[3])) return r"\textbf{{{}:}} {}".format( escape_tex(self.name.capitalize()), escape_tex(";".join(self.values))) @@ -305,6 +329,15 @@ class Tag: return ( '<sup id="#fnref{0}"><a href="#fn{0}">Fn</a></sup>'.format( footnote_hash(self.values[0]))) + elif self.name == "sitzung": + if protocol is not None: + return ("<a href=\"/protocol/show/{0}\"><b>Sitzung</b> am {1} um {2}</a>" + .format(protocol.id, protocol.date, protocol.start_time)) + else: + return "<b>Sitzung</b> am {0} um {1}".format(self.values[0], self.values[1]) + elif self.name == "termin": + return "<b>Termin:</b> {3} {0} {1} - {2}".format( + self.values[0], self.values[1], self.values[2], flask.escape(self.values[3])) return "[{}: {}]".format(self.name, ";".join(self.values)) elif render_type == RenderType.dokuwiki: if self.name == "url": @@ -343,7 +376,7 @@ class Tag: PATTERN = r"\[(?<content>[^\]]*)\]" - KNOWN_TAGS = ["todo", "url", "beschluss", "footnote", "sitzung"] + KNOWN_TAGS = ["todo", "url", "beschluss", "footnote", "sitzung", "termin"] class Empty(Element): diff --git a/requirements.txt b/requirements.txt index 5727135..e748ecf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ blessed==1.19.1 blessings==1.7 blinker==1.5 bpython==0.23 -caldav==0.10.0 +caldav~=1.3.6 celery==5.2.7 certifi==2022.9.24 chardet==5.0.0 diff --git a/tasks.py b/tasks.py index 909d51a..13ff7e7 100644 --- a/tasks.py +++ b/tasks.py @@ -5,7 +5,7 @@ import subprocess import shutil import tempfile from datetime import datetime, timedelta -import time +from dateutil import tz import traceback from copy import copy import xmlrpc.client @@ -506,6 +506,51 @@ def parse_protocol_async_inner(protocol, ignore_old_date=False): if new_protocol_date > datetime.now().date(): Protocol.create_new_protocol(protocol.protocoltype, new_protocol_date) + # Appointments + appointment_tags = [tag for tag in tags if tag.name == "termin"] + for appointment_tag in appointment_tags: + if len(appointment_tag.values) not in {4, 5}: + return _make_error( + protocol, "Parsing", "Invalid appointment", + "The appointment in line {} has to have either 4 or 5 values".format( + appointment_tag.linenumber)) + try: + date = parse_datetime_from_string(appointment_tag.values[0]) + except ValueError as exc: + return _make_error( + protocol, "Parsing", "Invalid date", + "'{}' is not a valid date.".format( + appointment_tag.values[0])) + + try: + begin_time = datetime.strptime(appointment_tag.values[1], "%H:%M").time() + end_time = datetime.strptime(appointment_tag.values[2], "%H:%M").time() + except ValueError as exc: + return _make_error( + protocol, "Parsing", "Invalid time", + "The time in line {} is not valid.".format( + appointment_tag.linenumber)) + name = appointment_tag.values[3] + if len(appointment_tag.values) == 5: + categories = appointment_tag.values[4].split(",") + categories = [category.strip() for category in categories] + categories = [category for category in categories if category] + if len(categories) > 0: + included_categories = protocol.protocoltype.get_calendar_categories() + if included_categories: + included_categories.add("Protokoll_{}".format(protocol.protocoltype.id)) + if included_categories and not any(category in included_categories for category in categories): + raise ValueError("The appointment in line {} has the categories {}, but needs to have at least " + "one of the following categories: {}." + .format(appointment_tag.linenumber, + ", ".join(categories), ", ".join(included_categories))) + + else: + categories = [] + + start = datetime.combine(date, begin_time, tzinfo=tz.tzlocal()) + push_appointments_to_calendar(protocol, start, end_time, name, categories) + # TOPs old_tops = list(protocol.tops) tops = [] @@ -994,13 +1039,40 @@ def push_tops_to_calendar_async(protocol_id): client = CalendarClient(protocol.protocoltype.calendar) client.set_event_at( begin=protocol.get_datetime(), - name=protocol.protocoltype.short_name, description=description) + name=protocol.protocoltype.short_name, description=description, + categories=["Sitzung", "Protokoll_" + str(protocol.protocoltype.id)]) except CalendarException as exc: return _make_error( protocol, "Calendar", "Pushing TOPs to Calendar failed", str(exc)) +def push_appointments_to_calendar(protocol, start, end, name, categories): + push_appointments_to_calendar_async.delay(protocol.id, start, end, name, categories) + + +@celery.task +def push_appointments_to_calendar_async(protocol_id, start, end, name, categories): + if not config.CALENDAR_ACTIVE: + return + with app.app_context(): + protocol = Protocol.query.filter_by(id=protocol_id).first() + if protocol.protocoltype.calendar == "": + return + + try: + client = CalendarClient(protocol.protocoltype.calendar) + client.set_event_at( + begin=start, + end=end, + name=name, + categories=categories) + except CalendarException as exc: + return _make_error( + protocol, "Calendar", + "Pushing Appointments to Calendar failed", str(exc)) + + def set_etherpad_content(protocol): # wait for the users browser to open the etherpad # and for etherpad to create it, otherwise the import will fail diff --git a/templates/documentation-syntax-tags.html b/templates/documentation-syntax-tags.html index 2d82715..d3805fe 100644 --- a/templates/documentation-syntax-tags.html +++ b/templates/documentation-syntax-tags.html @@ -34,6 +34,7 @@ <li><a href="#todo">Todo</a></li> <li><a href="#footnote">Fußnote</a></li> <li><a href="#session">Sitzung</a></li> + <li><a href="#termin">Termin</a></li> </ul> </div> </div> @@ -145,5 +146,16 @@ </p> <figure> <pre class="highlight"><code><span class="nt">[sitzung</span>;<span class="mi">Datum</span>;<span class="mi">Uhrzeit</span><span class="nt">]</span></code></pre> - </figure> + </figure> + + <h4 id="termin">Termin-Tag</h4> + Werden in einer Sitzung Termine festgelegt, so können diese direkt aus dem Protokoll heraus angelegt und im Protokoll entsprechend hervorgehoben werden. Sie werden, sofern konfiguriert, darüber hinaus auch im Kalender des Protokolltyps angelegt und in kommenden Sitzungen unter dem TOP „Kalender“ aufgeführt. + Es wird der Tag des Types <code class="highlight" style="color: inherit;"><span class="nt">termin</span></code> genutzt. + Als erstes Argument nimmt der Tag das Datum in der Form <code class="highlight" style="color: inherit;"><span class="nt">[</span>…;<span class="mi">dd.mm.yyyy</span><span class="nt">]</span></code> entgegen, als zweites die Startzeit in der Form <code class="highlight" style="color: inherit;"><span class="nt">[</span>…;<span class="mi">h:mm</span><span class="nt">]</span></code> und als drittes die Endzeit im selben Format. Anschließend muss der Titel des Termins angegeben werden. Optional können noch die Kategorien des Termins angegeben werden, um Termine zu bestimmten Themen zusammenfassen zu können und für Kalender filtern zu können. + <figure> + <pre class="highlight"><code><span class="nt">[termin</span>;<span class="mi">Datum</span>;<span class="mi">Startzeit</span>;<span class="mi">Endzeit</span>;<span class="sx">Titel</span>;<span class="sx">Kategorie1,Kategorie2,…</span><span class="nt">]</span></code></pre> + </figure> + + Falls der Kalender eine Liste von Kategorien voreingestellt hat, so werden nur diese Kategorien in der Liste angezeigt, sowie alle Termine ohne Kategorie. + {% endblock %} diff --git a/templates/protocol-template.txt b/templates/protocol-template.txt index 8a4d1fd..7e853c4 100644 --- a/templates/protocol-template.txt +++ b/templates/protocol-template.txt @@ -20,6 +20,13 @@ {% endfor %} {% else %} {% endif %} + {% elif top.name == "Kalender" %} + {% set events=protocol.get_events() %} + {% if events|length > 0 %} + {% for event in events %} + {{event.render_template()}}; + {% endfor %} + {% endif %} {% else %} {% if use_description %} {% if top.description|length > 0 %} diff --git a/templates/protokoll2.cls b/templates/protokoll2.cls index 0c7a630..874a07d 100644 --- a/templates/protokoll2.cls +++ b/templates/protokoll2.cls @@ -253,3 +253,13 @@ % Styling der Todo und Beschlusstags im Protokoll \newcommand{\Todo}[4]{\textbf{{#1}}: #2: #3 -- #4} \newcommand{\Beschluss}[2][]{\textbf{Beschluss:} #2 \def\temp{#1}\ifx\temp\empty\else\textit{(#1)}\fi} +\newcommand{\Sitzung}[3][]{ + \ifthenelse{\equal{#1}{}}{ + \textbf{Sitzung} am #2 um #3 Uhr + }{ + \href{#1}{\textbf{Sitzung} am #2 um #3 Uhr} + } +} +\newcommand{\Termin}[4]{ + \textbf{#1} von #2 Uhr bis #3 Uhr: #4 +} diff --git a/templates/top.tex b/templates/top.tex index b3d34e3..0510784 100644 --- a/templates/top.tex +++ b/templates/top.tex @@ -97,6 +97,16 @@ \newcommand{\Beschluss}[2][]{\textbf{Beschluss:} #2 \def\temp{#1}\ifx\temp\empty\else\textit{(#1)}\fi} \newcommand{\Todo}[4]{\textbf{{#1}}: #2: #3 -- #4} +\newcommand{\Sitzung}[3][]{ + \ifthenelse{\equal{#1}{}}{ + \textbf{Sitzung} am #2 um #3 Uhr + }{ + \href{#1}{\textbf{Sitzung} am #2 um #3 Uhr} + } +} +\newcommand{\Termin}[4]{ + \textbf{#1} von #2 Uhr bis #3 Uhr: #4 +} \ENV{if show_private}\setboolean{intern}{true}\ENV{endif} \intern{\SetWatermarkText{INTERN} \SetWatermarkScale{1}}{} diff --git a/views/forms.py b/views/forms.py index 5517633..a2f2551 100644 --- a/views/forms.py +++ b/views/forms.py @@ -174,6 +174,8 @@ class ProtocolTypeForm(FlaskForm): wiki_only_public = BooleanField("Wiki ist öffentlich") printer = SelectField("Drucker", choices=[]) calendar = SelectField("Kalender", choices=[]) + calendar_lookahead = IntegerField('Anzahl Tage im Kalender', validators=[Optional()]) + calendar_categories = StringField('Kalender Kategorien', validators=[Optional()]) recurrence = IntegerField("Turnus (in Tagen)", validators=[Optional()]) restrict_networks = BooleanField("Netzwerke einschränken") allowed_networks = IPNetworkField("Erlaubte Netzwerke") diff --git a/views/tables.py b/views/tables.py index d2efafa..c37e8fe 100644 --- a/views/tables.py +++ b/views/tables.py @@ -234,7 +234,7 @@ class ProtocolTypeTable(SingleValueTable): wiki_headers.append("Wiki-Kategorie") if not config.WIKI_ACTIVE: wiki_headers = [] - calendar_headers = ["Kalender"] + calendar_headers = ["Kalender", 'Anzahl Tage im Kalender', 'Termin Kategorien'] if not config.CALENDAR_ACTIVE: calendar_headers = [] recurrence_headers = ["Turnus"] @@ -295,7 +295,7 @@ class ProtocolTypeTable(SingleValueTable): wiki_part = [] calendar_part = [ self.value.calendar - if self.value.calendar is not None else ""] + if self.value.calendar is not None else "", self.value.calendar_lookahead, self.value.calendar_categories or ''] if not config.CALENDAR_ACTIVE: calendar_part = [] recurrence_part = [f"{self.value.recurrence} Tage" if self.value.recurrence is not None else ""] -- GitLab From e1560a4839c9ab7f3c7e52e92f4b2bb6d0a77f10 Mon Sep 17 00:00:00 2001 From: veni-vidi-code <tom.mucke@web.de> Date: Sat, 19 Apr 2025 13:34:37 +0200 Subject: [PATCH 4/4] Refact to allow newer dependencies, Bugfix Refactors some parts to allow for newer version of packages in the dependencies (e.g. the escape used for Termine is no longer available from Flask and should be imported from Markupsafe), removes one unused import Small Bugfix for Termine in the compile --- protoparser.py | 4 ++-- server.py | 4 ++-- tasks.py | 2 +- views/tables.py | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/protoparser.py b/protoparser.py index 9551385..40188f9 100644 --- a/protoparser.py +++ b/protoparser.py @@ -1,4 +1,4 @@ -import flask +import markupsafe import regex as re import sys from collections import OrderedDict @@ -337,7 +337,7 @@ class Tag: return "<b>Sitzung</b> am {0} um {1}".format(self.values[0], self.values[1]) elif self.name == "termin": return "<b>Termin:</b> {3} {0} {1} - {2}".format( - self.values[0], self.values[1], self.values[2], flask.escape(self.values[3])) + self.values[0], self.values[1], self.values[2], markupsafe.escape(self.values[3])) return "[{}: {}]".format(self.name, ";".join(self.values)) elif render_type == RenderType.dokuwiki: if self.name == "url": diff --git a/server.py b/server.py index 0997e7c..6c5c1f8 100755 --- a/server.py +++ b/server.py @@ -4,8 +4,8 @@ locale.setlocale(locale.LC_TIME, "de_DE.utf8") from flask import ( Flask, request, session, flash, redirect, - url_for, abort, render_template, Response, Markup) -import click + url_for, abort, render_template, Response) +from markupsafe import Markup from werkzeug.utils import secure_filename from flask_migrate import Migrate from celery import Celery diff --git a/tasks.py b/tasks.py index 13ff7e7..60b382d 100644 --- a/tasks.py +++ b/tasks.py @@ -536,7 +536,7 @@ def parse_protocol_async_inner(protocol, ignore_old_date=False): categories = [category.strip() for category in categories] categories = [category for category in categories if category] if len(categories) > 0: - included_categories = protocol.protocoltype.get_calendar_categories() + included_categories = protocol.get_calendar_categories() if included_categories: included_categories.add("Protokoll_{}".format(protocol.protocoltype.id)) if included_categories and not any(category in included_categories for category in categories): diff --git a/views/tables.py b/views/tables.py index c37e8fe..28af0d7 100644 --- a/views/tables.py +++ b/views/tables.py @@ -1,4 +1,5 @@ -from flask import Markup, url_for +from flask import url_for +from markupsafe import Markup from shared import date_filter, datetime_filter, time_filter, current_user from common.csrf import get_csrf_token -- GitLab