from datetime import timedelta from types import SimpleNamespace import pytest from django.contrib import messages from django.contrib.messages.storage.fallback import FallbackStorage from django.contrib.sessions.middleware import SessionMiddleware 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", "

body

")], ) 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", "

body

")], 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", "

body

")], 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", "

body

")], 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", "

b

")], 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", "

b

")], 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", "

body

")], ) 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", "

body

")], ) 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", "

body

")], ) 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 populate defaults before parent validation runs.""" 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="

Hello world body text.

")), ] form.cleaned_data = { "title": "Auto Defaults Title", "slug": "", "author": None, "category": None, "summary": "", "body": body, } observed = {} def fake_super_clean(_self): observed["slug_before_parent_clean"] = _self.cleaned_data.get("slug") 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 observed["slug_before_parent_clean"] == "auto-defaults-title" 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", "

This should become the summary text.

"), ], ) article_index.add_child(instance=article) article.save() assert article.summary == "This should become the summary text." @pytest.mark.django_db def test_article_page_omits_admin_messages_on_frontend(article_page, rf): """Frontend templates should not render admin session messages.""" request = rf.get(article_page.url) SessionMiddleware(lambda req: None).process_request(request) request.session.save() setattr(request, "_messages", FallbackStorage(request)) messages.success(request, "Page 'Test' has been published.") response = article_page.serve(request) response.render() content = response.content.decode() assert "Page 'Test' has been published." not in content assert 'aria-label="Messages"' not in content