- Introduced a new database model for calendar subscription tokens, allowing users to generate unique tokens for accessing their personal calendar. - Implemented API endpoint to return ICS files containing only the subscribing user's duties, enhancing user experience with personalized calendar access. - Added utility functions for generating ICS files from user duties, ensuring proper formatting and timezone handling. - Updated command handlers to support the new calendar link feature, providing users with easy access to their personal calendar subscriptions. - Included unit tests for the new functionality, ensuring reliability and correctness of token generation and ICS file creation.
58 lines
1.9 KiB
Python
58 lines
1.9 KiB
Python
"""Generate ICS calendar containing only one user's duties (for subscription link)."""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from icalendar import Calendar, Event
|
|
|
|
from duty_teller.db.models import Duty
|
|
|
|
# Summary labels by event_type (duty | unavailable | vacation)
|
|
SUMMARY_BY_TYPE: dict[str, str] = {
|
|
"duty": "Duty",
|
|
"unavailable": "Unavailable",
|
|
"vacation": "Vacation",
|
|
}
|
|
|
|
|
|
def _parse_utc_iso(iso_str: str) -> datetime:
|
|
"""Parse ISO 8601 UTC string (e.g. 2025-01-15T09:00:00Z) to timezone-aware datetime."""
|
|
s = iso_str.strip().rstrip("Z")
|
|
if "Z" in s:
|
|
s = s.replace("Z", "+00:00")
|
|
else:
|
|
s = s + "+00:00"
|
|
return datetime.fromisoformat(s)
|
|
|
|
|
|
def build_personal_ics(duties_with_name: list[tuple[Duty, str]]) -> bytes:
|
|
"""
|
|
Build a single VCALENDAR with one VEVENT per duty.
|
|
duties_with_name: list of (Duty, full_name); full_name is unused for SUMMARY
|
|
if we use event_type only; can be used later for DESCRIPTION.
|
|
"""
|
|
cal = Calendar()
|
|
cal.add("prodid", "-//Duty Teller//Personal Calendar//EN")
|
|
cal.add("version", "2.0")
|
|
cal.add("calscale", "GREGORIAN")
|
|
|
|
for duty, _full_name in duties_with_name:
|
|
event = Event()
|
|
start_dt = _parse_utc_iso(duty.start_at)
|
|
end_dt = _parse_utc_iso(duty.end_at)
|
|
# Ensure timezone-aware for icalendar
|
|
if start_dt.tzinfo is None:
|
|
start_dt = start_dt.replace(tzinfo=timezone.utc)
|
|
if end_dt.tzinfo is None:
|
|
end_dt = end_dt.replace(tzinfo=timezone.utc)
|
|
event.add("dtstart", start_dt)
|
|
event.add("dtend", end_dt)
|
|
summary = SUMMARY_BY_TYPE.get(
|
|
duty.event_type if duty.event_type else "duty", "Duty"
|
|
)
|
|
event.add("summary", summary)
|
|
event.add("uid", f"duty-{duty.id}@duty-teller")
|
|
event.add("dtstamp", datetime.now(timezone.utc))
|
|
cal.add_component(event)
|
|
|
|
return cal.to_ical()
|