Format multilingual audit extraction for CI lint
This commit is contained in:
@@ -20,7 +20,9 @@ from mandelblog_content_guard.mixins import MultilingualValidationMixin
|
||||
|
||||
|
||||
@register_snippet
|
||||
class LocalizedFooterContent(MultilingualValidationMixin, TranslatableMixin, models.Model):
|
||||
class LocalizedFooterContent(
|
||||
MultilingualValidationMixin, TranslatableMixin, models.Model
|
||||
):
|
||||
title = models.CharField(max_length=120, default="Footer content")
|
||||
site = models.ForeignKey(
|
||||
Site, on_delete=models.CASCADE, related_name="localized_footer_contents"
|
||||
|
||||
@@ -77,5 +77,7 @@ CONTENT_GUARD_REWRITE_BACKEND = None
|
||||
|
||||
if "test" in sys.argv:
|
||||
MIGRATION_MODULES = globals().get("MIGRATION_MODULES", {}).copy()
|
||||
MIGRATION_MODULES["template_engine"] = "mandelstudio.test_migrations.template_engine"
|
||||
MIGRATION_MODULES["template_engine"] = (
|
||||
"mandelstudio.test_migrations.template_engine"
|
||||
)
|
||||
TEST_RUNNER = "django.test.runner.DiscoverRunner"
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0001_initial").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0001_initial"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0002_templateenginesitesettings").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0002_templateenginesitesettings"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0003_templateenginesitesettings_nav_items").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0003_templateenginesitesettings_nav_items"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0004_alter_basehomepage_body_alter_basestandardpage_body").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0004_alter_basehomepage_body_alter_basestandardpage_body"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0005_templateenginesitesettings_header_variant_and_more").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0005_templateenginesitesettings_header_variant_and_more"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0006_templateenginesitesettings_footer_dynamic_fields").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0006_templateenginesitesettings_footer_dynamic_fields"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0007_templateenginesitesettings_header_cta_fields").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0007_templateenginesitesettings_header_cta_fields"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0008_templateenginesitesettings_footer_bottom_links_and_more").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0008_templateenginesitesettings_footer_bottom_links_and_more"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0009_alter_basehomepage_body_alter_basestandardpage_body_and_more").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0009_alter_basehomepage_body_alter_basestandardpage_body_and_more"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0010_enginepage_and_more").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0010_enginepage_and_more"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0011_alter_basehomepage_body_alter_basestandardpage_body_and_more").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0011_alter_basehomepage_body_alter_basestandardpage_body_and_more"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0012_alter_basehomepage_body_alter_basestandardpage_body_and_more").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0012_alter_basehomepage_body_alter_basestandardpage_body_and_more"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0013_engineblockpreset").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0013_engineblockpreset"
|
||||
).Migration
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0014_alter_basehomepage_body_alter_basestandardpage_body_and_more").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0014_alter_basehomepage_body_alter_basestandardpage_body_and_more"
|
||||
).Migration
|
||||
|
||||
@@ -13,7 +13,10 @@ def _ensure_navitem_table(apps, schema_editor):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("template_engine", "0014_alter_basehomepage_body_alter_basestandardpage_body_and_more"),
|
||||
(
|
||||
"template_engine",
|
||||
"0014_alter_basehomepage_body_alter_basestandardpage_body_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
from importlib import import_module
|
||||
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0016_alter_basehomepage_body_alter_basestandardpage_body_and_more").Migration
|
||||
|
||||
Migration = import_module(
|
||||
"ocyan.plugin.template_engine.engine.migrations.0016_alter_basehomepage_body_alter_basestandardpage_body_and_more"
|
||||
).Migration
|
||||
|
||||
@@ -9,39 +9,66 @@ from django.test import SimpleTestCase, override_settings
|
||||
|
||||
from mandelblog_content_guard.agents import get_language_agent
|
||||
from mandelblog_content_guard.ai import rewrite_ai_output
|
||||
from mandelblog_content_guard.system_strings import build_system_rewrite_candidates, build_system_vocabulary
|
||||
from mandelblog_content_guard.system_strings import (
|
||||
build_system_rewrite_candidates,
|
||||
build_system_vocabulary,
|
||||
)
|
||||
from mandelblog_content_guard.types import split_issues
|
||||
from mandelblog_content_guard.validators.multilingual import extract_visible_rendered_text, validate_text_nodes
|
||||
from mandelblog_content_guard.validators.multilingual import (
|
||||
extract_visible_rendered_text,
|
||||
validate_text_nodes,
|
||||
)
|
||||
|
||||
|
||||
class ContentGuardRuleTests(SimpleTestCase):
|
||||
def test_mixed_language_detection_blocks(self):
|
||||
issues = validate_text_nodes(
|
||||
"pt",
|
||||
[("body.hero_text", 'Poiché l\'input "Unverbindliche Erstberatung" è in tedesco')],
|
||||
[
|
||||
(
|
||||
"body.hero_text",
|
||||
'Poiché l\'input "Unverbindliche Erstberatung" è in tedesco',
|
||||
)
|
||||
],
|
||||
)
|
||||
blocking, _warnings = split_issues(issues)
|
||||
self.assertTrue(blocking)
|
||||
self.assertTrue(any(issue.issue_type == "known_bad_pattern" for issue in blocking))
|
||||
self.assertTrue(
|
||||
any(issue.issue_type == "known_bad_pattern" for issue in blocking)
|
||||
)
|
||||
|
||||
def test_cta_mismatch_detection_blocks(self):
|
||||
issues = validate_text_nodes("en", [("body.cta_text", "Plan kennismaking")])
|
||||
blocking, _warnings = split_issues(issues)
|
||||
self.assertTrue(any(issue.issue_type == "cta_language_mismatch" for issue in blocking))
|
||||
self.assertTrue(
|
||||
any(issue.issue_type == "cta_language_mismatch" for issue in blocking)
|
||||
)
|
||||
|
||||
def test_form_validation_blocks_wrong_language(self):
|
||||
issues = validate_text_nodes("ru", [("body.form.label", "Correo electrónico")])
|
||||
blocking, _warnings = split_issues(issues)
|
||||
self.assertTrue(any(issue.issue_type in {"known_bad_pattern", "form_language_mismatch"} for issue in blocking))
|
||||
self.assertTrue(
|
||||
any(
|
||||
issue.issue_type in {"known_bad_pattern", "form_language_mismatch"}
|
||||
for issue in blocking
|
||||
)
|
||||
)
|
||||
|
||||
@override_settings(CONTENT_GUARD_BLOCK_MEDIUM=True)
|
||||
def test_medium_can_be_blocked_in_strict_mode(self):
|
||||
issues = validate_text_nodes(
|
||||
"en",
|
||||
[("body.summary", "le la les et avec pour vous une pas des extra words to trigger heuristic")],
|
||||
[
|
||||
(
|
||||
"body.summary",
|
||||
"le la les et avec pour vous une pas des extra words to trigger heuristic",
|
||||
)
|
||||
],
|
||||
)
|
||||
blocking, _warnings = split_issues(issues)
|
||||
self.assertTrue(any(issue.issue_type == "language_heuristic" for issue in blocking))
|
||||
self.assertTrue(
|
||||
any(issue.issue_type == "language_heuristic" for issue in blocking)
|
||||
)
|
||||
|
||||
def test_language_agent_registry(self):
|
||||
agent = get_language_agent("pt")
|
||||
@@ -56,7 +83,9 @@ class ContentGuardRuleTests(SimpleTestCase):
|
||||
def test_portuguese_agent_contextual_badge_rewrite(self):
|
||||
agent = get_language_agent("pt")
|
||||
self.assertEqual(agent.rewrite("SERVICES", "body.cards[0].badge"), "SERVIÇOS")
|
||||
self.assertEqual(agent.rewrite("Transparent", "body.metrics[0].label"), "Investimento claro")
|
||||
self.assertEqual(
|
||||
agent.rewrite("Transparent", "body.metrics[0].label"), "Investimento claro"
|
||||
)
|
||||
|
||||
def test_french_agent_contextual_badge_rewrite(self):
|
||||
agent = get_language_agent("fr")
|
||||
@@ -66,8 +95,12 @@ class ContentGuardRuleTests(SimpleTestCase):
|
||||
def test_german_agent_normalizes_non_system_copy(self):
|
||||
agent = get_language_agent("de")
|
||||
self.assertEqual(agent.rewrite("New", "body.cards[0].badge"), "Neu")
|
||||
self.assertEqual(agent.rewrite("Intakegespräch", "body.stats[0].label"), "Erstgespräch")
|
||||
self.assertEqual(agent.rewrite("Was du bekommst", "body.heading"), "Was Sie erhalten")
|
||||
self.assertEqual(
|
||||
agent.rewrite("Intakegespräch", "body.stats[0].label"), "Erstgespräch"
|
||||
)
|
||||
self.assertEqual(
|
||||
agent.rewrite("Was du bekommst", "body.heading"), "Was Sie erhalten"
|
||||
)
|
||||
self.assertEqual(
|
||||
agent.rewrite("Sales-ready mit skalierbarem Stack", "body.cards[0].text"),
|
||||
"Verkaufsbereit mit skalierbarer Architektur",
|
||||
@@ -91,9 +124,16 @@ class ContentGuardRuleTests(SimpleTestCase):
|
||||
def test_portuguese_rewrite_candidates_are_detected(self):
|
||||
issues = validate_text_nodes(
|
||||
"pt",
|
||||
[("body.hero_text", "Siti web e negozi online che sono rapidamente online e facili da gestire")],
|
||||
[
|
||||
(
|
||||
"body.hero_text",
|
||||
"Siti web e negozi online che sono rapidamente online e facili da gestire",
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertTrue(
|
||||
any(issue.issue_type == "mixed_locale_heading" for issue in issues)
|
||||
)
|
||||
self.assertTrue(any(issue.issue_type == "mixed_locale_heading" for issue in issues))
|
||||
|
||||
def test_french_foreign_ui_label_is_detected(self):
|
||||
issues = validate_text_nodes(
|
||||
@@ -105,9 +145,14 @@ class ContentGuardRuleTests(SimpleTestCase):
|
||||
def test_de_canonical_system_strings_are_not_rewrite_candidates(self):
|
||||
issues = validate_text_nodes(
|
||||
"de",
|
||||
[("body.metric_label", "Durchschnittliche Lieferung"), ("body.badge", "PLAN")],
|
||||
[
|
||||
("body.metric_label", "Durchschnittliche Lieferung"),
|
||||
("body.badge", "PLAN"),
|
||||
],
|
||||
)
|
||||
self.assertFalse(
|
||||
any(issue.bad_value == "Durchschnittliche Lieferung" for issue in issues)
|
||||
)
|
||||
self.assertFalse(any(issue.bad_value == "Durchschnittliche Lieferung" for issue in issues))
|
||||
self.assertFalse(any(issue.bad_value == "PLAN" for issue in issues))
|
||||
|
||||
def test_extract_visible_rendered_text_ignores_hidden_script_and_style(self):
|
||||
@@ -131,21 +176,40 @@ class ContentGuardRuleTests(SimpleTestCase):
|
||||
|
||||
def test_system_strings_are_centralized_for_fr_and_pt(self):
|
||||
self.assertEqual(build_system_vocabulary("fr")["PLAN"], "FORFAIT")
|
||||
self.assertEqual(build_system_vocabulary("fr")["Reaktionszeit"], "Temps de réponse")
|
||||
self.assertEqual(
|
||||
build_system_vocabulary("fr")["Reaktionszeit"], "Temps de réponse"
|
||||
)
|
||||
self.assertEqual(build_system_vocabulary("pt")["Transparent"], "Transparente")
|
||||
self.assertEqual(build_system_vocabulary("fr")["Transparente Investition"], "Investissement transparent")
|
||||
self.assertEqual(build_system_vocabulary("pt")["Transparente Investition"], "Investimento transparente")
|
||||
self.assertEqual(build_system_rewrite_candidates()["Durchschnittliche Lieferung"], "foreign_ui_label")
|
||||
self.assertEqual(
|
||||
build_system_vocabulary("fr")["Transparente Investition"],
|
||||
"Investissement transparent",
|
||||
)
|
||||
self.assertEqual(
|
||||
build_system_vocabulary("pt")["Transparente Investition"],
|
||||
"Investimento transparente",
|
||||
)
|
||||
self.assertEqual(
|
||||
build_system_rewrite_candidates()["Durchschnittliche Lieferung"],
|
||||
"foreign_ui_label",
|
||||
)
|
||||
|
||||
|
||||
class AuditLocalesCommandTests(SimpleTestCase):
|
||||
@mock.patch("mandelblog_content_guard.management.commands.audit_locales.audit_locales")
|
||||
@mock.patch(
|
||||
"mandelblog_content_guard.management.commands.audit_locales.audit_locales"
|
||||
)
|
||||
def test_json_output(self, audit_locales_mock):
|
||||
run = mock.Mock()
|
||||
run.pk = 12
|
||||
run.total_urls_checked = 2
|
||||
run.issues_found = 1
|
||||
run.summary = {"en": {"total_urls_checked": 2, "issues_found": 1, "by_severity": {"block": 1}}}
|
||||
run.summary = {
|
||||
"en": {
|
||||
"total_urls_checked": 2,
|
||||
"issues_found": 1,
|
||||
"by_severity": {"block": 1},
|
||||
}
|
||||
}
|
||||
issue = mock.Mock(
|
||||
url="/en/contact/",
|
||||
title="Contact",
|
||||
@@ -166,16 +230,29 @@ class AuditLocalesCommandTests(SimpleTestCase):
|
||||
self.assertEqual(payload["run_id"], 12)
|
||||
self.assertEqual(payload["issues"]["en"][0]["bad_value"], "Correo electrónico")
|
||||
|
||||
@mock.patch("mandelblog_content_guard.management.commands.audit_locales.audit_locales")
|
||||
@mock.patch(
|
||||
"mandelblog_content_guard.management.commands.audit_locales.audit_locales"
|
||||
)
|
||||
def test_rewrite_flags_are_forwarded(self, audit_locales_mock):
|
||||
run = mock.Mock()
|
||||
run.pk = 13
|
||||
run.total_urls_checked = 1
|
||||
run.issues_found = 0
|
||||
run.summary = {"pt": {"total_urls_checked": 1, "issues_found": 0, "issues_fixed": 0, "by_severity": {"block": 0, "warn": 0, "log": 0}}}
|
||||
run.summary = {
|
||||
"pt": {
|
||||
"total_urls_checked": 1,
|
||||
"issues_found": 0,
|
||||
"issues_fixed": 0,
|
||||
"by_severity": {"block": 0, "warn": 0, "log": 0},
|
||||
}
|
||||
}
|
||||
run.issues.all.return_value.order_by.return_value = []
|
||||
audit_locales_mock.return_value = run
|
||||
|
||||
out = StringIO()
|
||||
call_command("audit_locales", "--locale", "pt", "--rewrite", "--dry-run", stdout=out)
|
||||
audit_locales_mock.assert_called_once_with(["pt"], fix=False, rewrite=True, dry_run=True)
|
||||
call_command(
|
||||
"audit_locales", "--locale", "pt", "--rewrite", "--dry-run", stdout=out
|
||||
)
|
||||
audit_locales_mock.assert_called_once_with(
|
||||
["pt"], fix=False, rewrite=True, dry_run=True
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user