Files
RosterChief/mobile/models.py
Bernard Siebens 09df5d25b8 Scaffold the mobile member app: PWA shell, push, and app-shell tokens
New `mobile` Django app mounted at /app/ -- the installed PWA for Member
mode (M1-M7, see design_handoff_rosterchief_platform/README.md). Coach
mode (C1-C6) is a later phase and has no routes yet.

Foundation pieces:
- assets/mobile.css: Tailwind v4 theme reusing management.css's design
  tokens (same per-club --tenant-* theming pattern), plus the ice/coach
  accent and mobile's 14px card radius.
- PushSubscription model + pywebpush-based sender (mobile/services/push.py),
  wired to notifications.Notification via a post_save signal so the
  existing notification system gains a push channel without knowing about
  PWAs itself.
- Per-club manifest.webmanifest + service worker (served at /app/sw.js)
  + a server-rendered fallback home-screen icon (club initials on
  secondary_color) for clubs without an uploaded logo -- confirmed with
  the user as the fallback, never a generic RosterChief mark.
- App shell (base.html): navy header, person switcher (every child a
  signed-in parent manages, plus "Me"), bottom tab bar, safe-area insets.
  Vendored htmx + Alpine for the screens built on top of it.
- Placeholder views/routes for all seven M1-M7 screens so the shell is
  fully wired end-to-end before each screen is built out individually.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
2026-08-21 10:27:13 +02:00

39 lines
1.7 KiB
Python

from django.db import models
from django.utils.translation import gettext_lazy as _
from members.models import Member
from rosterchief.base import ClubScopedModel, validate_club_scope
class PushSubscription(ClubScopedModel):
"""One browser/device's Web Push registration for one member -- created by
the subscribe flow in mobile/static/mobile/app.js, sent to whenever
mobile.signals pushes a new notifications.Notification for that member.
`endpoint` (the URL the browser's own push service gave it) is globally
unique by construction -- a browser only ever has one push registration
per origin -- so it doubles as the natural "already subscribed on this
device" key: re-subscribing replaces the row rather than creating a
second one for the same browser (see mobile.views.PushSubscribeView).
"""
member = models.ForeignKey(Member, on_delete=models.CASCADE, related_name="push_subscriptions", verbose_name=_("member"))
endpoint = models.URLField(_("endpoint"), max_length=500, unique=True)
p256dh = models.CharField(_("p256dh key"), max_length=255)
auth = models.CharField(_("auth key"), max_length=255)
user_agent = models.CharField(_("user agent"), max_length=255, blank=True)
class Meta:
verbose_name = _("push subscription")
verbose_name_plural = _("push subscriptions")
ordering = ["-created"]
def __str__(self):
return f"{self.member}{self.user_agent or self.endpoint[:40]}"
def clean(self):
validate_club_scope(self, self.club_id, member_fields=("member",))
def as_subscription_info(self) -> dict:
return {"endpoint": self.endpoint, "keys": {"p256dh": self.p256dh, "auth": self.auth}}