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: (
+ "
" +
+ escapeHtml(scheduler.locale.labels.calendars) +
+ "
"
+ ),
+ 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: