feat: improve Wagtail admin editor experience for articles
Some checks failed
CI / nightly-e2e (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / ci (pull_request) Failing after 9s
CI / pr-e2e (pull_request) Failing after 1m38s

- Add published_date field to ArticlePage with auto-populate from
  first_published_at on first publish, plus data migration backfill
- Surface go_live_at/expire_at scheduling fields in editor panels
- Reorganise ArticlePage editor with TabbedInterface (Content,
  Metadata, Publishing, SEO tabs)
- Add Articles PageListingViewSet to admin menu with custom columns
  (author, category, published date, status) and category/author filters
- Add Articles summary dashboard panel showing drafts, scheduled,
  and recently published articles
- Update all front-end queries and RSS feeds to use published_date
- Add 10 unit tests and 4 E2E tests for new admin features

Closes #39

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mark
2026-03-03 14:07:27 +00:00
parent 2d93555c60
commit 2c94040221
10 changed files with 527 additions and 15 deletions

View File

@@ -11,7 +11,7 @@ from django.shortcuts import get_object_or_404
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import Tag, TaggedItemBase
from wagtail.admin.panels import FieldPanel, PageChooserPanel
from wagtail.admin.panels import FieldPanel, ObjectList, PageChooserPanel, TabbedInterface
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page
@@ -38,7 +38,7 @@ class HomePage(Page):
.public()
.select_related("author", "category")
.prefetch_related("tags__metadata")
.order_by("-first_published_at")
.order_by("-published_date")
)
articles = list(articles_qs[:5])
ctx["featured_article"] = self.featured_article
@@ -64,7 +64,7 @@ class ArticleIndexPage(RoutablePageMixin, Page):
.live()
.select_related("author", "category")
.prefetch_related("tags__metadata")
.order_by("-first_published_at")
.order_by("-published_date")
)
def get_category_url(self, category):
@@ -191,21 +191,46 @@ class ArticlePage(SeoMixin, Page):
tags = ClusterTaggableManager(through="blog.ArticleTag", blank=True)
read_time_mins = models.PositiveIntegerField(editable=False, default=1)
comments_enabled = models.BooleanField(default=True)
published_date = models.DateTimeField(
null=True,
blank=True,
help_text="Display date for this article. Auto-set on first publish if left blank.",
)
parent_page_types = ["blog.ArticleIndexPage"]
subpage_types: list[str] = []
content_panels = Page.content_panels + [
FieldPanel("category"),
FieldPanel("author"),
FieldPanel("hero_image"),
content_panels = [
FieldPanel("title"),
FieldPanel("summary"),
FieldPanel("body"),
]
metadata_panels = [
FieldPanel("category"),
FieldPanel("author"),
FieldPanel("tags"),
FieldPanel("hero_image"),
FieldPanel("comments_enabled"),
]
promote_panels = Page.promote_panels + SeoMixin.seo_panels
publishing_panels = [
FieldPanel("published_date"),
FieldPanel("go_live_at"),
FieldPanel("expire_at"),
]
edit_handler = TabbedInterface(
[
ObjectList(content_panels, heading="Content"),
ObjectList(metadata_panels, heading="Metadata"),
ObjectList(publishing_panels, heading="Publishing"),
ObjectList(
Page.promote_panels + SeoMixin.seo_panels,
heading="SEO",
),
]
)
search_fields = Page.search_fields
@@ -215,6 +240,8 @@ class ArticlePage(SeoMixin, Page):
slug="general",
defaults={"name": "General", "description": "General articles", "colour": "neutral"},
)
if not self.published_date and self.first_published_at:
self.published_date = self.first_published_at
self.read_time_mins = self._compute_read_time()
return super().save(*args, **kwargs)
@@ -239,14 +266,14 @@ class ArticlePage(SeoMixin, Page):
.filter(tags__in=tag_ids)
.exclude(pk=self.pk)
.distinct()
.order_by("-first_published_at")[:count]
.order_by("-published_date")[:count]
)
if len(related) < count:
exclude_ids = [a.pk for a in related] + [self.pk]
fallback = list(
ArticlePage.objects.live()
.exclude(pk__in=exclude_ids)
.order_by("-first_published_at")[: count - len(related)]
.order_by("-published_date")[: count - len(related)]
)
return related + fallback
return related