Add birthday calendar backend support

Birthday calendars are ordinary local calendars flagged is_birthday, so they
flow to all clients via the merge read and inherit sharing/colors/reminders.

- models: LocalCalendar.is_birthday + birthday_notify_days_before;
  LocalEvent.external_uid (Contacts dedup) + birth_year
- build_local_event_dict: server-computed display_title "Name (age)" per
  occurrence, is_birthday flag for the client cake icon, and a reminder injected
  from birthday_notify_days_before so mobile schedulers fire it
- groups combined view keeps the birthday display_title instead of overwriting it
- local_router: calendar flags + event fields on create/update, plus
  GET /calendars/{id}/birthdays for importer reconcile by external_uid
- additive SQLite migrations

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-12 21:38:37 +02:00
parent d426f8985c
commit ca09538971
5 changed files with 123 additions and 4 deletions

View File

@@ -132,6 +132,25 @@ def build_local_event_dict(
"private": bool(ev.is_private),
"reminders": [int(x) for x in (ev.reminders or "").split(",") if x.strip().lstrip("-").isdigit()],
}
# Birthday calendars: the server owns the presentation. It flags the event so
# clients can show a cake icon, appends the age computed for THIS occurrence
# (so it stays correct as years pass), and — when the calendar defines a
# "notify N days before" — injects a reminder so the mobile schedulers fire
# it. Web has no notification delivery, so the reminder is display-only there.
if getattr(cal, "is_birthday", False):
d["is_birthday"] = True
display = ev.title
if ev.birth_year:
try:
occ_year = int(str(d["start"])[:4])
age = occ_year - int(ev.birth_year)
if age >= 0:
display = f"{ev.title} ({age})"
except (ValueError, TypeError):
pass
d["display_title"] = display
if not d["reminders"] and cal.birthday_notify_days_before is not None:
d["reminders"] = [int(cal.birthday_notify_days_before) * 1440]
if owner is not None:
d["owner"] = owner
if is_group_event: