Release v0.1.5

This commit is contained in:
2026-07-07 15:49:06 +02:00
parent 3bcb04ff76
commit b328a6d649
21 changed files with 5718 additions and 714 deletions

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
import unittest
from datetime import datetime, timezone
from types import SimpleNamespace
from govoplan_calendar.backend.ical import event_to_ics, parse_vevent
from govoplan_calendar.backend.ical import expand_event_occurrences, event_to_ics, parse_vevent
class ICalendarParsingTests(unittest.TestCase):
@@ -48,6 +50,154 @@ END:VCALENDAR
self.assertTrue(event["all_day"])
self.assertEqual(event["start_at"].hour, 0)
def test_parse_advanced_vevent_fields_and_valarm(self) -> None:
event = parse_vevent(
"""BEGIN:VCALENDAR
VERSION:2.0
METHOD:REQUEST
BEGIN:VEVENT
UID:event-advanced@example.test
DTSTAMP:20260707T080000Z
CREATED:20260707T070000Z
LAST-MODIFIED:20260707T073000Z
DTSTART;TZID=Europe/Berlin:20260708T090000
DURATION:PT1H30M
SUMMARY:Planning
DESCRIPTION:Line one\\nLine two
LOCATION:Room\\; 1
PRIORITY:5
URL:https://example.test/events/event-advanced
GEO:52.5200;13.4050
COMMENT:First comment
RESOURCES:Projector,Room
REQUEST-STATUS:2.0;Success
ORGANIZER;CN=Orga:mailto:orga@example.test
ATTENDEE;CN=Ada;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED:mailto:ada@example.test
RRULE:FREQ=WEEKLY;COUNT=2
EXDATE;TZID=Europe/Berlin:20260715T090000
RDATE;TZID=Europe/Berlin:20260722T090000
ATTACH;FMTTYPE=application/pdf:https://example.test/a.pdf
RELATED-TO;RELTYPE=PARENT:parent@example.test
X-GOVOPLAN-CUSTOM:kept
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Reminder
TRIGGER:-PT15M
END:VALARM
END:VEVENT
END:VCALENDAR
"""
)
self.assertEqual(event["start_at"], datetime(2026, 7, 8, 7, 0, tzinfo=timezone.utc))
self.assertEqual(event["end_at"], datetime(2026, 7, 8, 8, 30, tzinfo=timezone.utc))
self.assertEqual(event["duration_seconds"], 5400)
self.assertEqual(event["description"], "Line one\nLine two")
self.assertEqual(event["location"], "Room; 1")
self.assertEqual(event["timezone"], "Europe/Berlin")
self.assertEqual(event["organizer"]["params"]["CN"], ["Orga"])
self.assertEqual(event["attendees"][0]["params"]["PARTSTAT"], ["ACCEPTED"])
self.assertEqual(event["icalendar"]["method"], "REQUEST")
self.assertEqual(event["icalendar"]["standard"]["priority"]["value"], "5")
self.assertEqual(event["icalendar"]["standard"]["url"]["value"], "https://example.test/events/event-advanced")
self.assertEqual(event["reminders"][0]["trigger"]["value"], "-PT15M")
self.assertEqual(event["reminders"][0]["trigger"]["decoded"], -900)
property_names = [item["name"] for item in event["icalendar"]["properties"]]
self.assertIn("REQUEST-STATUS", property_names)
self.assertIn("X-GOVOPLAN-CUSTOM", property_names)
def test_generated_ics_preserves_unmapped_properties_and_alarms(self) -> None:
event = SimpleNamespace(
uid="event-export@example.test",
start_at=datetime(2026, 7, 8, 7, 0, tzinfo=timezone.utc),
end_at=datetime(2026, 7, 8, 8, 0, tzinfo=timezone.utc),
duration_seconds=None,
all_day=False,
timezone="Europe/Berlin",
summary="Generated",
sequence=4,
status="CONFIRMED",
transparency="OPAQUE",
classification="PUBLIC",
description=None,
location=None,
categories=["GovOPlaN"],
rrule={"FREQ": "WEEKLY", "COUNT": "2"},
organizer={"value": "mailto:orga@example.test", "params": {"CN": ["Orga"]}},
attendees=[{"value": "mailto:ada@example.test", "params": {"CN": ["Ada"], "ROLE": ["REQ-PARTICIPANT"]}}],
rdate=[],
exdate=[],
reminders=[],
attachments=[{"name": "ATTACH", "value": "https://example.test/a.pdf", "params": {"FMTTYPE": ["application/pdf"]}}],
related_to=[{"name": "RELATED-TO", "value": "parent@example.test", "params": {"RELTYPE": ["PARENT"]}}],
icalendar={
"method": "PUBLISH",
"properties": [
{"name": "URL", "value": "https://example.test/events/event-export", "params": {}},
{"name": "X-GOVOPLAN-CUSTOM", "value": "kept", "params": {}},
{"name": "SUMMARY", "value": "Stale summary", "params": {}},
],
"alarms": [
{
"component": "VALARM",
"properties": [
{"name": "ACTION", "value": "DISPLAY", "params": {}},
{"name": "DESCRIPTION", "value": "Reminder", "params": {}},
{"name": "TRIGGER", "value": "-PT15M", "params": {}},
],
}
],
},
)
ics = event_to_ics(event)
self.assertIn("METHOD:PUBLISH", ics)
self.assertIn("DTSTART;TZID=Europe/Berlin:20260708T090000", ics)
self.assertIn("SUMMARY:Generated", ics)
self.assertNotIn("SUMMARY:Stale summary", ics)
self.assertIn("RRULE:FREQ=WEEKLY;COUNT=2", ics)
self.assertIn("ATTACH;FMTTYPE=application/pdf:https://example.test/a.pdf", ics)
self.assertIn("RELATED-TO;RELTYPE=PARENT:parent@example.test", ics)
self.assertIn("URL:https://example.test/events/event-export", ics)
self.assertIn("X-GOVOPLAN-CUSTOM:kept", ics)
self.assertIn("BEGIN:VALARM", ics)
self.assertIn("TRIGGER:-PT15M", ics)
def test_expand_event_occurrences_applies_rrule_rdate_and_exdate(self) -> None:
parsed = parse_vevent(
"""BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event-recur@example.test
DTSTART;TZID=Europe/Berlin:20260708T090000
DTEND;TZID=Europe/Berlin:20260708T100000
SUMMARY:Recurring
RRULE:FREQ=WEEKLY;COUNT=3
EXDATE;TZID=Europe/Berlin:20260715T090000
RDATE;TZID=Europe/Berlin:20260729T090000
END:VEVENT
END:VCALENDAR
"""
)
event = SimpleNamespace(**parsed)
occurrences = expand_event_occurrences(
event,
datetime(2026, 7, 1, tzinfo=timezone.utc),
datetime(2026, 7, 31, 23, 59, tzinfo=timezone.utc),
)
self.assertEqual(
[item["start_at"] for item in occurrences],
[
datetime(2026, 7, 8, 7, 0, tzinfo=timezone.utc),
datetime(2026, 7, 22, 7, 0, tzinfo=timezone.utc),
datetime(2026, 7, 29, 7, 0, tzinfo=timezone.utc),
],
)
self.assertEqual(occurrences[0]["end_at"], datetime(2026, 7, 8, 8, 0, tzinfo=timezone.utc))
def test_generated_ics_contains_required_vevent_fields(self) -> None:
class Event:
uid = "event-3@example.test"