diff --git a/calendarpush.py b/calendarpush.py index f69e7d081c2b2a68bfb04909e871b60b35376d4e..d9b7f6b0a4974246d3d72ad53820d273c8dc7eed 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 -from caldav import DAVClient +import caldav from vobject.base import ContentLine -from shared import config +from shared import config, date_filter_short, time_filter_short class CalendarException(Exception): @@ -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( @@ -58,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): @@ -93,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): @@ -106,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] @@ -125,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() @@ -140,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 0000000000000000000000000000000000000000..e365f13afc941b0a9ccccd7b6b2855f7ad143b13 --- /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 6d98e438eb843b9d6e789706878938375b6e32d4..b209003c85a2715e32d3d163f3a56e46086aab36 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 c755c59f31c015d23b2d16a3d5b08d17c1396274..40188f9a618587b2f176c1071c9ddad9007dc5cb 100644 --- a/protoparser.py +++ b/protoparser.py @@ -1,10 +1,11 @@ +import markupsafe 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], markupsafe.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 57271353d070318274e6d64ff41ea36b5012be17..e748ecf53bdf735aa5e8333a1d4045b1bab88acf 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/server.py b/server.py index 0997e7ca654d2119ca65594664ec86f3bd2d2e3d..6c5c1f8f2c0edc5504a9d78570aa1112c098510e 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 d894769faa617cef4fe44395e9aaadc48960ec90..60b382d077c90b73c7550aeb8be1bde0abad7401 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.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 @@ -1012,4 +1084,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/templates/documentation-syntax-tags.html b/templates/documentation-syntax-tags.html index 2d827151d01a786656de956ea4120955079cbc5e..d3805fe4232e40ed772ef5611fc31c40cdad8b86 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 8a4d1fd3eece631bdd9f04de77cea93cef710c77..7e853c4f24125775f23e1e628610fa9d28690c52 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 0c7a630d1b21e9f3186e6750100624174f2b6094..874a07d156d95c308eeefb66fd28b016d80eda2f 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 b3d34e32f86b45fa87362c21c01fa1a7235b09c8..05107849ea9f15b0ebddfb451dfaac55591e2e72 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/utils.py b/utils.py index e3d68e4677aa5621459ce9f291483211f08a0b19..3dd7fcb5f55cc934b736b1c00e2cca319d76cfbf 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 diff --git a/views/forms.py b/views/forms.py index 551763351d47a69676f8140da6f7b72b64bc8fa3..a2f2551b46512a183c4713058988077c7b6edc2c 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 d2efafa1eb241ce8a450c7311c5a33c0973c076f..28af0d7f7b93eb047005e41e463a966a6e9a819a 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 @@ -234,7 +235,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 +296,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 ""]