Auto slug, auto summary/SEO, and deterministic tag colours
Issue #61: Strengthen auto-generation for slug, summary, and SEO fields. - ArticlePage.save() now auto-generates slug from title when empty - ArticlePage.save() auto-populates search_description from summary - Admin form also auto-populates search_description from summary Issue #63: Replace manual TagMetadata colour assignment with deterministic hash-based auto-colour. Tags get a consistent colour from a 12-entry palette without needing a TagMetadata snippet. TagMetadata still works as an explicit override. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,15 @@ import pytest
|
||||
from django.db import IntegrityError
|
||||
from taggit.models import Tag
|
||||
|
||||
from apps.blog.models import ArticleIndexPage, ArticlePage, Category, HomePage, TagMetadata
|
||||
from apps.blog.models import (
|
||||
TAG_COLOUR_PALETTE,
|
||||
ArticleIndexPage,
|
||||
ArticlePage,
|
||||
Category,
|
||||
HomePage,
|
||||
TagMetadata,
|
||||
get_auto_tag_colour_css,
|
||||
)
|
||||
from apps.blog.tests.factories import AuthorFactory
|
||||
|
||||
|
||||
@@ -92,3 +100,108 @@ def test_category_ordering():
|
||||
Category.objects.create(name="A", slug="a", sort_order=1)
|
||||
names = list(Category.objects.values_list("name", flat=True))
|
||||
assert names == ["General", "A", "Z"]
|
||||
|
||||
|
||||
# ── Auto tag colour tests ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_auto_tag_colour_is_deterministic():
|
||||
"""Same tag name always produces the same colour."""
|
||||
css1 = get_auto_tag_colour_css("python")
|
||||
css2 = get_auto_tag_colour_css("python")
|
||||
assert css1 == css2
|
||||
|
||||
|
||||
def test_auto_tag_colour_is_case_insensitive():
|
||||
"""Tag colour assignment is case-insensitive."""
|
||||
assert get_auto_tag_colour_css("Python") == get_auto_tag_colour_css("python")
|
||||
|
||||
|
||||
def test_auto_tag_colour_returns_valid_palette_entry():
|
||||
"""Returned CSS dict must be from the palette."""
|
||||
css = get_auto_tag_colour_css("llms")
|
||||
assert css in TAG_COLOUR_PALETTE
|
||||
|
||||
|
||||
def test_auto_tag_colour_distributes_across_palette():
|
||||
"""Different tag names should map to multiple palette entries."""
|
||||
sample_tags = ["python", "javascript", "rust", "go", "ruby", "java",
|
||||
"typescript", "css", "html", "sql", "llms", "mlops"]
|
||||
colours = {get_auto_tag_colour_css(t)["text"] for t in sample_tags}
|
||||
assert len(colours) >= 3, "Tags should spread across at least 3 palette colours"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_tag_without_metadata_uses_auto_colour():
|
||||
"""Tags without TagMetadata should get auto-assigned colour, not neutral."""
|
||||
tag = Tag.objects.create(name="fastapi", slug="fastapi")
|
||||
expected = get_auto_tag_colour_css("fastapi")
|
||||
# Verify no metadata exists
|
||||
assert not TagMetadata.objects.filter(tag=tag).exists()
|
||||
# The template tag helper should fall back to auto colour
|
||||
from apps.core.templatetags.core_tags import _resolve_tag_css
|
||||
assert _resolve_tag_css(tag) == expected
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_tag_with_metadata_overrides_auto_colour():
|
||||
"""Tags with explicit TagMetadata should use that colour."""
|
||||
tag = Tag.objects.create(name="django", slug="django")
|
||||
TagMetadata.objects.create(tag=tag, colour="pink")
|
||||
from apps.core.templatetags.core_tags import _resolve_tag_css
|
||||
css = _resolve_tag_css(tag)
|
||||
assert css["text"] == "text-brand-pink"
|
||||
|
||||
|
||||
# ── Auto slug tests ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_article_save_auto_generates_slug_from_title(home_page):
|
||||
"""Model save should auto-generate slug from title when slug is empty."""
|
||||
index = ArticleIndexPage(title="Articles", slug="articles")
|
||||
home_page.add_child(instance=index)
|
||||
author = AuthorFactory()
|
||||
article = ArticlePage(
|
||||
title="My Great Article",
|
||||
slug="my-great-article",
|
||||
author=author,
|
||||
summary="summary",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
)
|
||||
index.add_child(instance=article)
|
||||
article.save()
|
||||
assert article.slug == "my-great-article"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_article_save_auto_generates_search_description(article_index):
|
||||
"""Model save should populate search_description from summary."""
|
||||
author = AuthorFactory()
|
||||
article = ArticlePage(
|
||||
title="SEO Auto",
|
||||
slug="seo-auto",
|
||||
author=author,
|
||||
summary="This is the article summary.",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
)
|
||||
article_index.add_child(instance=article)
|
||||
article.save()
|
||||
assert article.search_description == "This is the article summary."
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_article_save_preserves_explicit_search_description(article_index):
|
||||
"""Explicit search_description should not be overwritten."""
|
||||
author = AuthorFactory()
|
||||
article = ArticlePage(
|
||||
title="SEO Explicit",
|
||||
slug="seo-explicit",
|
||||
author=author,
|
||||
summary="Generated summary.",
|
||||
search_description="Custom SEO description.",
|
||||
body=[("rich_text", "<p>body</p>")],
|
||||
)
|
||||
article_index.add_child(instance=article)
|
||||
article.save()
|
||||
assert article.search_description == "Custom SEO description."
|
||||
|
||||
Reference in New Issue
Block a user