Round out referee amounts/colors, and add bilingual (nl/en) news content
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>
This commit is contained in:
@@ -12,7 +12,7 @@ class NewsPhotoInline(admin.TabularInline):
|
||||
class NewsAdmin(admin.ModelAdmin):
|
||||
list_display = ["title", "club", "status", "visibility", "created_by"]
|
||||
list_filter = ["club", "status", "visibility"]
|
||||
search_fields = ["title"]
|
||||
search_fields = ["title", "title_en"]
|
||||
raw_id_fields = ["created_by"]
|
||||
inlines = [NewsPhotoInline]
|
||||
|
||||
|
||||
25
news/api.py
25
news/api.py
@@ -37,11 +37,21 @@ class NewsPhotoOut(Schema):
|
||||
|
||||
|
||||
class NewsItemOut(Schema):
|
||||
"""Both languages come back in one object -- title_nl/body_nl/excerpt_nl are
|
||||
the club's own-language text as authored; title_en/body_en/excerpt_en are the
|
||||
English text, falling back to the nl content when no translation was added
|
||||
(News.effective_title_en/effective_body_en) -- so these three are never blank,
|
||||
even for a news item nobody has translated yet. The consumer picks whichever
|
||||
it needs."""
|
||||
|
||||
id: uuid.UUID
|
||||
title: str
|
||||
title_nl: str
|
||||
title_en: str
|
||||
slug: str
|
||||
excerpt: str
|
||||
body: str
|
||||
excerpt_nl: str
|
||||
excerpt_en: str
|
||||
body_nl: str
|
||||
body_en: str
|
||||
published_at: datetime
|
||||
teams: list[str]
|
||||
photos: list[NewsPhotoOut]
|
||||
@@ -66,10 +76,13 @@ def _visible_news(club):
|
||||
def _to_news_item_out(item, request) -> NewsItemOut:
|
||||
return NewsItemOut(
|
||||
id=item.pk,
|
||||
title=item.title,
|
||||
title_nl=item.title,
|
||||
title_en=item.effective_title_en,
|
||||
slug=item.slug,
|
||||
excerpt=render_body_excerpt(item.body, words=EXCERPT_WORDS),
|
||||
body=render_body_html(item.body),
|
||||
excerpt_nl=render_body_excerpt(item.body, words=EXCERPT_WORDS),
|
||||
excerpt_en=render_body_excerpt(item.effective_body_en, words=EXCERPT_WORDS),
|
||||
body_nl=render_body_html(item.body),
|
||||
body_en=render_body_html(item.effective_body_en),
|
||||
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()],
|
||||
|
||||
23
news/migrations/0004_news_body_en_news_title_en.py
Normal file
23
news/migrations/0004_news_body_en_news_title_en.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 6.0.6 on 2026-08-10 18:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0003_alter_news_body'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='news',
|
||||
name='body_en',
|
||||
field=models.TextField(blank=True, help_text='Optional. Supports the same Markdown as the body above. Falls back to the body above when left blank.', verbose_name='body (English)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='news',
|
||||
name='title_en',
|
||||
field=models.CharField(blank=True, help_text='Optional. Falls back to the title above when left blank.', max_length=255, verbose_name='title (English)'),
|
||||
),
|
||||
]
|
||||
@@ -34,6 +34,10 @@ class News(ClubScopedModel):
|
||||
"shown as plain text here in the control panel."
|
||||
),
|
||||
)
|
||||
|
||||
title_en = models.CharField(_("title (English)"), max_length=255, blank=True, help_text=_("Optional. Falls back to the title above when left blank."))
|
||||
body_en = models.TextField(_("body (English)"), blank=True, help_text=_("Optional. Supports the same Markdown as the body above. Falls back to the body above when left blank."))
|
||||
|
||||
teams = models.ManyToManyField(Team, related_name="news_items", blank=True, verbose_name=_("teams"), help_text=_("Leave empty for club-wide news."))
|
||||
|
||||
visibility = models.CharField(_("visibility"), max_length=10, choices=Visibility.choices, default=Visibility.INTERNAL)
|
||||
@@ -61,6 +65,19 @@ class News(ClubScopedModel):
|
||||
self.status, self.published_at = self.Status.DRAFT, None
|
||||
self.save(update_fields=["status", "published_at"])
|
||||
|
||||
@property
|
||||
def effective_title_en(self) -> str:
|
||||
"""The English title to show -- `title_en` when the editor set one,
|
||||
else the Dutch `title`. Computed on read rather than copied into
|
||||
`title_en` at save time, so editing the Dutch title later keeps this
|
||||
current instead of leaving a stale English copy behind."""
|
||||
return self.title_en or self.title
|
||||
|
||||
@property
|
||||
def effective_body_en(self) -> str:
|
||||
"""See `effective_title_en` -- same fallback, for the body."""
|
||||
return self.body_en or self.body
|
||||
|
||||
@property
|
||||
def is_scheduled(self):
|
||||
"""PUBLISHED (past the editor's release gate) but its publish date hasn't
|
||||
|
||||
@@ -73,6 +73,29 @@ class NewsModelTests(TestCase):
|
||||
self.assertEqual(item.status, News.Status.DRAFT)
|
||||
self.assertIsNone(item.published_at)
|
||||
|
||||
def test_effective_english_falls_back_to_the_original_when_blank(self):
|
||||
item = News.objects.create(club=self.club, title="Seizoensstart", body="We beginnen het seizoen.")
|
||||
|
||||
self.assertEqual(item.effective_title_en, "Seizoensstart")
|
||||
self.assertEqual(item.effective_body_en, "We beginnen het seizoen.")
|
||||
|
||||
def test_effective_english_uses_its_own_text_when_set(self):
|
||||
item = News.objects.create(club=self.club, title="Seizoensstart", body="We beginnen het seizoen.", title_en="Season kickoff", body_en="We're starting the season.")
|
||||
|
||||
self.assertEqual(item.effective_title_en, "Season kickoff")
|
||||
self.assertEqual(item.effective_body_en, "We're starting the season.")
|
||||
|
||||
def test_effective_english_stays_current_after_the_original_changes(self):
|
||||
# Read-time fallback, not copy-on-save: editing the Dutch text later must
|
||||
# not leave a stale English "copy" behind.
|
||||
item = News.objects.create(club=self.club, title="Seizoensstart", body="We beginnen het seizoen.")
|
||||
|
||||
item.title, item.body = "Nieuwe titel", "Nieuwe tekst."
|
||||
item.save(update_fields=["title", "body"])
|
||||
|
||||
self.assertEqual(item.effective_title_en, "Nieuwe titel")
|
||||
self.assertEqual(item.effective_body_en, "Nieuwe tekst.")
|
||||
|
||||
|
||||
class NewsPhotoModelTests(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user