Files
calendars/openspec/changes/add-ical-subscription-export/tasks.md
Nathan Panchout a96be2c7b7 📝(docs) add OpenSpec workflow
Add OpenSpec workflow for AI-assisted change proposals
including proposal templates, archive commands, and
project configuration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:56:21 +01:00

4.9 KiB

Tasks: iCal Subscription Export

1. Backend - Data Model

  • 1.1 Create CalendarSubscriptionToken model in core/models.py
    • ForeignKey to User (owner, on_delete=CASCADE)
    • caldav_path field (CharField, max_length=512) - stores CalDAV path directly
    • calendar_name field (CharField, max_length=255, optional) - for display
    • token field (UUID, unique, indexed, default=uuid4)
    • is_active boolean (default=True)
    • last_accessed_at DateTimeField (nullable)
    • UniqueConstraint on (owner, caldav_path)
  • 1.2 Create and run database migration
  • 1.3 Add model to Django admin for debugging

2. SabreDAV - Enable ICSExportPlugin

  • 2.1 Add ICSExportPlugin to server.php:
    $server->addPlugin(new CalDAV\ICSExportPlugin());
    
  • 2.2 Test that ?export works via existing CalDAV proxy

3. Backend - Public iCal Endpoint

  • 3.1 Create ICalExportView in core/api/viewsets_ical.py
    • No authentication required (public endpoint)
    • Extract token from URL path
    • Lookup CalendarSubscriptionToken by token
    • Return 404 if token invalid/inactive
    • Update last_accessed_at on access
    • Proxy request to SabreDAV using token.caldav_path and token.owner.email
    • Return ICS response with Content-Type: text/calendar
    • Set security headers (Cache-Control, Referrer-Policy)
  • 3.2 Add URL route: path('ical/<uuid:token>.ics', ...)
  • 3.3 Write tests for public endpoint (valid token, invalid token, inactive)

4. Backend - Standalone Token Management API

  • 4.1 Create serializers in core/api/serializers.py
    • CalendarSubscriptionTokenSerializer - fields: token, url, caldav_path, calendar_name, etc.
    • CalendarSubscriptionTokenCreateSerializer - for POST body validation
  • 4.2 Create standalone SubscriptionTokenViewSet in core/api/viewsets.py:
    • POST /subscription-tokens/ - create token with { caldav_path, calendar_name }
    • GET /subscription-tokens/by-path/?caldav_path=... - get existing token
    • DELETE /subscription-tokens/by-path/?caldav_path=... - revoke token
    • Permission verification: user's email must be in caldav_path
  • 4.3 Register viewset in core/urls.py
  • 4.4 Write API tests for token management (create, get, delete, permissions)

5. Frontend - API Integration

  • 5.1 Add API functions in features/calendar/api.ts:
    • getSubscriptionToken(caldavPath) - GET by-path
    • createSubscriptionToken({ caldavPath, calendarName }) - POST
    • deleteSubscriptionToken(caldavPath) - DELETE by-path
  • 5.2 Update React Query hooks in hooks/useCalendars.ts
    • Use caldavPath instead of calendarId

6. Frontend - UI Components

  • 6.1 Update SubscriptionUrlModal component
    • Accept caldavPath prop instead of calendarId
    • Extract caldavPath from calendar URL in parent component
    • Display the subscription URL in a copyable field
    • "Copy to clipboard" button with success feedback
    • Warning text about URL being private
    • "Regenerate URL" button with confirmation dialog
    • Only show error alert for real errors (not for expected 404)
  • 6.2 Update CalendarList.tsx
    • Extract CalDAV path from calendar URL
    • Pass caldavPath to SubscriptionUrlModal
  • 6.3 Add translations (i18n) for new UI strings

7. Cleanup

  • 7.1 Remove old subscription_token action from CalendarViewSet
  • 7.2 Remove sync-from-caldav endpoint (no longer needed)
  • 7.3 Remove syncFromCaldav from frontend API

8. Testing & Validation

  • 8.1 Manual test: add URL to Apple Calendar
  • 8.2 Manual test: add URL to Google Calendar
  • 8.3 Verify token regeneration invalidates old URL
  • 8.4 E2E test for subscription workflow (optional)

Dependencies

1 (Django model)
    ↓
2 (ICSExportPlugin) ──────┐
    ↓                     │
3 (Public endpoint) ──────┤ can run in parallel after 1
    ↓                     │
4 (Token API) ────────────┘
    ↓
5 (Frontend API)
    ↓
6 (Frontend UI)
    ↓
7 (Cleanup)
    ↓
8 (Testing)

Key Files Modified

Backend

  • src/backend/core/models.py - CalendarSubscriptionToken model (standalone)
  • src/backend/core/migrations/0002_calendarsubscriptiontoken.py
  • src/backend/core/api/serializers.py - Token serializers
  • src/backend/core/api/viewsets.py - SubscriptionTokenViewSet
  • src/backend/core/api/viewsets_ical.py - ICalExportView
  • src/backend/core/urls.py - Route registration
  • src/backend/core/admin.py - Admin configuration
  • src/backend/core/factories.py - Test factory

Frontend

  • src/features/calendar/api.ts - API functions with caldavPath
  • src/features/calendar/hooks/useCalendars.ts - React Query hooks
  • src/features/calendar/components/calendar-list/CalendarList.tsx
  • src/features/calendar/components/calendar-list/SubscriptionUrlModal.tsx

SabreDAV

  • docker/sabredav/server.php - ICSExportPlugin enabled