Complete missing UX flows and production integrity commands
All checks were successful
CI / ci (pull_request) Successful in 32s
All checks were successful
CI / ci (pull_request) Successful in 32s
This commit is contained in:
@@ -59,3 +59,36 @@ def test_article_page_related_context(client, home_page):
|
||||
resp = client.get("/articles/main/")
|
||||
assert resp.status_code == 200
|
||||
assert "related_articles" in resp.context
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_newsletter_forms_render_in_nav_and_footer(client, home_page):
|
||||
resp = client.get("/")
|
||||
html = resp.content.decode()
|
||||
assert resp.status_code == 200
|
||||
assert 'name="source" value="nav"' in html
|
||||
assert 'name="source" value="footer"' in html
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_article_page_renders_share_links_and_newsletter_form(client, home_page):
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
article = ArticlePage(
|
||||
title="Main",
|
||||
slug="main",
|
||||
author=author,
|
||||
summary="summary",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
)
|
||||
index.add_child(instance=article)
|
||||
article.save_revision().publish()
|
||||
|
||||
resp = client.get("/articles/main/")
|
||||
html = resp.content.decode()
|
||||
assert resp.status_code == 200
|
||||
assert "Share on X" in html
|
||||
assert "Share on LinkedIn" in html
|
||||
assert 'data-copy-link' in html
|
||||
assert 'name="source" value="article"' in html
|
||||
|
||||
1
apps/comments/management/__init__.py
Normal file
1
apps/comments/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
apps/comments/management/commands/__init__.py
Normal file
1
apps/comments/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
31
apps/comments/management/commands/purge_old_comment_data.py
Normal file
31
apps/comments/management/commands/purge_old_comment_data.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.comments.models import Comment
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Nullify comment personal data for comments older than the retention window."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--months",
|
||||
type=int,
|
||||
default=24,
|
||||
help="Retention window in months before personal data is purged (default: 24).",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
months = options["months"]
|
||||
cutoff = timezone.now() - timedelta(days=30 * months)
|
||||
|
||||
purged = (
|
||||
Comment.objects.filter(created_at__lt=cutoff)
|
||||
.exclude(author_email="")
|
||||
.update(author_email="", ip_address=None)
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"Purged personal data for {purged} comment(s)."))
|
||||
40
apps/comments/tests/test_commands.py
Normal file
40
apps/comments/tests/test_commands.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.blog.models import ArticleIndexPage, ArticlePage
|
||||
from apps.blog.tests.factories import AuthorFactory
|
||||
from apps.comments.models import Comment
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_purge_old_comment_data_clears_personal_fields(home_page):
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
article = ArticlePage(
|
||||
title="Article",
|
||||
slug="article",
|
||||
author=author,
|
||||
summary="summary",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
)
|
||||
index.add_child(instance=article)
|
||||
article.save_revision().publish()
|
||||
|
||||
old_comment = Comment.objects.create(
|
||||
article=article,
|
||||
author_name="Old",
|
||||
author_email="old@example.com",
|
||||
body="legacy",
|
||||
ip_address="127.0.0.1",
|
||||
)
|
||||
Comment.objects.filter(pk=old_comment.pk).update(created_at=timezone.now() - timedelta(days=800))
|
||||
|
||||
call_command("purge_old_comment_data")
|
||||
|
||||
old_comment.refresh_from_db()
|
||||
assert old_comment.author_email == ""
|
||||
assert old_comment.ip_address is None
|
||||
@@ -27,6 +27,7 @@ def test_comment_post_flow(client, home_page):
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert resp["Location"].endswith("?commented=1")
|
||||
assert Comment.objects.count() == 1
|
||||
|
||||
|
||||
|
||||
1
apps/core/management/__init__.py
Normal file
1
apps/core/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
apps/core/management/commands/__init__.py
Normal file
1
apps/core/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
42
apps/core/management/commands/check_content_integrity.py
Normal file
42
apps/core/management/commands/check_content_integrity.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db.models.functions import Trim
|
||||
from wagtail.models import Site
|
||||
|
||||
from apps.blog.models import ArticlePage
|
||||
from apps.core.models import SiteSettings
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Validate content-integrity constraints for live article pages."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
errors: list[str] = []
|
||||
|
||||
missing_summary = ArticlePage.objects.live().annotate(summary_trimmed=Trim("summary")).filter(
|
||||
summary_trimmed=""
|
||||
)
|
||||
if missing_summary.exists():
|
||||
errors.append(f"{missing_summary.count()} live article(s) have an empty summary.")
|
||||
|
||||
missing_author = ArticlePage.objects.live().filter(author__isnull=True)
|
||||
if missing_author.exists():
|
||||
errors.append(f"{missing_author.count()} live article(s) have no author.")
|
||||
|
||||
default_site = Site.objects.filter(is_default_site=True).first()
|
||||
default_og_image = None
|
||||
if default_site:
|
||||
default_og_image = SiteSettings.for_site(default_site).default_og_image
|
||||
|
||||
if default_og_image is None:
|
||||
missing_hero = ArticlePage.objects.live().filter(hero_image__isnull=True)
|
||||
if missing_hero.exists():
|
||||
errors.append(
|
||||
f"{missing_hero.count()} live article(s) have no hero image and no site default OG image is set."
|
||||
)
|
||||
|
||||
if errors:
|
||||
raise CommandError("Content integrity check failed: " + " ".join(errors))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Content integrity check passed."))
|
||||
30
apps/core/tests/test_commands.py
Normal file
30
apps/core/tests/test_commands.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
from apps.blog.models import ArticleIndexPage, ArticlePage
|
||||
from apps.blog.tests.factories import AuthorFactory
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_check_content_integrity_passes_when_requirements_met(home_page):
|
||||
call_command("check_content_integrity")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_check_content_integrity_fails_for_blank_summary(home_page):
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
article = ArticlePage(
|
||||
title="Article",
|
||||
slug="article",
|
||||
author=author,
|
||||
summary=" ",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
)
|
||||
index.add_child(instance=article)
|
||||
article.save_revision().publish()
|
||||
|
||||
with pytest.raises(CommandError, match="empty summary"):
|
||||
call_command("check_content_integrity")
|
||||
Reference in New Issue
Block a user