From 1d82097af1913cefc15040e06027c50b55f3c751 Mon Sep 17 00:00:00 2001 From: Joao Carreira Date: Tue, 6 Oct 2026 16:46:38 +0100 Subject: [PATCH] feat: improve calendar sharing --- app/calendar.py | 4 +++- docs/DOMAIN.md | 1 + tests/test_calendar.py | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/calendar.py b/app/calendar.py index 0df470f..ff716a8 100644 --- a/app/calendar.py +++ b/app/calendar.py @@ -98,7 +98,9 @@ def _event(assignment: Assignment, person: User | None, tzid: str) -> list[str]: end = datetime.combine(assignment.date, schedule.end_time) if end <= start: # a shift that runs past midnight end += timedelta(days=1) - summary = f"{schedule.name} · {window_label(schedule.start_time, schedule.end_time)}" + # The person's name is part of the event title, so a list of shifts reads at a glance. + summary = f"{person.name} · {schedule.name} · {window_label(schedule.start_time, schedule.end_time)}" \ + if person is not None else f"{schedule.name} · {window_label(schedule.start_time, schedule.end_time)}" description = [ day_long(assignment.date), f"{schedule.start_time:%H:%M}–{schedule.end_time:%H:%M}", diff --git a/docs/DOMAIN.md b/docs/DOMAIN.md index 32ba817..7872adc 100644 --- a/docs/DOMAIN.md +++ b/docs/DOMAIN.md @@ -21,6 +21,7 @@ Every start copies the SQLite file aside before anything else runs, using SQLite - `/calendar` is the page holding those links: the rota only has a button pointing there, so it keeps showing nothing but who works each day. Each address sits in a readonly field with a **Copiar** button (clipboard when the browser allows it, selecting the field otherwise) plus a `webcal://` shortcut. - Administrators also get one shared feed with the whole rota, using the team token in `CALENDAR_TOKEN` (generated on first use and shown on `/admin/schedules`). - A feed publishes one month back and six months forward. Times carry the bar's timezone (`DTSTART;TZID=Europe/Lisbon:20261017T203000`) and the feed declares it in `X-WR-TIMEZONE` plus a `VTIMEZONE` block with the European summer-time rules (WET/WEST), so every client shows a night at 20:30 and keeps the right offset across the year. The zone comes from `TIMEZONE`; an unknown zone falls back to `Europe/Lisbon`. Times and names are private member data: the token is the only protection, so the shared link goes only to the team. +- Each event title is ` · · ` so a list of shifts reads at a glance; a shift without a person keeps ` · `. - An unknown token returns 404, and a deactivated person's token stops working. ### Night (`Noite`) diff --git a/tests/test_calendar.py b/tests/test_calendar.py index 1bd9bee..f8ab230 100644 --- a/tests/test_calendar.py +++ b/tests/test_calendar.py @@ -41,8 +41,8 @@ def test_the_personal_feed_only_carries_that_person_shifts(client, db): flat = unfold(feed) assert "BEGIN:VCALENDAR" in feed and feed.endswith("END:VCALENDAR\r\n") - assert "SUMMARY:Noite · 20:30–00:00" in flat - assert "SUMMARY:Almoço - Sábados · 13:00–16:00" not in flat # Bruno's shift + assert "SUMMARY:Ana · Noite · 20:30–00:00" in flat + assert "SUMMARY:Ana · Almoço - Sábados" not in flat # Bruno's shift assert "Pessoa: Ana" in flat assert "Bruno" not in flat assert f"DTSTART;TZID=Europe/Lisbon:{today:%Y%m%d}T203000" in flat @@ -191,3 +191,33 @@ def test_an_unknown_timezone_falls_back_to_lisbon(client, db, monkeypatch): monkeypatch.setattr("app.config.settings.timezone", "Marte/Phobos") assert feed_timezone() == "Europe/Lisbon" + + +def test_an_unassigned_shift_still_has_an_event(client, db): + night, _, _, ana, _, _ = seed(db) + _hours(db, night, "20:30", "00:00") + assignment = Assignment(schedule_id=night.id, date=date.today(), user_id=None, source="generated") + db.add(assignment) + db.commit() + night_row = db.get(Assignment, assignment.id) + + feed = unfold(build_feed(db, None, "Rota completa")) + + assert "SUMMARY:Noite · 20:30–00:00" in feed # nobody to name + assert "SUMMARY:Noite · 20:30–00:00 ·" not in feed + + +def test_both_feeds_publish_the_same_timezone(client, db): + """A person's own feed and the shared one must not drift apart on the timezone.""" + night, sat, _, ana, bruno, admin = seed(db) + _hours(db, night, "20:30", "00:00") + _shift(db, night, date.today(), ana) + + personal = unfold(build_feed(db, ana, "Rota — Ana")) + shared = unfold(build_feed(db, None, settings.app_name)) + + for feed in (personal, shared): + assert "X-WR-TIMEZONE:Europe/Lisbon" in feed + assert "TZID:Europe/Lisbon" in feed + assert "BEGIN:VTIMEZONE" in feed and "END:VTIMEZONE" in feed + assert f"DTSTART;TZID=Europe/Lisbon:{date.today():%Y%m%d}T203000" in feed