feat: improve Wagtail admin editor experience for articles #40
@@ -16,7 +16,7 @@ class AllArticlesFeed(Feed):
|
||||
return None
|
||||
|
||||
def items(self):
|
||||
return ArticlePage.objects.live().order_by("-first_published_at")[:20]
|
||||
return ArticlePage.objects.live().order_by("-published_date")[:20]
|
||||
|
||||
def item_title(self, item: ArticlePage):
|
||||
return item.title
|
||||
@@ -25,7 +25,7 @@ class AllArticlesFeed(Feed):
|
||||
return item.summary
|
||||
|
||||
def item_pubdate(self, item: ArticlePage):
|
||||
return item.first_published_at
|
||||
return item.published_date or item.first_published_at
|
||||
|
||||
def item_author_name(self, item: ArticlePage):
|
||||
return item.author.name
|
||||
@@ -47,7 +47,7 @@ class TagArticlesFeed(AllArticlesFeed):
|
||||
return f"No Hype AI — {obj.name}"
|
||||
|
||||
def items(self, obj):
|
||||
return ArticlePage.objects.live().filter(tags=obj).order_by("-first_published_at")[:20]
|
||||
return ArticlePage.objects.live().filter(tags=obj).order_by("-published_date")[:20]
|
||||
|
||||
|
||||
class CategoryArticlesFeed(AllArticlesFeed):
|
||||
@@ -59,4 +59,4 @@ class CategoryArticlesFeed(AllArticlesFeed):
|
||||
return f"No Hype AI — {obj.name}"
|
||||
|
||||
def items(self, obj):
|
||||
return ArticlePage.objects.live().filter(category=obj).order_by("-first_published_at")[:20]
|
||||
return ArticlePage.objects.live().filter(category=obj).order_by("-published_date")[:20]
|
||||
|
||||
18
apps/blog/migrations/0003_add_published_date.py
Normal file
18
apps/blog/migrations/0003_add_published_date.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.11 on 2026-03-03 13:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0002_category_articlepage_category'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='articlepage',
|
||||
name='published_date',
|
||||
field=models.DateTimeField(blank=True, help_text='Display date for this article. Auto-set on first publish if left blank.', null=True),
|
||||
),
|
||||
]
|
||||
24
apps/blog/migrations/0004_backfill_published_date.py
Normal file
24
apps/blog/migrations/0004_backfill_published_date.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.2.11 on 2026-03-03 13:59
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def backfill_published_date(apps, schema_editor):
|
||||
schema_editor.execute(
|
||||
"UPDATE blog_articlepage SET published_date = p.first_published_at "
|
||||
"FROM wagtailcore_page p "
|
||||
"WHERE blog_articlepage.page_ptr_id = p.id "
|
||||
"AND blog_articlepage.published_date IS NULL "
|
||||
"AND p.first_published_at IS NOT NULL"
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0003_add_published_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(backfill_published_date, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -37,6 +37,7 @@ class ArticlePageFactory(wagtail_factories.PageFactory):
|
||||
summary = "Summary"
|
||||
body = [("rich_text", "<p>Hello world</p>")]
|
||||
first_published_at = factory.LazyFunction(timezone.now)
|
||||
published_date = factory.LazyFunction(timezone.now)
|
||||
|
||||
|
||||
class LegalIndexPageFactory(wagtail_factories.PageFactory):
|
||||
|
||||
232
apps/blog/tests/test_admin_experience.py
Normal file
232
apps/blog/tests/test_admin_experience.py
Normal file
@@ -0,0 +1,232 @@
|
||||
import pytest
|
||||
from django.test import override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.blog.models import ArticleIndexPage, ArticlePage, Category
|
||||
from apps.blog.tests.factories import AuthorFactory
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_published_date_auto_set_on_first_publish(home_page):
|
||||
"""published_date should be auto-populated from first_published_at on first publish."""
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
article = ArticlePage(
|
||||
title="Auto Date",
|
||||
slug="auto-date",
|
||||
author=author,
|
||||
summary="summary",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
)
|
||||
index.add_child(instance=article)
|
||||
article.save_revision().publish()
|
||||
article.refresh_from_db()
|
||||
assert article.published_date is not None
|
||||
assert article.published_date == article.first_published_at
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_published_date_preserved_when_explicitly_set(home_page):
|
||||
"""An explicitly set published_date should not be overwritten on save."""
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
custom_date = timezone.now() - timezone.timedelta(days=30)
|
||||
article = ArticlePage(
|
||||
title="Custom Date",
|
||||
slug="custom-date",
|
||||
author=author,
|
||||
summary="summary",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
published_date=custom_date,
|
||||
)
|
||||
index.add_child(instance=article)
|
||||
article.save_revision().publish()
|
||||
article.refresh_from_db()
|
||||
assert article.published_date == custom_date
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_homepage_orders_articles_by_published_date(home_page):
|
||||
"""HomePage context should list articles ordered by -published_date."""
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
|
||||
older = ArticlePage(
|
||||
title="Older",
|
||||
slug="older",
|
||||
author=author,
|
||||
summary="s",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
published_date=timezone.now() - timezone.timedelta(days=10),
|
||||
)
|
||||
index.add_child(instance=older)
|
||||
older.save_revision().publish()
|
||||
|
||||
newer = ArticlePage(
|
||||
title="Newer",
|
||||
slug="newer",
|
||||
author=author,
|
||||
summary="s",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
published_date=timezone.now(),
|
||||
)
|
||||
index.add_child(instance=newer)
|
||||
newer.save_revision().publish()
|
||||
|
||||
ctx = home_page.get_context(type("Req", (), {"GET": {}})())
|
||||
titles = [a.title for a in ctx["latest_articles"]]
|
||||
assert titles.index("Newer") < titles.index("Older")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_article_index_orders_by_published_date(home_page, rf):
|
||||
"""ArticleIndexPage.get_articles should order by -published_date."""
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
|
||||
old = ArticlePage(
|
||||
title="Old",
|
||||
slug="old",
|
||||
author=author,
|
||||
summary="s",
|
||||
body=[("rich_text", "<p>b</p>")],
|
||||
published_date=timezone.now() - timezone.timedelta(days=5),
|
||||
)
|
||||
index.add_child(instance=old)
|
||||
old.save_revision().publish()
|
||||
|
||||
new = ArticlePage(
|
||||
title="New",
|
||||
slug="new",
|
||||
author=author,
|
||||
summary="s",
|
||||
body=[("rich_text", "<p>b</p>")],
|
||||
published_date=timezone.now(),
|
||||
)
|
||||
index.add_child(instance=new)
|
||||
new.save_revision().publish()
|
||||
|
||||
articles = list(index.get_articles())
|
||||
assert articles[0].title == "New"
|
||||
assert articles[1].title == "Old"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_feed_uses_published_date(article_page):
|
||||
"""RSS feed item_pubdate should use published_date."""
|
||||
from apps.blog.feeds import AllArticlesFeed
|
||||
|
||||
feed = AllArticlesFeed()
|
||||
assert feed.item_pubdate(article_page) == article_page.published_date
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(ALLOWED_HOSTS=["testserver", "localhost", "127.0.0.1"])
|
||||
def test_articles_listing_viewset_loads(client, django_user_model, home_page):
|
||||
"""The Articles PageListingViewSet index page should load."""
|
||||
admin = django_user_model.objects.create_superuser(
|
||||
username="admin", email="admin@example.com", password="admin-pass"
|
||||
)
|
||||
client.force_login(admin)
|
||||
response = client.get("/cms/articles/")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(ALLOWED_HOSTS=["testserver", "localhost", "127.0.0.1"])
|
||||
def test_articles_listing_shows_articles(client, django_user_model, home_page):
|
||||
"""The Articles listing should show existing articles."""
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
article = ArticlePage(
|
||||
title="Listed Article",
|
||||
slug="listed-article",
|
||||
author=author,
|
||||
summary="summary",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
)
|
||||
index.add_child(instance=article)
|
||||
article.save_revision().publish()
|
||||
|
||||
admin = django_user_model.objects.create_superuser(
|
||||
username="admin", email="admin@example.com", password="admin-pass"
|
||||
)
|
||||
client.force_login(admin)
|
||||
response = client.get("/cms/articles/")
|
||||
assert response.status_code == 200
|
||||
assert "Listed Article" in response.content.decode()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(ALLOWED_HOSTS=["testserver", "localhost", "127.0.0.1"])
|
||||
def test_dashboard_panel_renders(client, django_user_model, home_page):
|
||||
"""The Wagtail admin dashboard should include the articles summary panel."""
|
||||
admin = django_user_model.objects.create_superuser(
|
||||
username="admin", email="admin@example.com", password="admin-pass"
|
||||
)
|
||||
client.force_login(admin)
|
||||
response = client.get("/cms/")
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode()
|
||||
assert "Articles overview" in content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(ALLOWED_HOSTS=["testserver", "localhost", "127.0.0.1"])
|
||||
def test_dashboard_panel_shows_drafts(client, django_user_model, home_page):
|
||||
"""Dashboard panel should list draft articles."""
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
draft = ArticlePage(
|
||||
title="My Draft Post",
|
||||
slug="draft-post",
|
||||
author=author,
|
||||
summary="summary",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
)
|
||||
index.add_child(instance=draft)
|
||||
draft.save_revision() # save revision but don't publish
|
||||
|
||||
admin = django_user_model.objects.create_superuser(
|
||||
username="admin", email="admin@example.com", password="admin-pass"
|
||||
)
|
||||
client.force_login(admin)
|
||||
response = client.get("/cms/")
|
||||
content = response.content.decode()
|
||||
assert "My Draft Post" in content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(ALLOWED_HOSTS=["testserver", "localhost", "127.0.0.1"])
|
||||
def test_article_edit_page_has_tabbed_interface(client, django_user_model, home_page):
|
||||
"""ArticlePage editor should have tabbed panels (Content, Metadata, Publishing, SEO)."""
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
article = ArticlePage(
|
||||
title="Tabbed",
|
||||
slug="tabbed",
|
||||
author=author,
|
||||
summary="summary",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
)
|
||||
index.add_child(instance=article)
|
||||
article.save_revision().publish()
|
||||
|
||||
admin = django_user_model.objects.create_superuser(
|
||||
username="admin", email="admin@example.com", password="admin-pass"
|
||||
)
|
||||
client.force_login(admin)
|
||||
response = client.get(f"/cms/pages/{article.pk}/edit/")
|
||||
content = response.content.decode()
|
||||
assert response.status_code == 200
|
||||
assert "Content" in content
|
||||
assert "Metadata" in content
|
||||
assert "Publishing" in content
|
||||
assert "SEO" in content
|
||||
@@ -1,7 +1,17 @@
|
||||
from django.utils.html import format_html
|
||||
|
||||
import django_filters
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.filters import WagtailFilterSet
|
||||
from wagtail.admin.ui.components import Component
|
||||
from wagtail.admin.ui.tables import Column, DateColumn
|
||||
from wagtail.admin.ui.tables.pages import BulkActionsColumn, PageStatusColumn, PageTitleColumn
|
||||
from wagtail.admin.viewsets.pages import PageListingViewSet
|
||||
from wagtail.snippets.models import register_snippet
|
||||
from wagtail.snippets.views.snippets import SnippetViewSet
|
||||
|
||||
from apps.blog.models import Category, TagMetadata
|
||||
from apps.authors.models import Author
|
||||
from apps.blog.models import ArticlePage, Category, TagMetadata
|
||||
|
||||
|
||||
class TagMetadataViewSet(SnippetViewSet):
|
||||
@@ -22,3 +32,74 @@ class CategoryViewSet(SnippetViewSet):
|
||||
|
||||
|
||||
register_snippet(CategoryViewSet)
|
||||
|
||||
|
||||
# ── Articles page listing ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ArticleFilterSet(WagtailFilterSet):
|
||||
category = django_filters.ModelChoiceFilter(
|
||||
queryset=Category.objects.all(),
|
||||
empty_label="All categories",
|
||||
)
|
||||
author = django_filters.ModelChoiceFilter(
|
||||
queryset=Author.objects.all(),
|
||||
empty_label="All authors",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ArticlePage
|
||||
fields = []
|
||||
|
||||
|
||||
class ArticlePageListingViewSet(PageListingViewSet):
|
||||
model = ArticlePage
|
||||
icon = "doc-full"
|
||||
menu_label = "Articles"
|
||||
menu_order = 200
|
||||
add_to_admin_menu = True
|
||||
name = "articles"
|
||||
columns = [
|
||||
BulkActionsColumn("bulk_actions"),
|
||||
PageTitleColumn("title", classname="title"),
|
||||
Column("author", label="Author", sort_key="author__name"),
|
||||
Column("category", label="Category"),
|
||||
DateColumn("published_date", label="Published", sort_key="published_date"),
|
||||
PageStatusColumn("status", sort_key="live"),
|
||||
]
|
||||
filterset_class = ArticleFilterSet
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
def register_article_listing():
|
||||
return ArticlePageListingViewSet("articles")
|
||||
|
||||
|
||||
# ── Dashboard panel ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class ArticlesSummaryPanel(Component):
|
||||
name = "articles_summary"
|
||||
template_name = "blog/panels/articles_summary.html"
|
||||
order = 110
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
context = super().get_context_data(parent_context)
|
||||
context["drafts"] = (
|
||||
ArticlePage.objects.not_live()
|
||||
.order_by("-latest_revision_created_at")[:5]
|
||||
)
|
||||
context["scheduled"] = (
|
||||
ArticlePage.objects.filter(go_live_at__isnull=False, live=False)
|
||||
.order_by("go_live_at")[:5]
|
||||
)
|
||||
context["recent"] = (
|
||||
ArticlePage.objects.live()
|
||||
.order_by("-published_date")[:5]
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
@hooks.register("construct_homepage_panels")
|
||||
def add_articles_summary_panel(request, panels):
|
||||
panels.append(ArticlesSummaryPanel())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
from taggit.models import Tag
|
||||
from wagtail.models import Page, Site
|
||||
@@ -10,6 +11,8 @@ from apps.comments.models import Comment
|
||||
from apps.core.models import NavigationMenuItem, SiteSettings, SocialMediaLink
|
||||
from apps.legal.models import LegalIndexPage, LegalPage
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed deterministic content for E2E checks."
|
||||
@@ -181,4 +184,12 @@ class Command(BaseCommand):
|
||||
]
|
||||
)
|
||||
|
||||
# Admin user for E2E admin tests
|
||||
if not User.objects.filter(username="e2e-admin").exists():
|
||||
User.objects.create_superuser(
|
||||
username="e2e-admin",
|
||||
email="e2e-admin@example.com",
|
||||
password="e2e-admin-pass",
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Seeded E2E content."))
|
||||
|
||||
56
e2e/test_admin_experience.py
Normal file
56
e2e/test_admin_experience.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""E2E tests for Wagtail admin editor experience improvements."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
def admin_login(page: Page, base_url: str) -> None:
|
||||
"""Log in to the Wagtail admin using the seeded E2E admin user."""
|
||||
page.goto(f"{base_url}/cms/login/", wait_until="networkidle")
|
||||
page.fill('input[name="username"]', "e2e-admin")
|
||||
page.fill('input[name="password"]', "e2e-admin-pass")
|
||||
page.click('button[type="submit"]')
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_articles_menu_item_visible(page: Page, base_url: str) -> None:
|
||||
"""The admin sidebar should contain an 'Articles' menu item."""
|
||||
admin_login(page, base_url)
|
||||
sidebar = page.locator("[data-side-panel]").first
|
||||
articles_link = sidebar.get_by_role("link", name="Articles")
|
||||
expect(articles_link).to_be_visible()
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_articles_listing_page_loads(page: Page, base_url: str) -> None:
|
||||
"""Clicking 'Articles' should load the articles listing with seeded articles."""
|
||||
admin_login(page, base_url)
|
||||
page.goto(f"{base_url}/cms/articles/", wait_until="networkidle")
|
||||
expect(page.get_by_role("heading").first).to_be_visible()
|
||||
# Seeded articles should appear
|
||||
expect(page.get_by_text("Nightly Playwright Journey")).to_be_visible()
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_dashboard_has_articles_panel(page: Page, base_url: str) -> None:
|
||||
"""The admin dashboard should include the articles summary panel."""
|
||||
admin_login(page, base_url)
|
||||
page.goto(f"{base_url}/cms/", wait_until="networkidle")
|
||||
expect(page.get_by_text("Articles overview")).to_be_visible()
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_article_editor_has_tabs(page: Page, base_url: str) -> None:
|
||||
"""The article editor should have Content, Metadata, Publishing, and SEO tabs."""
|
||||
admin_login(page, base_url)
|
||||
page.goto(f"{base_url}/cms/articles/", wait_until="networkidle")
|
||||
# Click the first article to edit it
|
||||
page.get_by_text("Nightly Playwright Journey").first.click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
expect(page.get_by_role("tab", name="Content")).to_be_visible()
|
||||
expect(page.get_by_role("tab", name="Metadata")).to_be_visible()
|
||||
expect(page.get_by_role("tab", name="Publishing")).to_be_visible()
|
||||
expect(page.get_by_role("tab", name="SEO")).to_be_visible()
|
||||
62
templates/blog/panels/articles_summary.html
Normal file
62
templates/blog/panels/articles_summary.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% load wagtailadmin_tags %}
|
||||
<section class="nice-padding">
|
||||
<h2 class="visuallyhidden">Articles overview</h2>
|
||||
|
||||
{% if drafts %}
|
||||
<div class="w-mb-4">
|
||||
<h3><svg class="icon icon-doc-empty" aria-hidden="true"><use href="#icon-doc-empty"></use></svg> Drafts</h3>
|
||||
<table class="listing">
|
||||
<tbody>
|
||||
{% for page in drafts %}
|
||||
<tr>
|
||||
<td class="title">
|
||||
<a href="{% url 'wagtailadmin_pages:edit' page.pk %}">{{ page.title }}</a>
|
||||
</td>
|
||||
<td>{{ page.latest_revision_created_at|timesince }} ago</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if scheduled %}
|
||||
<div class="w-mb-4">
|
||||
<h3><svg class="icon icon-time" aria-hidden="true"><use href="#icon-time"></use></svg> Scheduled</h3>
|
||||
<table class="listing">
|
||||
<tbody>
|
||||
{% for page in scheduled %}
|
||||
<tr>
|
||||
<td class="title">
|
||||
<a href="{% url 'wagtailadmin_pages:edit' page.pk %}">{{ page.title }}</a>
|
||||
</td>
|
||||
<td>{{ page.go_live_at|date:"N j, Y H:i" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if recent %}
|
||||
<div class="w-mb-4">
|
||||
<h3><svg class="icon icon-doc-full" aria-hidden="true"><use href="#icon-doc-full"></use></svg> Recently published</h3>
|
||||
<table class="listing">
|
||||
<tbody>
|
||||
{% for page in recent %}
|
||||
<tr>
|
||||
<td class="title">
|
||||
<a href="{% url 'wagtailadmin_pages:edit' page.pk %}">{{ page.title }}</a>
|
||||
</td>
|
||||
<td>{{ page.published_date|timesince }} ago</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not drafts and not scheduled and not recent %}
|
||||
<p>No articles yet. <a href="{% url 'articles:choose_parent' %}">Create one</a>.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
Reference in New Issue
Block a user