Skip to content

Commit

Permalink
Merge pull request #629 from niccokunzmann/issue-466-add-mergecal
Browse files Browse the repository at this point in the history
Issue 466 add mergecal
  • Loading branch information
niccokunzmann authored Feb 7, 2025
2 parents 57bf396 + b180499 commit 07d2a08
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 23 deletions.
29 changes: 9 additions & 20 deletions open_web_calendar/convert_to_ics.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from flask import Response
from icalendar import Calendar, Event, Timezone
from icalendar.prop import vDDDTypes
from mergecal import merge_calendars

from .conversion_base import ConversionStrategy

Expand All @@ -27,17 +28,8 @@ def is_timezone(self, component):
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):
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)
with self.lock:
self.components.extend(calendars)

def convert_error(self, error, url, tb_s):
"""Create an error which can be used by the dhtmlx scheduler."""
Expand All @@ -48,20 +40,17 @@ def convert_error(self, error, url, tb_s):
event["UID"] = "error" + str(id(error))
if url:
event["URL"] = url
return event

def create_calendar(self):
calendar = Calendar()
calendar.add_component(event)
return calendar

def merge(self):
calendar = merge_calendars(self.components + [Calendar()])
calendar["VERSION"] = "2.0"
calendar["PRODID"] = "open-web-calendar"
calendar["CALSCALE"] = "GREGORIAN"
calendar["METHOD"] = "PUBLISH"
calendar["X-WR-CALNAME"] = self.title
calendar["NAME"] = self.title
calendar["X-PROD-SOURCE"] = self.specification["source_code"]
return calendar

def merge(self):
calendar = self.create_calendar()
for event in self.components:
calendar.add_component(event)
return Response(calendar.to_ical(), mimetype="text/calendar")
28 changes: 27 additions & 1 deletion open_web_calendar/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@

import sys
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Callable, Optional
from unittest.mock import Mock

import icalendar
import pytest
import requests

Expand Down Expand Up @@ -112,3 +113,28 @@ def calendar_content():
if file.suffix.lower() == ".ics":
mapping[file.stem] = content
return mapping


@pytest.fixture
def merged(
client, calendar_urls
) -> Callable[[Optional[list[str]], Optional[dict[str, str]]], icalendar.Calendar]:
"""Return a function to get a parsed calendar that is merged according to spec."""

def _merged_calendars(
urls: Optional[list[str]] = None, specification: Optional[dict[str, str]] = None
) -> icalendar.Calendar:
"""Return the merged ICS calendar."""
urls = urls or []
specification = specification or {}
query = "?"
for url in urls:
query += f"url={calendar_urls[url]}&"
for k, v in specification.items():
query += f"{k}={v}&"
response = client.get(f"/calendar.ics{query}")
assert response.status_code == 200
print(response.data.decode("utf-8"))
return icalendar.Calendar.from_ical(response.data)

return _merged_calendars
78 changes: 78 additions & 0 deletions open_web_calendar/test/test_issue_466_merge_calendars_into_ics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# SPDX-FileCopyrightText: 2024 Nicco Kunzmann and Open Web Calendar Contributors <https://open-web-calendar.quelltext.eu/>
#
# SPDX-License-Identifier: GPL-2.0-only

"""Test the merging of calendars into .ics files."""

from typing import TYPE_CHECKING

import pytest

if TYPE_CHECKING:
from icalendar import Calendar


@pytest.mark.parametrize(
("attr", "expected_value", "spec"),
[
("VERSION", "2.0", {}),
("PRODID", "open-web-calendar", {}),
("CALSCALE", "GREGORIAN", {}),
("X-WR-CALNAME", "My Calendar", {"title": "My Calendar"}),
("X-WR-CALNAME", "Company Calendar", {"title": "Company Calendar"}),
("NAME", "My Calendar", {"title": "My Calendar"}),
("NAME", "Company Calendar", {"title": "Company Calendar"}),
("X-PROD-SOURCE", "http://my-code", {"source_code": "http://my-code"}),
("X-PROD-SOURCE", "XXX", {"source_code": "XXX"}),
],
)
def test_default_parameters(attr, expected_value, spec, merged):
"""Check that the parameters are set in the merged calendar."""
cal: Calendar = merged([], spec)
assert cal[attr] == expected_value


def test_empty_calendar_has_no_events(merged):
"""No events in an empty calendar."""
assert merged().events == []


def test_get_events_from_one_calendar(merged):
"""All events should be in the calendar."""
cal: Calendar = merged(["food.ics"])
assert len(cal.events) == 132


def test_get_events_from_two_calendars(merged):
"""All events should be in the calendar."""
cal: Calendar = merged(["food.ics", "one-event.ics"])
assert len(cal.events) == 133


def test_timezone_is_included(merged):
"""The timezone should be included."""
cal: Calendar = merged(["one-event.ics"])
assert len(cal.timezones) == 1
assert cal.timezones[0].tz_name == "Europe/Berlin"


def test_url_property(merged):
"""Purpose: This property may be used to convey a location where a more
dynamic rendition of the calendar information can be found.
https://www.rfc-editor.org/rfc/rfc7986.html#section-5.5
"""
pytest.skip("TODO")


def test_source_property(merged):
"""Description: This property identifies a location where a client can
retrieve updated data for the calendar. Clients SHOULD honor any
specified "REFRESH-INTERVAL" value when periodically retrieving
data. Note that this property differs from the "URL" property in
that "URL" is meant to provide an alternative representation of
the calendar data rather than the original location of the data.
https://www.rfc-editor.org/rfc/rfc7986.html#section-5.8
"""
pytest.skip("TODO")
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ beautifulsoup4
lxml_html_clean
pytz
icalendar-compatibility>=0.1.3
mergecal>=0.4.2
28 changes: 26 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ charset-normalizer==3.4.1
click==8.1.8
# via
# flask
# typer
# x-wr-timezone
flask==3.1.0
# via
Expand All @@ -33,6 +34,7 @@ icalendar==6.1.1
# via
# -r requirements.in
# icalendar-compatibility
# mergecal
# recurring-ical-events
# x-wr-timezone
icalendar-compatibility==0.1.4
Expand All @@ -49,12 +51,20 @@ lxml==5.3.0
# lxml-html-clean
lxml-html-clean==0.4.1
# via -r requirements.in
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
# via
# jinja2
# werkzeug
mdurl==0.1.2
# via markdown-it-py
mergecal==0.5.0
# via -r requirements.in
packaging==24.2
# via gunicorn
pygments==2.19.1
# via rich
pysocks==1.7.1
# via requests
python-dateutil==2.9.0.post0
Expand All @@ -69,10 +79,22 @@ recurring-ical-events==3.4.1
# via -r requirements.in
requests[socks]==2.32.3
# via -r requirements.in
rich==13.9.4
# via
# mergecal
# typer
shellingham==1.5.4
# via typer
six==1.17.0
# via python-dateutil
soupsieve==2.6
# via beautifulsoup4
typer==0.15.1
# via mergecal
typing-extensions==4.12.2
# via
# beautifulsoup4
# typer
tzdata==2025.1
# via
# icalendar
Expand All @@ -82,5 +104,7 @@ urllib3==2.3.0
# via requests
werkzeug==3.1.3
# via flask
x-wr-timezone==2.0.0
# via recurring-ical-events
x-wr-timezone==2.0.1
# via
# mergecal
# recurring-ical-events

0 comments on commit 07d2a08

Please sign in to comment.