All checks were successful
CI / lint-and-test (push) Successful in 23s
- Introduced a new configuration option `DUTY_PIN_NOTIFY` to control whether the bot re-pins the duty message when updated, providing notifications to group members. - Updated the architecture documentation to reflect the new functionality of re-pinning duty messages. - Enhanced the `.env.example` file to include the new configuration option with a description. - Added tests to verify the behavior of the new refresh pin command and its integration with the existing group duty pin functionality. - Updated internationalization messages to include help text for the new `/refresh_pin` command.
92 lines
3.1 KiB
Python
92 lines
3.1 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 set to the duty holder's full_name (or "Duty" if full_name is
|
|
empty or missing), and DESCRIPTION set to full_name.
|
|
|
|
Args:
|
|
duties_with_name: List of (Duty, full_name). full_name is used for
|
|
SUMMARY and 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)
|
|
summary = (full_name or "").strip() or "Duty"
|
|
event.add("summary", summary)
|
|
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()
|