News gains a PENDING_REVIEW status between draft and published. A
non-editor author (can_add_news but not can_publish_news -- a
coach_manager, not an ADMIN/EDITOR) gets a "Send for review" button
instead of Publish; an editor/admin always sees Publish directly, no
review step. Submitting notifies every ADMIN/EDITOR in-app only (see
notify_members' new send_email=False) -- a review queue that emailed
on every submission would get noisy fast.
The notification area itself: a topbar bell (badge + dropdown, same
<details>/<summary> convention as the sidebar's user-menu, generalised
to a shared .dismissable-details close handler) visible on every page,
plus a fuller "Notifications" card on the dashboard, both fed by a new
notification_bell context processor. "Mark all read" clears the
signed-in staff member's own unread notifications for this club.
This is the reusable notification system's first consumer beyond news
publishing itself -- validates that notify_members()/Notification
generalise the way they were meant to.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
New `notifications` app: Notification (club-scoped, keyed to the
member it's about, generic `source` via a ContentType/object_id pair
so future activities can reuse this without a new model each time)
plus notify_members(), which resolves each member's own email (if
they hold a login) and every parent/guardian's, always -- a child
with their own account doesn't opt their parents out -- and emails
the club-branded template to whichever addresses that resolves to.
Delivery is email-only for now (no in-app feed exists yet); the row
is created either way, ready for one later.
news.tasks.notify_news_published resolves the audience (a team-scoped
item's current rosters, or every active member if it's club-wide) and
calls notify_members with the item's title/plain-text body.
NewsPublishForm gained a "Notify linked members" checkbox (opt-in,
default off); when checked, NewsPublishView schedules the task with
Celery's `eta` set to the item's own published_at -- a scheduled
item's notification arrives when it actually goes live, and an
immediate publish (eta in the past) just runs right away, no separate
branch needed.
Registered the new notification email on the Club identity page's
Email tab alongside the others.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Replaces the flat news list with a D8-style two-pane layout: a filterable
list on the left (All/Drafts/Scheduled/Published chips with counts) and a
preview of whichever item is selected on the right, reusing the same
article/photos/publish markup news_detail.html already had (now factored
into _news_preview.html so both pages share it). Adds an NL/EN language
toggle to the article preview -- the club's two content languages are now
switchable in place, with a fallback note when a translation is missing,
instead of a separate "English" card that only appeared once translated.
The standalone news_detail.html permalink page is unchanged behaviourally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Referee display: drop the redundant "External" pill from the PDF form, round
the event/dashboard "due" summary to 2 decimals (a fine-grained km rate like
0.083 was pushing the raw total to 3+ decimals), and theme the PDF's accent
colors off the club's own primary/secondary colors instead of a hardcoded
default.
News: title/body stay the club's own-language (Dutch) text; new optional
title_en/body_en carry a translation, with News.effective_title_en/
effective_body_en resolving the fallback to the original on read rather than
copying it in at save time -- so editing the Dutch text later never leaves a
stale English copy behind, and existing rows get correct fallback behaviour
with no backfill. The news form lays both languages out side by side; the
detail page only shows an English section when one was actually added. The
public API returns both languages in one call (title_nl/body_nl/excerpt_nl
alongside title_en/body_en/excerpt_en, the latter never blank) -- a breaking
rename of the previously-unprefixed title/body/excerpt fields.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Club staff author body as Markdown in the control panel (help_text
now explains the syntax); the public API renders it to sanitized
HTML on the way out via news/services.py -- markdown for the
conversion, nh3 (Rust/ammonia) to strip anything staff's raw
Markdown source might smuggle through (script tags, event handler
attributes, javascript: URLs) before it reaches someone else's
public website. The control panel's own preview is untouched and
still shows the raw source.
Excerpt is now derived from the rendered HTML's plain text rather
than the raw Markdown source, so syntax like ** or [text](url)
doesn't leak into what's meant to be a short teaser.
- news: NewsItemOut gains `excerpt` (truncated body); GET /news/{slug}/
fetches a single item. slug already auto-populates on save, but a
data migration backfills any pre-existing blank ones.
- games: home_team/away_team change from plain strings to {id, name,
logo_url} objects -- home links to the actual Team (logo from the
club's own logo, since teams have none of their own), away links
to the actual Opponent (which already had a logo field). Breaking
change for any existing consumer of the old string shape.
- sponsors: SponsorOut gains logo_width/logo_height, computed in
Sponsor.save() -- Pillow for raster, a bounded regex read of the
SVG root tag for vector logos (not a full XML parse, since that's
exposed to entity-expansion attacks on untrusted uploads). A data
migration backfills dimensions for existing sponsor logos.
- teams: PlayerOut gains `license`, sourced from ClubMembership (not
Member -- it's per-club, per-season), batched in one query.
A large batch of club-management features built up over one session:
- Club dashboard banner warning admins 1 month before billing ends
- Full Events/EventSeries CRUD (recurrence builder, occurrence lifecycle,
per-team permissions), with match->game rename and game-specific fields
(score, competition, live status, external game ID)
- Django-admin competition dropdown, gated per-club by feature flag
- Auto-import of RBIHF fixtures (scrape -> diff -> preview -> confirm),
with location/opponent dropdowns suggested from existing club data
- Feature-flag-gated Shop/Forms nav sections, reusing the same flag
machinery for the RBIHF import button
- Team roster now scoped to members active this season or next, sorted and
grouped by position
- Club sport type (ice hockey / other), shown in the control panel's club
subtitle
- Per-season team photo upload from the team page
- New public read-only API (Django Ninja) at /api/v1/: news, team rosters,
upcoming/live/per-team games, and sponsors -- auto-documented via Swagger
UI, CORS-enabled for a club's own external website
- Club sponsors: admin-only CRUD (logo, URL, active date window) plus a
date-windowed, optionally randomized API endpoint
- Assorted fixes: NullBooleanField dropdown rendering, cross-club event
validation timing, searchable-select chip placement, btn-neutral ->
default button style sweep, calendar-month chart windows
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R1gj3J1QPfP38XWpnpbFpy
Team managers/coaches now only see their own teams, can't create teams,
and can view (but not edit) positions -- admins keep full rights.
Locations and Opponents move from read-only stubs to full CRUD, gated
to admins and management-position staff, with a country dropdown
(django-countries) instead of free text. Also: the team list shows
player/staff counts, and deleting a news item's main photo promotes
another one instead of leaving the item without one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R1gj3J1QPfP38XWpnpbFpy
News and NewsPhoto (team-tagged instead of categorised, one photo taggable
as main via a partial unique constraint), gated per club/services/access.py:
any current-season coach_manager, EDITOR, or ADMIN can draft and edit a news
item; only EDITOR/ADMIN can publish it, or edit it once it's live. Publishing
takes a date so it can be scheduled ahead of time rather than only right now.
Authoring/release only for now -- no member-facing reading page or public API
yet, the visibility field (internal/external/both) is there for when those land.