Add calendar sync: a combined iCal subscription feed per account
New mobile:calendar_feed (/app/calendar/<token>.ics) -- a standard webcal-style subscription feed calendar apps can poll, so a family's schedule shows up in Apple/Google/Outlook Calendar alongside everything else. Token-authenticated rather than session-authenticated (calendar apps can't do interactive login); CalendarFeedToken is deliberately not club-scoped, since the same token works under whichever club subdomain the request is fetched from -- an account active in more than one club needs only the one link. Combined across every managed person (their name goes in the event title when there's more than one), every event they're invited to regardless of RSVP status, not capped to Calendar's own 2-week window -- a synced calendar app is exactly where someone wants the whole season visible. A cancelled event stays in the feed as STATUS:CANCELLED rather than disappearing, so it's removed properly on the subscriber's next refresh instead of just vanishing. New mobile:calendar_feed_settings (linked from Me) shows the webcal:// and https:// links plus a "reset my calendar link" action that immediately invalidates the old one. Uses the icalendar package for RFC 5545 generation rather than hand-rolling escaping/line-folding. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
@@ -36,7 +36,8 @@ from teams.models import TeamMembership
|
||||
|
||||
from .forms import MemberProfileForm
|
||||
from .mixins import PersonScopeMixin
|
||||
from .models import PushSubscription
|
||||
from .models import CalendarFeedToken, PushSubscription
|
||||
from .services.calendar_feed import build_feed
|
||||
from .services.icons import render_fallback_icon
|
||||
|
||||
|
||||
@@ -121,6 +122,54 @@ class PushSubscribeView(LoginRequiredMixin, ClubScopedPublicMixin, View):
|
||||
return JsonResponse({"status": "ok"})
|
||||
|
||||
|
||||
class CalendarFeedView(ClubScopedPublicMixin, View):
|
||||
"""The .ics subscription feed a calendar app polls -- URL-token
|
||||
authenticated, not LoginRequiredMixin (a calendar app can't do
|
||||
interactive login). See mobile.services.calendar_feed.build_feed for the
|
||||
feed's own shape/scope, and CalendarFeedToken's own docstring for why one
|
||||
token covers every club the account belongs to."""
|
||||
|
||||
def get(self, request, token):
|
||||
feed_token = get_object_or_404(CalendarFeedToken.objects.select_related("user"), token=token)
|
||||
me = Member.objects.filter(user=feed_token.user).first()
|
||||
people = []
|
||||
if me is not None:
|
||||
children = Member.objects.filter(
|
||||
family_memberships__role=FamilyMembership.FamilyRole.CHILD,
|
||||
family_memberships__family__memberships__member=me,
|
||||
family_memberships__family__memberships__role__in=[FamilyMembership.FamilyRole.PARENT, FamilyMembership.FamilyRole.GUARDIAN],
|
||||
member_of__club=request.club,
|
||||
).distinct()
|
||||
people = [me, *children]
|
||||
|
||||
feed = build_feed(request.club, people)
|
||||
return HttpResponse(feed, content_type="text/calendar; charset=utf-8")
|
||||
|
||||
|
||||
class CalendarFeedSettingsView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
"""M5's "Calendar sync" row -- the webcal://.../https:// subscription
|
||||
links for this account's combined feed, plus a reset action that
|
||||
immediately invalidates the old URL (e.g. a link shared/leaked by
|
||||
accident)."""
|
||||
|
||||
template_name = "mobile/calendar_feed_settings.html"
|
||||
screen_title = _("Calendar sync")
|
||||
active_tab = "me"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
feed_token, _created = CalendarFeedToken.objects.get_or_create(user=self.request.user)
|
||||
feed_url = self.request.build_absolute_uri(reverse("mobile:calendar_feed", kwargs={"token": feed_token.token}))
|
||||
return super().get_context_data(feed_url=feed_url, webcal_url=feed_url.replace("https://", "webcal://", 1).replace("http://", "webcal://", 1), **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
feed_token, _created = CalendarFeedToken.objects.get_or_create(user=request.user)
|
||||
feed_token.regenerate()
|
||||
title = _("Calendar link reset")
|
||||
body = _("Your old calendar link no longer works. Re-subscribe using the new one below.")
|
||||
notify(request, f"s|{title}|{body}")
|
||||
return HttpResponseRedirect(reverse("mobile:calendar_feed_settings"))
|
||||
|
||||
|
||||
class HomeView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
|
||||
"""M1 -- design_handoff_rosterchief_platform/README.md's M1 section: a
|
||||
hero card for the soonest upcoming event across everyone currently in
|
||||
|
||||
Reference in New Issue
Block a user