Files
RosterChief/news/api.py
Bernard Siebens 98b8002a04 Add billing-ending banner, events CRUD, RBIHF import, public API, team photos, and sponsors
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
2026-08-06 17:36:04 +02:00

84 lines
2.3 KiB
Python

"""Public read-only news endpoint -- see api/urls.py for how this is mounted.
Pagination is hand-rolled (limit/offset params) rather than Ninja's built-in
@paginate: a photo's URL has to be made absolute against `request`
(api/urls.py's docstring explains why), and Ninja's response-schema resolvers
don't have request access, so building the page manually here is simpler than
fighting that.
"""
import uuid
from datetime import datetime
from django.utils import timezone
from ninja import Router, Schema
from api.errors import require_club
from .models import News
router = Router(tags=["news"])
DEFAULT_LIMIT = 20
MAX_LIMIT = 100
class NewsPhotoOut(Schema):
url: str
is_main: bool
ordering: int
class NewsItemOut(Schema):
id: uuid.UUID
title: str
slug: str
body: str
published_at: datetime
teams: list[str]
photos: list[NewsPhotoOut]
class NewsListOut(Schema):
count: int
limit: int
offset: int
results: list[NewsItemOut]
def _to_news_item_out(item, request) -> NewsItemOut:
return NewsItemOut(
id=item.pk,
title=item.title,
slug=item.slug,
body=item.body,
published_at=item.published_at,
teams=[team.name for team in item.teams.all()],
photos=[NewsPhotoOut(url=request.build_absolute_uri(photo.image.url), is_main=photo.is_main, ordering=photo.ordering) for photo in item.photos.all()],
)
@router.get("/", response=NewsListOut, summary="Published news")
def list_news(request, limit: int = DEFAULT_LIMIT, offset: int = 0):
"""Published news items whose release date has passed and that are marked
visible outside the club (`external` or `both`) -- newest first."""
club = require_club(request)
limit = max(1, min(limit, MAX_LIMIT))
offset = max(0, offset)
queryset = (
News.objects.filter(
club=club,
status=News.Status.PUBLISHED,
published_at__lte=timezone.now(),
visibility__in=[News.Visibility.EXTERNAL, News.Visibility.BOTH],
)
.prefetch_related("photos", "teams")
.order_by("-published_at")
)
count = queryset.count()
page = queryset[offset : offset + limit]
return NewsListOut(count=count, limit=limit, offset=offset, results=[_to_news_item_out(item, request) for item in page])