All checks were successful
CI / lint-and-test (push) Successful in 23s
- Implemented a new API endpoint to generate an ICS calendar for team duty shifts, accessible via a valid token. - Enhanced the `calendar_link` command to return both personal and team calendar URLs. - Added a new function to build the team ICS file, ensuring each event includes the duty holder's name in the description. - Updated tests to cover the new team calendar functionality, including validation for token formats and response content. - Revised internationalization messages to reflect the new team calendar links.
89 lines
3.0 KiB
Python
89 lines
3.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
|
|
from duty_teller.utils.dates import parse_utc_iso
|
|
|
|
# Summary labels by event_type (duty | unavailable | vacation)
|
|
SUMMARY_BY_TYPE: dict[str, str] = {
|
|
"duty": "Duty",
|
|
"unavailable": "Unavailable",
|
|
"vacation": "Vacation",
|
|
}
|
|
|
|
|
|
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()
|
|
|
|
|
|
def build_team_ics(duties_with_name: list[tuple[Duty, str]]) -> bytes:
|
|
"""Build a VCALENDAR (ICS) with one VEVENT per duty for team calendar.
|
|
|
|
Same structure as personal calendar; PRODID is Team Calendar; each VEVENT
|
|
has SUMMARY='Duty' and DESCRIPTION set to the duty holder's full_name.
|
|
|
|
Args:
|
|
duties_with_name: List of (Duty, full_name). full_name is used for DESCRIPTION.
|
|
|
|
Returns:
|
|
ICS file content as bytes (UTF-8).
|
|
"""
|
|
cal = Calendar()
|
|
cal.add("prodid", "-//Duty Teller//Team 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)
|
|
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)
|
|
event.add("summary", "Duty")
|
|
event.add("description", full_name)
|
|
event.add("uid", f"duty-{duty.id}@duty-teller")
|
|
event.add("dtstamp", datetime.now(timezone.utc))
|
|
cal.add_component(event)
|
|
|
|
return cal.to_ical()
|