From 8cf2272c9a580ed76f6b4a8b037720ec0aa0b880 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 20 Jul 2024 18:12:14 +0100 Subject: [PATCH 1/8] Start testing the calendar name --- .../features/calendars/calendar-name-rfc-.ics | 34 ++++++++++++++++++ .../calendars/calendar-x-wr-calname.ics | 35 +++++++++++++++++++ open_web_calendar/static/js/configure.js | 6 ++++ .../test/test_issue_406_calendar_name.py | 12 +++++++ 4 files changed, 87 insertions(+) create mode 100644 open_web_calendar/features/calendars/calendar-name-rfc-.ics create mode 100644 open_web_calendar/features/calendars/calendar-x-wr-calname.ics create mode 100644 open_web_calendar/test/test_issue_406_calendar_name.py diff --git a/open_web_calendar/features/calendars/calendar-name-rfc-.ics b/open_web_calendar/features/calendars/calendar-name-rfc-.ics new file mode 100644 index 0000000000..6e2f423598 --- /dev/null +++ b/open_web_calendar/features/calendars/calendar-name-rfc-.ics @@ -0,0 +1,34 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:test +X-APPLE-CALENDAR-COLOR:#e78074 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20190303T111937 +DTSTAMP:20190303T111937 +LAST-MODIFIED:20190303T111937 +UID:UYDQSG9TH4DE0WM3QFL2J +SUMMARY:test1 +DTSTART;TZID=Europe/Berlin:20190304T080000 +DTEND;TZID=Europe/Berlin:20190304T083000 +END:VEVENT +END:VCALENDAR diff --git a/open_web_calendar/features/calendars/calendar-x-wr-calname.ics b/open_web_calendar/features/calendars/calendar-x-wr-calname.ics new file mode 100644 index 0000000000..2addecb173 --- /dev/null +++ b/open_web_calendar/features/calendars/calendar-x-wr-calname.ics @@ -0,0 +1,35 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:old calendar description +X-WR-CALDESC: This calendar uses non-standard descriptions +X-APPLE-CALENDAR-COLOR:#e78074 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20190303T111937 +DTSTAMP:20190303T111937 +LAST-MODIFIED:20190303T111937 +UID:UYDQSG9TH4DE0WM3QFL2J +SUMMARY:test1 +DTSTART;TZID=Europe/Berlin:20190304T080000 +DTEND;TZID=Europe/Berlin:20190304T083000 +END:VEVENT +END:VCALENDAR diff --git a/open_web_calendar/static/js/configure.js b/open_web_calendar/static/js/configure.js index 9e6e162e12..fecf9b57a6 100644 --- a/open_web_calendar/static/js/configure.js +++ b/open_web_calendar/static/js/configure.js @@ -133,6 +133,12 @@ var template = { } return ""; }, + "calendar_name": function (event) { + if (event.calendar_name) { + return escapeHtml(event.calendar_name); + } + return ""; + }, "date": function (start, end) { /* One day * Multiday diff --git a/open_web_calendar/test/test_issue_406_calendar_name.py b/open_web_calendar/test/test_issue_406_calendar_name.py new file mode 100644 index 0000000000..1f11dfb723 --- /dev/null +++ b/open_web_calendar/test/test_issue_406_calendar_name.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2024 Nicco Kunzmann and Open Web Calendar Contributors +# +# SPDX-License-Identifier: GPL-2.0-only + +"""Events should know which calendar they are in. + +See https://github.com/niccokunzmann/open-web-calendar/issues/406 + +See https://datatracker.ietf.org/doc/html/rfc7986#section-5.1 +And X-WR-CALNAME is in use, too. +""" + From 8f0e4c815c125806be400d5b54fe3b3a957a8308 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 20 Jul 2024 18:55:04 +0100 Subject: [PATCH 2/8] Add calendar description and name to event JSON --- open_web_calendar/conversion_base.py | 54 +++++++++++++++++-- open_web_calendar/convert_to_dhtmlx.py | 24 ++++----- open_web_calendar/convert_to_ics.py | 21 ++++---- ...me-rfc-.ics => calendar-name-rfc-7986.ics} | 3 +- .../test/test_issue_406_calendar_name.py | 36 +++++++++++++ 5 files changed, 111 insertions(+), 27 deletions(-) rename open_web_calendar/features/calendars/{calendar-name-rfc-.ics => calendar-name-rfc-7986.ics} (85%) diff --git a/open_web_calendar/conversion_base.py b/open_web_calendar/conversion_base.py index 51d83f8b25..e3cb3f0734 100644 --- a/open_web_calendar/conversion_base.py +++ b/open_web_calendar/conversion_base.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2024 Nicco Kunzmann and Open Web Calendar Contributors # # SPDX-License-Identifier: GPL-2.0-only +from __future__ import annotations import io import sys @@ -19,6 +20,52 @@ def get_text_from_url(url): return requests.get(url, timeout=10).text +class CalendarInfo: + """Provide an easy API for calendar information.""" + + def __init__(self, index: int, url: str, calendar: Calendar): + """Create a new calendar info.""" + self._calendar = calendar + self._index = index + self._url = url + + @property + def name(self) -> str: + """The name of the calendar.""" + name = self._calendar.get("name", self._calendar.get("x-wr-calname")) + if name is not None: + return name + return self._url.rsplit("/", 1)[-1].rsplit(".", 1)[0] + + @property + def description(self) -> str: + """The name of the calendar.""" + return self._calendar.get("description", self._calendar.get("x-wr-caldesc", "")) + + @property + def calendar(self) -> Calendar: + """My calendar.""" + return self._calendar + + @property + def index(self) -> int: + """The index of the calendar url.""" + return self._index + + @property + def event_css_classes(self) -> list[str]: + """The css classes for all events in this calendar.""" + return [f"CALENDAR-INDEX-{self.index}"] + + def to_json(self) -> dict: + """Return this calendar information as JSON.""" + return { + "index": self.index, + "name": self.name, + "description": self.description, + } + + class ConversionStrategy: """Base class for conversions.""" @@ -83,13 +130,14 @@ def retrieve_calendar(self, index_url): try: index, url = index_url calendars = self.get_calendars_from_url(url) - self.collect_components_from(index, calendars) + for calendar in calendars: + self.collect_components_from(CalendarInfo(index, url, calendar)) except: ty, err, tb = sys.exc_info() with self.lock: self.components.append(self.error(ty, err, tb, url)) - def collect_components_from(self, index, calendars): + def collect_components_from(self, calendar_info: CalendarInfo): """Collect all the compenents from the calendar.""" raise NotImplementedError("to be implemented in subclasses") @@ -98,4 +146,4 @@ def merge(self): raise NotImplementedError("to be implemented in subclasses") -__all__ = ["ConversionStrategy", "get_text_from_url"] +__all__ = ["ConversionStrategy", "get_text_from_url", "CalendarInfo"] diff --git a/open_web_calendar/convert_to_dhtmlx.py b/open_web_calendar/convert_to_dhtmlx.py index dfb4b6a8e8..e733e472a7 100644 --- a/open_web_calendar/convert_to_dhtmlx.py +++ b/open_web_calendar/convert_to_dhtmlx.py @@ -13,7 +13,7 @@ from flask import jsonify from .clean_html import clean_html -from .conversion_base import ConversionStrategy +from .conversion_base import CalendarInfo, ConversionStrategy def is_date(date): @@ -66,7 +66,7 @@ def date_to_string(self, date): viewed_date = date.astimezone(self.timezone) return viewed_date.strftime("%Y-%m-%d %H:%M") - def convert_ical_event(self, calendar_index, calendar_event): + def convert_ical_event(self, calendar_info: CalendarInfo, calendar_event): start = calendar_event["DTSTART"].dt end = calendar_event.get("DTEND", calendar_event["DTSTART"]).dt if is_date(start) and is_date(end) and end == start: @@ -104,7 +104,8 @@ def convert_ical_event(self, calendar_index, calendar_event): "categories": self.get_event_categories(calendar_event), "css-classes": ["event"] + self.get_event_classes(calendar_event) - + [f"CALENDAR-INDEX-{calendar_index}"], + + calendar_info.event_css_classes, + "calendar": calendar_info.to_json(), } def convert_error(self, error, url, tb_s): @@ -164,16 +165,15 @@ def clean_html(self, html): def merge(self): return jsonify(self.components) - def collect_components_from(self, calendar_index, calendars): + def collect_components_from(self, calendar_info: CalendarInfo): # see https://stackoverflow.com/a/16115575/1320237 - for calendar in calendars: - events = recurring_ical_events.of(calendar).between( - self.from_date, self.to_date - ) - with self.lock: - for event in events: - json_event = self.convert_ical_event(calendar_index, event) - self.components.append(json_event) + events = recurring_ical_events.of(calendar_info.calendar).between( + self.from_date, self.to_date + ) + with self.lock: + for event in events: + json_event = self.convert_ical_event(calendar_info, event) + self.components.append(json_event) def get_event_classes(self, event) -> list[str]: """Return the CSS classes that should be used for the event styles.""" diff --git a/open_web_calendar/convert_to_ics.py b/open_web_calendar/convert_to_ics.py index 0cf541b5de..047c42f380 100644 --- a/open_web_calendar/convert_to_ics.py +++ b/open_web_calendar/convert_to_ics.py @@ -8,7 +8,7 @@ from icalendar import Calendar, Event, Timezone from icalendar.prop import vDDDTypes -from .conversion_base import ConversionStrategy +from .conversion_base import CalendarInfo, ConversionStrategy class ConvertToICS(ConversionStrategy): @@ -26,18 +26,17 @@ def is_timezone(self, component): """Whether a component is an event.""" return isinstance(component, Timezone) - def collect_components_from(self, calendar_index, calendars): - for calendar in calendars: - for component in calendar.walk(): - if self.is_event(component): + def collect_components_from(self, calendar_info: CalendarInfo): + for component in calendar_info.calendar.walk(): + if self.is_event(component): + with self.lock: + self.components.append(component) + if self.is_timezone(component): + tzid = component.get("TZID") + if tzid and tzid not in self.timezones: with self.lock: self.components.append(component) - if self.is_timezone(component): - tzid = component.get("TZID") - if tzid and tzid not in self.timezones: - with self.lock: - self.components.append(component) - self.timezones.add(tzid) + self.timezones.add(tzid) def convert_error(self, error, url, tb_s): """Create an error which can be used by the dhtmlx scheduler.""" diff --git a/open_web_calendar/features/calendars/calendar-name-rfc-.ics b/open_web_calendar/features/calendars/calendar-name-rfc-7986.ics similarity index 85% rename from open_web_calendar/features/calendars/calendar-name-rfc-.ics rename to open_web_calendar/features/calendars/calendar-name-rfc-7986.ics index 6e2f423598..e6a934585f 100644 --- a/open_web_calendar/features/calendars/calendar-name-rfc-.ics +++ b/open_web_calendar/features/calendars/calendar-name-rfc-7986.ics @@ -2,7 +2,8 @@ BEGIN:VCALENDAR VERSION:2.0 PRODID:-//SabreDAV//SabreDAV//EN CALSCALE:GREGORIAN -X-WR-CALNAME:test +NAME:RFC 7986 compatible calendar +DESCRIPTION:This is a later version with attributes X-APPLE-CALENDAR-COLOR:#e78074 BEGIN:VTIMEZONE TZID:Europe/Berlin diff --git a/open_web_calendar/test/test_issue_406_calendar_name.py b/open_web_calendar/test/test_issue_406_calendar_name.py index 1f11dfb723..0e87fddc3b 100644 --- a/open_web_calendar/test/test_issue_406_calendar_name.py +++ b/open_web_calendar/test/test_issue_406_calendar_name.py @@ -10,3 +10,39 @@ And X-WR-CALNAME is in use, too. """ +import pytest +from icalendar import Calendar + +from open_web_calendar.conversion_base import CalendarInfo + + +@pytest.mark.parametrize( + ("file", "name"), + [ + ("calendar-name-rfc-7986", "RFC 7986 compatible calendar"), + ("calendar-x-wr-calname", "old calendar description"), + ("food", "food"), + ], +) +def test_calendar_name_is_known(calendar_content, file, name): + """Check that we can extract the calendar name.""" + cal = CalendarInfo( + 0, f"https://localhost/{file}.ics", Calendar.from_ical(calendar_content[file]) + ) + assert cal.name == name + + +@pytest.mark.parametrize( + ("file", "description"), + [ + ("calendar-name-rfc-7986", "This is a later version with attributes"), + ("calendar-x-wr-calname", " This calendar uses non-standard descriptions"), + ("food", ""), + ], +) +def test_calendar_description(calendar_content, file, description): + """Check that we can extract the calendar name.""" + cal = CalendarInfo( + 0, f"https://localhost/{file}.ics", Calendar.from_ical(calendar_content[file]) + ) + assert cal.description == description From 37cdf69eae72976ce0965f5ffca26894e57b181a Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 22 Jul 2024 19:23:16 +0100 Subject: [PATCH 3/8] add calendar.json endpoint --- open_web_calendar/app.py | 19 ++-- open_web_calendar/conversion_base.py | 42 ++++++--- open_web_calendar/convert_to_dhtmlx.py | 13 ++- open_web_calendar/convert_to_ics.py | 5 +- open_web_calendar/convert_to_metadata.py | 43 +++++++++ open_web_calendar/test/test_date_to_string.py | 4 +- .../test/test_issue_300_html_description.py | 8 +- .../test/test_issue_305_event_style.py | 4 +- .../test/test_issue_406_calendar_info.py | 91 +++++++++++++++++++ .../test/test_issue_406_calendar_name.py | 48 ---------- 10 files changed, 192 insertions(+), 85 deletions(-) create mode 100644 open_web_calendar/convert_to_metadata.py create mode 100644 open_web_calendar/test/test_issue_406_calendar_info.py delete mode 100644 open_web_calendar/test/test_issue_406_calendar_name.py diff --git a/open_web_calendar/app.py b/open_web_calendar/app.py index bca0c76df3..cbf086d2ab 100755 --- a/open_web_calendar/app.py +++ b/open_web_calendar/app.py @@ -27,8 +27,9 @@ from flask_caching import Cache from . import translate, version -from .convert_to_dhtmlx import ConvertToDhtmlx +from .convert_to_dhtmlx import ConvertToDhtmlxEvents from .convert_to_ics import ConvertToICS +from .convert_to_metadata import ConvertToMetadata # configuration DEBUG = os.environ.get("APP_DEBUG", "true").lower() == "true" @@ -209,6 +210,12 @@ def render_app_template(template, specification): language=language, ) +CALENDAR_DISPATCH = { + "events.json": ConvertToDhtmlxEvents, + "json": ConvertToMetadata, + "ics": ConvertToICS, +} + @app.route("/calendar.", methods=["GET", "OPTIONS"]) @limit_hosts(allowed_hosts=ALLOWED_HOSTS) @@ -219,12 +226,9 @@ def get_calendar(ext): specification = get_specification() if ext == "spec": return jsonify(specification) - if ext == "events.json": - strategy = ConvertToDhtmlx(specification, get_text_from_url) - strategy.retrieve_calendars() - return set_js_headers(strategy.merge()) - if ext == "ics": - strategy = ConvertToICS(specification, get_text_from_url) + if ext in CALENDAR_DISPATCH: + Strategy = CALENDAR_DISPATCH[ext] # noqa: N806 + strategy = Strategy(specification, get_text_from_url) strategy.retrieve_calendars() return set_js_headers(strategy.merge()) if ext == "html": @@ -241,7 +245,6 @@ def get_calendar(ext): f"Cannot use extension {ext}. Please see the documentation or report an error." ) - for folder_path in STATIC_FOLDER_PATH.iterdir(): if not folder_path.is_dir(): continue diff --git a/open_web_calendar/conversion_base.py b/open_web_calendar/conversion_base.py index e3cb3f0734..d9403a3ea9 100644 --- a/open_web_calendar/conversion_base.py +++ b/open_web_calendar/conversion_base.py @@ -20,10 +20,12 @@ def get_text_from_url(url): return requests.get(url, timeout=10).text +INDEX_TYPE = tuple[int, int] + class CalendarInfo: """Provide an easy API for calendar information.""" - def __init__(self, index: int, url: str, calendar: Calendar): + def __init__(self, index: INDEX_TYPE, url: str, calendar: Calendar): """Create a new calendar info.""" self._calendar = calendar self._index = index @@ -48,21 +50,32 @@ def calendar(self) -> Calendar: return self._calendar @property - def index(self) -> int: - """The index of the calendar url.""" + def index(self) -> INDEX_TYPE: + """The index of the calendar url. + + Since one URL can have several calendars, this is multiple indices.""" return self._index @property def event_css_classes(self) -> list[str]: """The css classes for all events in this calendar.""" - return [f"CALENDAR-INDEX-{self.index}"] + return [f"CALENDAR-INDEX-{self.index[0]}", f"CALENDAR-INDEX-{self.index[0]}-{self.index[1]}"] - def to_json(self) -> dict: + @property + def id(self) -> str: """Return this calendar information as JSON.""" + return f"{self.index[0]}-{self.index[1]}" + + def to_json(self) -> dict[str, any]: + """Return a JSON compatible version of this information.""" return { - "index": self.index, + "id": self.id, + "type": "calendar", + "url-index": self.index[0], + "calendar-index": self.index[0], "name": self.name, "description": self.description, + "url": self._url, } @@ -130,12 +143,16 @@ def retrieve_calendar(self, index_url): try: index, url = index_url calendars = self.get_calendars_from_url(url) - for calendar in calendars: - self.collect_components_from(CalendarInfo(index, url, calendar)) + for i, calendar in enumerate(calendars): + self.collect_components_from(CalendarInfo((index, i), url, calendar)) except: ty, err, tb = sys.exc_info() - with self.lock: - self.components.append(self.error(ty, err, tb, url)) + self.add_component(self.error(ty, err, tb, url)) + + def add_component(self, component): + """Add a component to the result.""" + with self.lock: + self.components.append(component) def collect_components_from(self, calendar_info: CalendarInfo): """Collect all the compenents from the calendar.""" @@ -145,5 +162,8 @@ def merge(self): """Return the flask Response for the merged calendars.""" raise NotImplementedError("to be implemented in subclasses") + def convert_error(self, err: Exception, url: str, traceback: str): + """Convert an error.""" + raise NotImplementedError("to be implemented in subclasses") -__all__ = ["ConversionStrategy", "get_text_from_url", "CalendarInfo"] +__all__ = ["ConversionStrategy", "get_text_from_url", "CalendarInfo", "INDEX_TYPE"] diff --git a/open_web_calendar/convert_to_dhtmlx.py b/open_web_calendar/convert_to_dhtmlx.py index e733e472a7..8193319c45 100644 --- a/open_web_calendar/convert_to_dhtmlx.py +++ b/open_web_calendar/convert_to_dhtmlx.py @@ -21,7 +21,7 @@ def is_date(date): return isinstance(date, datetime.date) and not isinstance(date, datetime.datetime) -class ConvertToDhtmlx(ConversionStrategy): +class ConvertToDhtmlxEvents(ConversionStrategy): """Convert events to dhtmlx. This conforms to a stratey pattern. - timeshift_minutes is the timeshift specified by the calendar @@ -105,7 +105,7 @@ def convert_ical_event(self, calendar_info: CalendarInfo, calendar_event): "css-classes": ["event"] + self.get_event_classes(calendar_event) + calendar_info.event_css_classes, - "calendar": calendar_info.to_json(), + "calendar": calendar_info.id, } def convert_error(self, error, url, tb_s): @@ -170,10 +170,9 @@ def collect_components_from(self, calendar_info: CalendarInfo): events = recurring_ical_events.of(calendar_info.calendar).between( self.from_date, self.to_date ) - with self.lock: - for event in events: - json_event = self.convert_ical_event(calendar_info, event) - self.components.append(json_event) + for event in events: + json_event = self.convert_ical_event(calendar_info, event) + self.add_component(json_event) def get_event_classes(self, event) -> list[str]: """Return the CSS classes that should be used for the event styles.""" @@ -194,4 +193,4 @@ def get_event_categories(self, event) -> list[str]: return categories.cats if categories is not None else [] -__all__ = ["ConvertToDhtmlx"] +__all__ = ["ConvertToDhtmlxEvents"] diff --git a/open_web_calendar/convert_to_ics.py b/open_web_calendar/convert_to_ics.py index 047c42f380..fe1c98e44e 100644 --- a/open_web_calendar/convert_to_ics.py +++ b/open_web_calendar/convert_to_ics.py @@ -29,13 +29,12 @@ def is_timezone(self, component): def collect_components_from(self, calendar_info: CalendarInfo): for component in calendar_info.calendar.walk(): if self.is_event(component): - with self.lock: - self.components.append(component) + self.add_component(component) if self.is_timezone(component): tzid = component.get("TZID") if tzid and tzid not in self.timezones: with self.lock: - self.components.append(component) + self.add_component(component) self.timezones.add(tzid) def convert_error(self, error, url, tb_s): diff --git a/open_web_calendar/convert_to_metadata.py b/open_web_calendar/convert_to_metadata.py new file mode 100644 index 0000000000..400b00dae0 --- /dev/null +++ b/open_web_calendar/convert_to_metadata.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: 2024 Nicco Kunzmann and Open Web Calendar Contributors +# +# SPDX-License-Identifier: GPL-2.0-only +from __future__ import annotations + +from flask import jsonify + +from .conversion_base import CalendarInfo, ConversionStrategy + + +class ConvertToMetadata(ConversionStrategy): + """Convert the specification into metadata information about the calendars given.""" + + def created(self): + """This instance is created.""" + self.components = { + "calendars": {}, + "errors": {} + } + + def collect_components_from(self, calendar_info: CalendarInfo): + self.add_component(calendar_info.to_json()) + + def add_component(self, component: dict): + with self.lock: + kind = f"{component['type']}s" + self.components[kind][component["id"]] = component + + def convert_error(self, err: Exception, url: str, traceback: str): + """Convert an error.""" + return { + "type": "error", + "id": url, + "url": url, + "traceback": traceback, + "error": str(err), + } + + def merge(self): + """Return the response.""" + return jsonify(self.components) + +__all__ = ["ConvertToMetadata"] diff --git a/open_web_calendar/test/test_date_to_string.py b/open_web_calendar/test/test_date_to_string.py index 825dd57622..2ae3d42054 100644 --- a/open_web_calendar/test/test_date_to_string.py +++ b/open_web_calendar/test/test_date_to_string.py @@ -7,7 +7,7 @@ import pytest from pytz import timezone, utc -from open_web_calendar.app import ConvertToDhtmlx +from open_web_calendar.app import ConvertToDhtmlxEvents berlin = timezone("Europe/Berlin") eastern = timezone("US/Eastern") @@ -55,6 +55,6 @@ ) def test_date_to_string_conversion(date, tz, expected): """Convert dates and datetime objects for the events.json""" - string = ConvertToDhtmlx({"timezone": tz}).date_to_string(date) + string = ConvertToDhtmlxEvents({"timezone": tz}).date_to_string(date) print(date) assert string == expected diff --git a/open_web_calendar/test/test_issue_300_html_description.py b/open_web_calendar/test/test_issue_300_html_description.py index 3ef06cf2c2..8713477406 100644 --- a/open_web_calendar/test/test_issue_300_html_description.py +++ b/open_web_calendar/test/test_issue_300_html_description.py @@ -15,7 +15,7 @@ from icalendar import Calendar, Event -from open_web_calendar.app import ConvertToDhtmlx +from open_web_calendar.app import ConvertToDhtmlxEvents def event_with_uid(calendar_content: str, uid: str) -> Event: @@ -31,7 +31,7 @@ def test_description_is_html(calendar_content): event = event_with_uid( calendar_content["issue-287-links-1"], "1jkma74p98bj79c760660arc07@google.com" ) - description = ConvertToDhtmlx({"timezone": "Europe/London"}).get_event_description( + description = ConvertToDhtmlxEvents({"timezone": "Europe/London"}).get_event_description( event ) assert '\n Know This Heading!\n" in description @@ -52,7 +52,7 @@ def test_description_is_parameter(calendar_content): def test_alt_attribute(calendar_content): """The HTML description is in an alt attribute.""" event = event_with_uid(calendar_content["food"], "2851") - description = ConvertToDhtmlx({"timezone": "Europe/London"}).get_event_description( + description = ConvertToDhtmlxEvents({"timezone": "Europe/London"}).get_event_description( event ) assert "Cauliflower\n
" in description diff --git a/open_web_calendar/test/test_issue_305_event_style.py b/open_web_calendar/test/test_issue_305_event_style.py index 3fd7cead21..e400e4c511 100644 --- a/open_web_calendar/test/test_issue_305_event_style.py +++ b/open_web_calendar/test/test_issue_305_event_style.py @@ -13,7 +13,7 @@ import pytest -from open_web_calendar.app import ConvertToDhtmlx +from open_web_calendar.app import ConvertToDhtmlxEvents class CATEGORY(NamedTuple): @@ -36,7 +36,7 @@ class CATEGORY(NamedTuple): ) def test_get_event_classes(event, expected_classes): """Check that event classes are correctly extracted.""" - dhx = ConvertToDhtmlx({"timezone": "Europe/London"}) + dhx = ConvertToDhtmlxEvents({"timezone": "Europe/London"}) classes = dhx.get_event_classes(event) assert classes == expected_classes diff --git a/open_web_calendar/test/test_issue_406_calendar_info.py b/open_web_calendar/test/test_issue_406_calendar_info.py new file mode 100644 index 0000000000..21c458c12e --- /dev/null +++ b/open_web_calendar/test/test_issue_406_calendar_info.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2024 Nicco Kunzmann and Open Web Calendar Contributors +# +# SPDX-License-Identifier: GPL-2.0-only + +"""Events should know which calendar they are in. + +See https://github.com/niccokunzmann/open-web-calendar/issues/406 + +See https://datatracker.ietf.org/doc/html/rfc7986#section-5.1 +And X-WR-CALNAME is in use, too. +""" + +import pytest +from icalendar import Calendar + +from open_web_calendar.conversion_base import CalendarInfo + + +@pytest.mark.parametrize( + ("file", "name"), + [ + ("calendar-name-rfc-7986", "RFC 7986 compatible calendar"), + ("calendar-x-wr-calname", "old calendar description"), + ("food", "food"), + ], +) +def test_calendar_name_is_known(calendar_content, file, name): + """Check that we can extract the calendar name.""" + cal = CalendarInfo( + (0,0), f"https://localhost/{file}.ics", Calendar.from_ical(calendar_content[file]) + ) + assert cal.name == name + + +@pytest.mark.parametrize( + ("file", "description"), + [ + ("calendar-name-rfc-7986", "This is a later version with attributes"), + ("calendar-x-wr-calname", " This calendar uses non-standard descriptions"), + ("food", ""), + ], +) +def test_calendar_description(calendar_content, file, description): + """Check that we can extract the calendar name.""" + cal = CalendarInfo( + (0,0), f"https://localhost/{file}.ics", Calendar.from_ical(calendar_content[file]) + ) + assert cal.description == description + + +@pytest.mark.parametrize( + ("calendar_file", "attribute", "value"), + [ + ("calendar-name-rfc-7986", "description", "This is a later version with attributes"), + ("calendar-name-rfc-7986", "id", "0-0"), + ("food", "id", "0-0"), + ("calendar-x-wr-calname", "id", "0-0"), + ("calendar-x-wr-calname", "name", "old calendar description"), + ("calendar-x-wr-calname", "url-index", 0), + ] +) +def test_calendar_information(client, calendar_urls, calendar_file,attribute,value): + """Test the the information yielded from the calendars is correct.""" + result = client.get(f"/calendar.json?url={calendar_urls[calendar_file]}").json + print(result) + assert len(result["calendars"]) == 1 + cal = result["calendars"]["0-0"] + assert cal[attribute] == value, f"attribute {attribute} expected to be {value} but found {cal[attribute]}" + + +@pytest.mark.parametrize("index", + [(0,0), (0, 3), (3, 1)]) +def test_event_classes(index, calendar_content): + ci = CalendarInfo( + index, f"https://localhost/file.ics", Calendar.from_ical(calendar_content["food"]) + ) + assert f"CALENDAR-INDEX-{index[0]}" in ci.event_css_classes + assert f"CALENDAR-INDEX-{index[0]}-{index[1]}" in ci.event_css_classes + + +@pytest.mark.parametrize("url", + ["http://localhost:8001/nanan.ics", + "https://abc.com/cal.ics"]) +def test_event_classes(url): + ci = CalendarInfo( + (1,1), url, Calendar() + ) + assert ci.to_json()["url"] == url + + +# TODO: test what happens if a URL does not work. \ No newline at end of file diff --git a/open_web_calendar/test/test_issue_406_calendar_name.py b/open_web_calendar/test/test_issue_406_calendar_name.py deleted file mode 100644 index 0e87fddc3b..0000000000 --- a/open_web_calendar/test/test_issue_406_calendar_name.py +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Nicco Kunzmann and Open Web Calendar Contributors -# -# SPDX-License-Identifier: GPL-2.0-only - -"""Events should know which calendar they are in. - -See https://github.com/niccokunzmann/open-web-calendar/issues/406 - -See https://datatracker.ietf.org/doc/html/rfc7986#section-5.1 -And X-WR-CALNAME is in use, too. -""" - -import pytest -from icalendar import Calendar - -from open_web_calendar.conversion_base import CalendarInfo - - -@pytest.mark.parametrize( - ("file", "name"), - [ - ("calendar-name-rfc-7986", "RFC 7986 compatible calendar"), - ("calendar-x-wr-calname", "old calendar description"), - ("food", "food"), - ], -) -def test_calendar_name_is_known(calendar_content, file, name): - """Check that we can extract the calendar name.""" - cal = CalendarInfo( - 0, f"https://localhost/{file}.ics", Calendar.from_ical(calendar_content[file]) - ) - assert cal.name == name - - -@pytest.mark.parametrize( - ("file", "description"), - [ - ("calendar-name-rfc-7986", "This is a later version with attributes"), - ("calendar-x-wr-calname", " This calendar uses non-standard descriptions"), - ("food", ""), - ], -) -def test_calendar_description(calendar_content, file, description): - """Check that we can extract the calendar name.""" - cal = CalendarInfo( - 0, f"https://localhost/{file}.ics", Calendar.from_ical(calendar_content[file]) - ) - assert cal.description == description From ef7376c257645288499c1ec95deaed59fcf3bd6a Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 22 Jul 2024 19:24:25 +0100 Subject: [PATCH 4/8] format ruff --- open_web_calendar/app.py | 2 + open_web_calendar/conversion_base.py | 9 +++- open_web_calendar/convert_to_metadata.py | 8 ++-- .../test/test_issue_300_html_description.py | 18 ++++---- .../test/test_issue_406_calendar_info.py | 43 +++++++++++-------- 5 files changed, 47 insertions(+), 33 deletions(-) diff --git a/open_web_calendar/app.py b/open_web_calendar/app.py index cbf086d2ab..bc4b3999a1 100755 --- a/open_web_calendar/app.py +++ b/open_web_calendar/app.py @@ -210,6 +210,7 @@ def render_app_template(template, specification): language=language, ) + CALENDAR_DISPATCH = { "events.json": ConvertToDhtmlxEvents, "json": ConvertToMetadata, @@ -245,6 +246,7 @@ def get_calendar(ext): f"Cannot use extension {ext}. Please see the documentation or report an error." ) + for folder_path in STATIC_FOLDER_PATH.iterdir(): if not folder_path.is_dir(): continue diff --git a/open_web_calendar/conversion_base.py b/open_web_calendar/conversion_base.py index d9403a3ea9..7737c0ad16 100644 --- a/open_web_calendar/conversion_base.py +++ b/open_web_calendar/conversion_base.py @@ -22,6 +22,7 @@ def get_text_from_url(url): INDEX_TYPE = tuple[int, int] + class CalendarInfo: """Provide an easy API for calendar information.""" @@ -52,14 +53,17 @@ def calendar(self) -> Calendar: @property def index(self) -> INDEX_TYPE: """The index of the calendar url. - + Since one URL can have several calendars, this is multiple indices.""" return self._index @property def event_css_classes(self) -> list[str]: """The css classes for all events in this calendar.""" - return [f"CALENDAR-INDEX-{self.index[0]}", f"CALENDAR-INDEX-{self.index[0]}-{self.index[1]}"] + return [ + f"CALENDAR-INDEX-{self.index[0]}", + f"CALENDAR-INDEX-{self.index[0]}-{self.index[1]}", + ] @property def id(self) -> str: @@ -166,4 +170,5 @@ def convert_error(self, err: Exception, url: str, traceback: str): """Convert an error.""" raise NotImplementedError("to be implemented in subclasses") + __all__ = ["ConversionStrategy", "get_text_from_url", "CalendarInfo", "INDEX_TYPE"] diff --git a/open_web_calendar/convert_to_metadata.py b/open_web_calendar/convert_to_metadata.py index 400b00dae0..c94c501739 100644 --- a/open_web_calendar/convert_to_metadata.py +++ b/open_web_calendar/convert_to_metadata.py @@ -13,10 +13,7 @@ class ConvertToMetadata(ConversionStrategy): def created(self): """This instance is created.""" - self.components = { - "calendars": {}, - "errors": {} - } + self.components = {"calendars": {}, "errors": {}} def collect_components_from(self, calendar_info: CalendarInfo): self.add_component(calendar_info.to_json()) @@ -35,9 +32,10 @@ def convert_error(self, err: Exception, url: str, traceback: str): "traceback": traceback, "error": str(err), } - + def merge(self): """Return the response.""" return jsonify(self.components) + __all__ = ["ConvertToMetadata"] diff --git a/open_web_calendar/test/test_issue_300_html_description.py b/open_web_calendar/test/test_issue_300_html_description.py index 8713477406..1a0d917f58 100644 --- a/open_web_calendar/test/test_issue_300_html_description.py +++ b/open_web_calendar/test/test_issue_300_html_description.py @@ -31,9 +31,9 @@ def test_description_is_html(calendar_content): event = event_with_uid( calendar_content["issue-287-links-1"], "1jkma74p98bj79c760660arc07@google.com" ) - description = ConvertToDhtmlxEvents({"timezone": "Europe/London"}).get_event_description( - event - ) + description = ConvertToDhtmlxEvents( + {"timezone": "Europe/London"} + ).get_event_description(event) assert '
\n Know This Heading!\n" in description def test_alt_attribute(calendar_content): """The HTML description is in an alt attribute.""" event = event_with_uid(calendar_content["food"], "2851") - description = ConvertToDhtmlxEvents({"timezone": "Europe/London"}).get_event_description( - event - ) + description = ConvertToDhtmlxEvents( + {"timezone": "Europe/London"} + ).get_event_description(event) assert "Cauliflower\n
" in description diff --git a/open_web_calendar/test/test_issue_406_calendar_info.py b/open_web_calendar/test/test_issue_406_calendar_info.py index 21c458c12e..cdbc33befc 100644 --- a/open_web_calendar/test/test_issue_406_calendar_info.py +++ b/open_web_calendar/test/test_issue_406_calendar_info.py @@ -27,7 +27,9 @@ def test_calendar_name_is_known(calendar_content, file, name): """Check that we can extract the calendar name.""" cal = CalendarInfo( - (0,0), f"https://localhost/{file}.ics", Calendar.from_ical(calendar_content[file]) + (0, 0), + f"https://localhost/{file}.ics", + Calendar.from_ical(calendar_content[file]), ) assert cal.name == name @@ -43,7 +45,9 @@ def test_calendar_name_is_known(calendar_content, file, name): def test_calendar_description(calendar_content, file, description): """Check that we can extract the calendar name.""" cal = CalendarInfo( - (0,0), f"https://localhost/{file}.ics", Calendar.from_ical(calendar_content[file]) + (0, 0), + f"https://localhost/{file}.ics", + Calendar.from_ical(calendar_content[file]), ) assert cal.description == description @@ -51,41 +55,46 @@ def test_calendar_description(calendar_content, file, description): @pytest.mark.parametrize( ("calendar_file", "attribute", "value"), [ - ("calendar-name-rfc-7986", "description", "This is a later version with attributes"), + ( + "calendar-name-rfc-7986", + "description", + "This is a later version with attributes", + ), ("calendar-name-rfc-7986", "id", "0-0"), ("food", "id", "0-0"), ("calendar-x-wr-calname", "id", "0-0"), ("calendar-x-wr-calname", "name", "old calendar description"), ("calendar-x-wr-calname", "url-index", 0), - ] + ], ) -def test_calendar_information(client, calendar_urls, calendar_file,attribute,value): +def test_calendar_information(client, calendar_urls, calendar_file, attribute, value): """Test the the information yielded from the calendars is correct.""" result = client.get(f"/calendar.json?url={calendar_urls[calendar_file]}").json print(result) assert len(result["calendars"]) == 1 cal = result["calendars"]["0-0"] - assert cal[attribute] == value, f"attribute {attribute} expected to be {value} but found {cal[attribute]}" + assert ( + cal[attribute] == value + ), f"attribute {attribute} expected to be {value} but found {cal[attribute]}" -@pytest.mark.parametrize("index", - [(0,0), (0, 3), (3, 1)]) +@pytest.mark.parametrize("index", [(0, 0), (0, 3), (3, 1)]) def test_event_classes(index, calendar_content): ci = CalendarInfo( - index, f"https://localhost/file.ics", Calendar.from_ical(calendar_content["food"]) + index, + "https://localhost/file.ics", + Calendar.from_ical(calendar_content["food"]), ) assert f"CALENDAR-INDEX-{index[0]}" in ci.event_css_classes assert f"CALENDAR-INDEX-{index[0]}-{index[1]}" in ci.event_css_classes -@pytest.mark.parametrize("url", - ["http://localhost:8001/nanan.ics", - "https://abc.com/cal.ics"]) -def test_event_classes(url): - ci = CalendarInfo( - (1,1), url, Calendar() - ) +@pytest.mark.parametrize( + "url", ["http://localhost:8001/nanan.ics", "https://abc.com/cal.ics"] +) +def test_calendar_info_url(url): + ci = CalendarInfo((1, 1), url, Calendar()) assert ci.to_json()["url"] == url -# TODO: test what happens if a URL does not work. \ No newline at end of file +# TODO: test what happens if a URL does not work. From 03e188adda77015e5fc6d626a7b061704356d55a Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 22 Jul 2024 20:11:03 +0100 Subject: [PATCH 5/8] Add method to get calendar info in JS --- open_web_calendar/static/js/common.js | 32 +++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/open_web_calendar/static/js/common.js b/open_web_calendar/static/js/common.js index e4d2b9ba16..598c272372 100644 --- a/open_web_calendar/static/js/common.js +++ b/open_web_calendar/static/js/common.js @@ -7,6 +7,7 @@ */ const DEFAULT_URL = document.location.protocol + "//" + document.location.host; const CALENDAR_ENDPOINT = "/calendar.html"; +const CALENDAR_INFO_ENDPOINT = "/calendar.json"; /* Return the properties of an object. @@ -57,8 +58,8 @@ function fillTimezoneUIElements(defaultTimeZone) { } } -function getCalendarUrl(specification) { - var url = DEFAULT_URL + CALENDAR_ENDPOINT + "?"; +function getCalendarUrl(specification, calendarEndpoint) { + var url = DEFAULT_URL + (calendarEndpoint ? calendarEndpoint : CALENDAR_ENDPOINT) + "?"; var parameters = []; getOwnProperties(specification).forEach(function(property) { (Array.isArray(specification[property]) ? specification[property].length ? specification[property] : [""] : [specification[property]] @@ -68,3 +69,30 @@ function getCalendarUrl(specification) { }); return url + parameters.join("&"); } + +/* Get the calendar information. */ +function getCalendarInfo(onSuccess, spec) { + var requestSpec = spec ? spec : specification; + var endpoint = getCalendarUrl(requestSpec, CALENDAR_INFO_ENDPOINT); + + // from https://developer.mozilla.org/en-US/docs/Web/API/Request/json + //const request = new Request(endpoint, {method: "GET"}); + //console.log("GET " + endpoint); + //return request; + // from https://www.w3schools.com/js/js_json_http.asp + var xmlhttp = new XMLHttpRequest(); + var url = endpoint; + + xmlhttp.onreadystatechange = function() { + if (this.readyState == 4) { + if (this.status == 200) { + var value = JSON.parse(this.responseText); + onSuccess(value); + } else { + // TODO: report error + } + } + }; + xmlhttp.open("GET", url, true); + xmlhttp.send(); +} From 5387cc499be5678aa905cc0b07289b3460090503 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 22 Jul 2024 20:26:39 +0100 Subject: [PATCH 6/8] Add calendar list button to top header of calendar --- open_web_calendar/static/js/configure.js | 3 +++ open_web_calendar/templates/index.html | 9 +++++---- open_web_calendar/translate.py | 1 + open_web_calendar/translations/en/calendar.yml | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/open_web_calendar/static/js/configure.js b/open_web_calendar/static/js/configure.js index fecf9b57a6..0f437bace4 100644 --- a/open_web_calendar/static/js/configure.js +++ b/open_web_calendar/static/js/configure.js @@ -220,6 +220,7 @@ function getHeader() { "month": specification.tabs.includes("month"), "today": specification.controls.includes("today"), "agenda": specification.tabs.includes("agenda"), + "calendars": specification.controls.includes("calendars"), } function showSelected(headerElements) { return headerElements.filter(function(element){ @@ -235,6 +236,7 @@ function getHeader() { cols: showSelected([ "prev", "date", + "calendars", "next", ]) }, @@ -257,6 +259,7 @@ function getHeader() { "month", "agenda", "date", + "calendars", "prev", "today", "next" diff --git a/open_web_calendar/templates/index.html b/open_web_calendar/templates/index.html index cd78bdecec..c326aedc7c 100644 --- a/open_web_calendar/templates/index.html +++ b/open_web_calendar/templates/index.html @@ -246,10 +246,11 @@

{{ html("tabs-title") }}

- - - - + + + + +

{{ html("timezone-title") }}

diff --git a/open_web_calendar/translate.py b/open_web_calendar/translate.py index 0ed133f8c9..8a67d16ac5 100644 --- a/open_web_calendar/translate.py +++ b/open_web_calendar/translate.py @@ -151,6 +151,7 @@ def html(language: str, file: str, tid: str, **template_replacements) -> str: "repeat_text_occurences_count", "repeat_radio_end2", "repeat_radio_end3", + "calendars", ] diff --git a/open_web_calendar/translations/en/calendar.yml b/open_web_calendar/translations/en/calendar.yml index 99f86d375e..28c59811b0 100644 --- a/open_web_calendar/translations/en/calendar.yml +++ b/open_web_calendar/translations/en/calendar.yml @@ -53,6 +53,7 @@ labels_month: "Month" labels_day: "Day" labels_hour: "Hour" labels_minute: "Minute" +labels_calendars: "Calendar List" # Translate this as the language you are working on. # This appears for the users to choose the language. From 783e564d1c832ab962b8a294e55f01a93fe19781 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Tue, 23 Jul 2024 20:13:12 +0100 Subject: [PATCH 7/8] Add toggle menu for the calendars --- open_web_calendar/conversion_base.py | 1 + open_web_calendar/static/css/dhtmlx/style.css | 19 +++++ open_web_calendar/static/js/configure.js | 81 ++++++++++++++++++- .../translations/en/calendar.yml | 2 +- 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/open_web_calendar/conversion_base.py b/open_web_calendar/conversion_base.py index 7737c0ad16..57c198531d 100644 --- a/open_web_calendar/conversion_base.py +++ b/open_web_calendar/conversion_base.py @@ -80,6 +80,7 @@ def to_json(self) -> dict[str, any]: "name": self.name, "description": self.description, "url": self._url, + "event-css-classes": self.event_css_classes, } diff --git a/open_web_calendar/static/css/dhtmlx/style.css b/open_web_calendar/static/css/dhtmlx/style.css index 6812bb2e5f..0bd46a592b 100644 --- a/open_web_calendar/static/css/dhtmlx/style.css +++ b/open_web_calendar/static/css/dhtmlx/style.css @@ -120,3 +120,22 @@ div.dhx_agenda_line > span { .dhx_cal_qi_tcontent a { color: inherit; } + + + +/* The container
- needed to position the dropdown content */ +.dropdown { + position: relative; + display: inline-block; + /*background-color: white; /* TODO: use that of the skin */ +} + +/* Dropdown Content (Hidden by Default) */ +.dropdown-content { + position: absolute; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; + text-transform: initial; +} + diff --git a/open_web_calendar/static/js/configure.js b/open_web_calendar/static/js/configure.js index 0f437bace4..14788665b7 100644 --- a/open_web_calendar/static/js/configure.js +++ b/open_web_calendar/static/js/configure.js @@ -224,9 +224,17 @@ function getHeader() { } function showSelected(headerElements) { return headerElements.filter(function(element){ - return useHeaderElement[element] != false; // null for absent + return useHeaderElement[element.id || element] != false; // null for absent }); } + var calendars = { + id:"calendars", + html: ( + "" + ), + click:toggleCalendars}; // switch the header to a compact one // see https://docs.dhtmlx.com/scheduler/touch_support.html if (window.innerWidth < Number.parseInt(specification.compact_layout_width)) { @@ -236,7 +244,6 @@ function getHeader() { cols: showSelected([ "prev", "date", - "calendars", "next", ]) }, @@ -247,6 +254,7 @@ function getHeader() { "month", "agenda", "spacer", + calendars, "today" ]) } @@ -259,7 +267,7 @@ function getHeader() { "month", "agenda", "date", - "calendars", + calendars, "prev", "today", "next" @@ -267,6 +275,7 @@ function getHeader() { } } + function resetConfig() { scheduler.config.header = getHeader(); return true; @@ -304,6 +313,9 @@ function loadCalendar() { scheduler.config.responsive_lightbox = true; resetConfig(); scheduler.attachEvent("onBeforeViewChange", resetConfig); + scheduler.attachEvent("onAfterViewChange", function() { + resetConfigWithCalendarInfo(); + }); scheduler.attachEvent("onSchedulerResize", resetConfig); // we do not allow changes to the source calendar @@ -410,6 +422,7 @@ function loadCalendar() { //dp.init(scheduler); setLoader(); + requestCalendarInfo(); } /* Agenda view @@ -465,4 +478,66 @@ scheduler.attachEvent("onBeforeViewChange", function(old_mode, old_date, mode, d return true; }); + +/* Request the inform us about the current calendars in use. + * + * info is from /calendars.json + * { + "calendars": { + "0-0": { + "calendar-index": 0, + "description": "", + "id": "0-0", + "name": "gancio.antroposofiachile.net", + "type": "calendar", + "url": "http://localhost:8001/gancio.antroposofiachile.net.ics", + "url-index": 0 + } + }, + "errors": {} + } + */ + +function resetConfigWithCalendarInfo() { + // do nothing. This will be replaced. +} + +function requestCalendarInfo() { + getCalendarInfo(function (info) { + resetConfigWithCalendarInfo = function() { + console.log("resetConfigWithCalendarInfo"); + updateCalendarsLegend(info); + }; + resetConfigWithCalendarInfo(); + }); +} + +/* Update the calendars legend info in the header. */ +function updateCalendarsLegend(info) { + var calendars = document.getElementById("calendars-list"); + if (calendars) { + calendars.innerHTML = ""; + getOwnProperties(info.calendars).forEach(function(calendarId){ + var calendarInfo = info.calendars[calendarId]; + var cal = document.createElement("div"); + cal.innerText = calendarInfo.name; + calendarInfo["event-css-classes"].forEach( + function(t) { + cal.classList.add(t) + } + ); + cal.classList.add("dhx_cal_event_line"); + calendars.appendChild(cal); + }); + } +} + +function toggleCalendars() { + resetConfigWithCalendarInfo(); + var calendars = document.getElementById("calendars-list"); + if (calendars) { + calendars.classList.toggle("hidden"); + } +} + window.addEventListener("load", loadCalendar); diff --git a/open_web_calendar/translations/en/calendar.yml b/open_web_calendar/translations/en/calendar.yml index 28c59811b0..ee09accdc9 100644 --- a/open_web_calendar/translations/en/calendar.yml +++ b/open_web_calendar/translations/en/calendar.yml @@ -53,7 +53,7 @@ labels_month: "Month" labels_day: "Day" labels_hour: "Hour" labels_minute: "Minute" -labels_calendars: "Calendar List" +labels_calendars: "Calendars" # Translate this as the language you are working on. # This appears for the users to choose the language. From f198173fc23c8f743100036fbbb49dfb6abcf8e9 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Tue, 23 Jul 2024 20:16:02 +0100 Subject: [PATCH 8/8] py38 compatibility --- open_web_calendar/conversion_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/open_web_calendar/conversion_base.py b/open_web_calendar/conversion_base.py index 57c198531d..7bdebbc6f2 100644 --- a/open_web_calendar/conversion_base.py +++ b/open_web_calendar/conversion_base.py @@ -8,6 +8,7 @@ import traceback from concurrent.futures import ThreadPoolExecutor from threading import RLock +from typing import Tuple from urllib.parse import urljoin import requests @@ -20,7 +21,7 @@ def get_text_from_url(url): return requests.get(url, timeout=10).text -INDEX_TYPE = tuple[int, int] +INDEX_TYPE = Tuple[int, int] class CalendarInfo: