Files
main-site/apps/blog/tests/test_admin_experience.py
codex_a 521075cf04
Some checks failed
CI / nightly-e2e (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / ci (pull_request) Failing after 1m33s
CI / pr-e2e (pull_request) Successful in 1m35s
fix(editor): auto-default article metadata and de-duplicate SEO panels
2026-03-04 22:32:14 +00:00

381 lines
13 KiB
Python

from datetime import timedelta
from types import SimpleNamespace
import pytest
from django.test import override_settings
from django.utils import timezone
from apps.blog.models import ArticleIndexPage, ArticlePage, ArticlePageAdminForm, 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() - 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() - 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() - 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
@pytest.mark.django_db
@override_settings(ALLOWED_HOSTS=["testserver", "localhost", "127.0.0.1"])
def test_articles_listing_has_status_filter(client, django_user_model, home_page):
"""The Articles listing should accept status filter parameter."""
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/?status=live")
assert response.status_code == 200
@pytest.mark.django_db
@override_settings(ALLOWED_HOSTS=["testserver", "localhost", "127.0.0.1"])
def test_articles_listing_has_tag_filter(client, django_user_model, home_page):
"""The Articles listing should accept tag filter parameter."""
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/?tag=1")
assert response.status_code == 200
@pytest.mark.django_db
def test_article_listing_default_ordering():
"""ArticlePageListingViewSet should default to -published_date ordering."""
from apps.blog.wagtail_hooks import ArticlePageListingViewSet
assert ArticlePageListingViewSet.default_ordering == "-published_date"
@pytest.mark.django_db
def test_article_search_fields_include_summary():
"""ArticlePage.search_fields should index the summary field."""
field_names = [
f.field_name for f in ArticlePage.search_fields if hasattr(f, "field_name")
]
assert "summary" in field_names
@pytest.mark.django_db
def test_article_admin_form_relaxes_initial_required_fields(article_index, django_user_model):
"""Slug/author/category/summary should not block initial draft validation."""
user = django_user_model.objects.create_user(
username="writer",
email="writer@example.com",
password="writer-pass",
)
form_class = ArticlePage.get_edit_handler().get_form_class()
form = form_class(parent_page=article_index, for_user=user)
assert form.fields["slug"].required is False
assert form.fields["author"].required is False
assert form.fields["category"].required is False
assert form.fields["summary"].required is False
@pytest.mark.django_db
def test_article_admin_form_clean_applies_defaults(article_index, django_user_model, monkeypatch):
"""Form clean should auto-fill slug/author/category/summary when omitted."""
user = django_user_model.objects.create_user(
username="writer",
email="writer@example.com",
password="writer-pass",
first_name="Writer",
last_name="User",
)
form_class = ArticlePage.get_edit_handler().get_form_class()
form = form_class(parent_page=article_index, for_user=user)
body = [
SimpleNamespace(block_type="code", value=SimpleNamespace(raw_code="print('ignore')")),
SimpleNamespace(block_type="rich_text", value=SimpleNamespace(source="<p>Hello world body text.</p>")),
]
def fake_super_clean(_self):
_self.cleaned_data = {
"title": "Auto Defaults Title",
"slug": "",
"author": None,
"category": None,
"summary": "",
"body": body,
}
return _self.cleaned_data
mro = form.__class__.__mro__
super_form_class = mro[mro.index(ArticlePageAdminForm) + 1]
monkeypatch.setattr(super_form_class, "clean", fake_super_clean)
cleaned = form.clean()
assert cleaned["slug"] == "auto-defaults-title"
assert cleaned["author"] is not None
assert cleaned["author"].user_id == user.id
assert cleaned["category"] is not None
assert cleaned["category"].slug == "general"
assert cleaned["summary"] == "Hello world body text."
@pytest.mark.django_db
def test_article_seo_tab_fields_not_duplicated():
"""SEO tab should include each promote/SEO field only once."""
handler = ArticlePage.get_edit_handler()
seo_tab = next(panel for panel in handler.children if panel.heading == "SEO")
def flatten_field_names(panel):
names = []
for child in panel.children:
if hasattr(child, "field_name"):
names.append(child.field_name)
else:
names.extend(flatten_field_names(child))
return names
field_names = flatten_field_names(seo_tab)
assert field_names.count("slug") == 1
assert field_names.count("seo_title") == 1
assert field_names.count("search_description") == 1
assert field_names.count("show_in_menus") == 1
@pytest.mark.django_db
def test_article_save_autogenerates_summary_when_missing(article_index):
"""Model save fallback should generate summary from prose blocks."""
category = Category.objects.create(name="Guides", slug="guides")
author = AuthorFactory()
article = ArticlePage(
title="Summary Auto",
slug="summary-auto",
author=author,
category=category,
summary="",
body=[
("code", {"language": "python", "filename": "", "raw_code": "print('skip')"}),
("rich_text", "<p>This should become the summary text.</p>"),
],
)
article_index.add_child(instance=article)
article.save()
assert article.summary == "This should become the summary text."