All checks were successful
CI / lint-and-test (push) Successful in 17s
- Created a new `CHANGELOG.md` file to document all notable changes to the project, adhering to the Keep a Changelog format. - Updated `CONTRIBUTING.md` to include instructions for building and previewing documentation using MkDocs. - Added `mkdocs.yml` configuration for documentation generation, including navigation structure and theme settings. - Enhanced various documentation files, including API reference, architecture overview, configuration reference, and runbook, to provide comprehensive guidance for users and developers. - Included new sections in the README for changelog and documentation links, improving accessibility to project information.
62 lines
2.0 KiB
Python
62 lines
2.0 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 VCALENDAR (ICS) with one VEVENT per duty.
|
|
|
|
Args:
|
|
duties_with_name: List of (Duty, full_name). full_name is available for
|
|
DESCRIPTION; SUMMARY is taken from event_type (duty/unavailable/vacation).
|
|
|
|
Returns:
|
|
ICS file content as bytes (UTF-8).
|
|
"""
|
|
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()
|