M4 Calendar: publish public .ics links, subscriptions, multi-calendar settings
Calendar (builtin plugin):
- Publish read-only public .ics feed per calendar via vault-sealed token
(full-detail or free/busy-only); unauthenticated GET /public/{token},
404 on bad/absent token. Migration calendar0004 adds publish_token,
publish_busy, subscription_url + partial unique index.
- Subscribe to external iCal (.ics) URLs (SSRF-guarded) as read-only
calendars; CSRF-guarded POST /subscriptions.
- Settings > Calendars: create/edit/delete, publish toggles with live
shareable link + copy, Add-account stubs (Google/iCloud/CalDAV, deferred),
Subscriptions section; default calendar + default reminders; auto-detected
time zone; color-dropdown contrast fix; 3x-larger edit arrow.
- Left panel: shared Mail-parity sidebar template (.wm-side-*), calendar
search panel, font/spacing tuning, per-section "+" create shortcut.
Frontend: CalendarOut publish fields, api publish/subscribe, settings keys.
Backend: settings default_calendar + reminder lists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
This commit is contained in:
@@ -26,6 +26,9 @@ class SettingsOut(BaseModel):
|
||||
tz: str = "UTC"
|
||||
time_format: str = "24" # 12 = AM/PM, 24 = 24-hour (default); applies app-wide
|
||||
jitsi_server: str = "" # blank => https://meet.jit.si/
|
||||
default_calendar: str = "" # calendar id new events default to (blank => first)
|
||||
default_reminders_timed: list[int] = [] # minutes-before defaults for timed events
|
||||
default_reminders_allday: list[int] = [] # minutes-before defaults for all-day events
|
||||
allowed_image_senders: list[str] = []
|
||||
|
||||
|
||||
@@ -34,6 +37,9 @@ class SettingsPatch(BaseModel):
|
||||
tz: str | None = None
|
||||
time_format: str | None = None
|
||||
jitsi_server: str | None = None
|
||||
default_calendar: str | None = None
|
||||
default_reminders_timed: list[int] | None = None
|
||||
default_reminders_allday: list[int] | None = None
|
||||
|
||||
|
||||
class QuotaOut(BaseModel):
|
||||
@@ -56,6 +62,10 @@ def add_allowed_image_sender(user: User, email: str) -> None:
|
||||
user.settings = {**user.settings, "allowed_image_senders": current}
|
||||
|
||||
|
||||
def _int_list(value: object) -> list[int]:
|
||||
return [int(x) for x in value] if isinstance(value, list) else []
|
||||
|
||||
|
||||
def _settings_out(user: User) -> SettingsOut:
|
||||
s = user.settings
|
||||
return SettingsOut(
|
||||
@@ -63,6 +73,9 @@ def _settings_out(user: User) -> SettingsOut:
|
||||
tz=str(s.get("tz", "UTC")),
|
||||
time_format=str(s.get("time_format", "24")),
|
||||
jitsi_server=str(s.get("jitsi_server", "")),
|
||||
default_calendar=str(s.get("default_calendar", "")),
|
||||
default_reminders_timed=_int_list(s.get("default_reminders_timed")),
|
||||
default_reminders_allday=_int_list(s.get("default_reminders_allday")),
|
||||
allowed_image_senders=sorted(allowed_image_senders(user)),
|
||||
)
|
||||
|
||||
@@ -92,6 +105,12 @@ async def patch_settings(body: SettingsPatch, user: CurrentUser, db: DbSession)
|
||||
updates["time_format"] = body.time_format
|
||||
if body.jitsi_server is not None:
|
||||
updates["jitsi_server"] = body.jitsi_server.strip()
|
||||
if body.default_calendar is not None:
|
||||
updates["default_calendar"] = body.default_calendar
|
||||
if body.default_reminders_timed is not None:
|
||||
updates["default_reminders_timed"] = [int(x) for x in body.default_reminders_timed]
|
||||
if body.default_reminders_allday is not None:
|
||||
updates["default_reminders_allday"] = [int(x) for x in body.default_reminders_allday]
|
||||
user.settings = updates
|
||||
await db.commit()
|
||||
return _settings_out(user)
|
||||
|
||||
Reference in New Issue
Block a user