feat: improve calendar sharing

This commit is contained in:
2026-10-06 16:46:38 +01:00
parent 1445f1c112
commit 1d82097af1
3 changed files with 36 additions and 3 deletions
+3 -1
View File
@@ -98,7 +98,9 @@ def _event(assignment: Assignment, person: User | None, tzid: str) -> list[str]:
end = datetime.combine(assignment.date, schedule.end_time) end = datetime.combine(assignment.date, schedule.end_time)
if end <= start: # a shift that runs past midnight if end <= start: # a shift that runs past midnight
end += timedelta(days=1) 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 = [ description = [
day_long(assignment.date), day_long(assignment.date),
f"{schedule.start_time:%H:%M}–{schedule.end_time:%H:%M}", f"{schedule.start_time:%H:%M}–{schedule.end_time:%H:%M}",
+1
View File
@@ -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. - `/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`). - 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. - 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 `<pessoa> · <escala> · <horas>` so a list of shifts reads at a glance; a shift without a person keeps `<escala> · <horas>`.
- An unknown token returns 404, and a deactivated person's token stops working. - An unknown token returns 404, and a deactivated person's token stops working.
### Night (`Noite`) ### Night (`Noite`)
+32 -2
View File
@@ -41,8 +41,8 @@ def test_the_personal_feed_only_carries_that_person_shifts(client, db):
flat = unfold(feed) flat = unfold(feed)
assert "BEGIN:VCALENDAR" in feed and feed.endswith("END:VCALENDAR\r\n") assert "BEGIN:VCALENDAR" in feed and feed.endswith("END:VCALENDAR\r\n")
assert "SUMMARY:Noite · 20:30–00:00" in flat assert "SUMMARY:Ana · 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 · Almoço - Sábados" not in flat # Bruno's shift
assert "Pessoa: Ana" in flat assert "Pessoa: Ana" in flat
assert "Bruno" not in flat assert "Bruno" not in flat
assert f"DTSTART;TZID=Europe/Lisbon:{today:%Y%m%d}T203000" 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") monkeypatch.setattr("app.config.settings.timezone", "Marte/Phobos")
assert feed_timezone() == "Europe/Lisbon" 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