Replace static nav/footer links with Wagtail-managed NavigationMenuItem and SocialMediaLink orderables on SiteSettings. Unpublished pages are automatically excluded from rendering, fixing the dead-link problem. - Extend SiteSettings with site_name, tagline, footer_description, copyright_text branding fields - Add NavigationMenuItem orderable (link_page/link_url, show_in_header, show_in_footer, sort_order) with automatic live-page filtering - Add SocialMediaLink orderable with platform icon templates - New template tags: get_nav_items, get_social_links - Update nav.html and footer.html to render from CMS data - Data migration seeds existing hardcoded values for zero-change deploy - Update seed_e2e_content command for test/dev environments - 18 new tests covering models, template tags, and rendered output Closes #32 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
192 lines
6.7 KiB
Python
192 lines
6.7 KiB
Python
import pytest
|
|
from wagtail.models import Site
|
|
|
|
from apps.blog.models import AboutPage, ArticleIndexPage, HomePage
|
|
from apps.core.models import NavigationMenuItem, SiteSettings, SocialMediaLink
|
|
|
|
|
|
@pytest.fixture
|
|
def site_with_nav(home_page):
|
|
"""Create SiteSettings with nav items and social links for testing."""
|
|
site = Site.objects.get(is_default_site=True)
|
|
settings, _ = SiteSettings.objects.get_or_create(site=site)
|
|
|
|
# Clear any items seeded by the data migration
|
|
settings.navigation_items.all().delete()
|
|
settings.social_links.all().delete()
|
|
|
|
# Create article index and about page
|
|
article_index = ArticleIndexPage(title="Articles", slug="articles")
|
|
home_page.add_child(instance=article_index)
|
|
article_index.save_revision().publish()
|
|
|
|
about = AboutPage(
|
|
title="About",
|
|
slug="about",
|
|
mission_statement="Test mission",
|
|
body="<p>About page</p>",
|
|
)
|
|
home_page.add_child(instance=about)
|
|
about.save_revision().publish()
|
|
|
|
# Create nav items
|
|
NavigationMenuItem.objects.create(
|
|
settings=settings,
|
|
link_page=home_page,
|
|
link_title="Home",
|
|
show_in_header=True,
|
|
show_in_footer=True,
|
|
sort_order=0,
|
|
)
|
|
NavigationMenuItem.objects.create(
|
|
settings=settings,
|
|
link_page=article_index,
|
|
link_title="Articles",
|
|
show_in_header=True,
|
|
show_in_footer=True,
|
|
sort_order=1,
|
|
)
|
|
NavigationMenuItem.objects.create(
|
|
settings=settings,
|
|
link_page=about,
|
|
link_title="About",
|
|
show_in_header=True,
|
|
show_in_footer=False,
|
|
sort_order=2,
|
|
)
|
|
|
|
# Social links
|
|
SocialMediaLink.objects.create(
|
|
settings=settings,
|
|
platform="twitter",
|
|
url="https://twitter.com/nohypeai",
|
|
label="Twitter (X)",
|
|
sort_order=0,
|
|
)
|
|
SocialMediaLink.objects.create(
|
|
settings=settings,
|
|
platform="rss",
|
|
url="/feed/",
|
|
label="RSS Feed",
|
|
sort_order=1,
|
|
)
|
|
|
|
return settings
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestNavigationMenuItem:
|
|
def test_live_page_is_rendered(self, site_with_nav):
|
|
items = [i for i in site_with_nav.navigation_items.all() if i.is_live]
|
|
assert len(items) == 3
|
|
|
|
def test_unpublished_page_excluded(self, site_with_nav):
|
|
about_item = site_with_nav.navigation_items.get(link_title="About")
|
|
about_item.link_page.unpublish()
|
|
items = [i for i in site_with_nav.navigation_items.all() if i.is_live]
|
|
assert len(items) == 2
|
|
assert all(i.link_title != "About" for i in items)
|
|
|
|
def test_external_url_item(self, site_with_nav):
|
|
NavigationMenuItem.objects.create(
|
|
settings=site_with_nav,
|
|
link_url="https://example.com",
|
|
link_title="External",
|
|
sort_order=10,
|
|
)
|
|
item = site_with_nav.navigation_items.get(link_title="External")
|
|
assert item.is_live is True
|
|
assert item.url == "https://example.com"
|
|
assert item.title == "External"
|
|
|
|
def test_title_falls_back_to_page_title(self, site_with_nav):
|
|
item = site_with_nav.navigation_items.get(sort_order=0)
|
|
item.link_title = ""
|
|
item.save()
|
|
assert item.title == item.link_page.title
|
|
|
|
def test_header_footer_filtering(self, site_with_nav):
|
|
header_items = site_with_nav.navigation_items.filter(show_in_header=True)
|
|
footer_items = site_with_nav.navigation_items.filter(show_in_footer=True)
|
|
assert header_items.count() == 3
|
|
assert footer_items.count() == 2 # About excluded from footer
|
|
|
|
def test_sort_order_respected(self, site_with_nav):
|
|
items = list(site_with_nav.navigation_items.all().order_by("sort_order"))
|
|
assert [i.link_title for i in items] == ["Home", "Articles", "About"]
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestSocialMediaLink:
|
|
def test_display_label_from_field(self, site_with_nav):
|
|
link = site_with_nav.social_links.get(platform="twitter")
|
|
assert link.display_label == "Twitter (X)"
|
|
|
|
def test_display_label_fallback(self, site_with_nav):
|
|
link = site_with_nav.social_links.get(platform="twitter")
|
|
link.label = ""
|
|
assert link.display_label == "Twitter / X"
|
|
|
|
def test_icon_template_path(self, site_with_nav):
|
|
link = site_with_nav.social_links.get(platform="rss")
|
|
assert link.icon_template == "components/icons/rss.html"
|
|
|
|
def test_ordering(self, site_with_nav):
|
|
links = list(site_with_nav.social_links.all().order_by("sort_order"))
|
|
assert [l.platform for l in links] == ["twitter", "rss"]
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestSiteSettingsDefaults:
|
|
def test_default_site_name(self, home_page):
|
|
site = Site.objects.get(is_default_site=True)
|
|
settings, _ = SiteSettings.objects.get_or_create(site=site)
|
|
assert settings.site_name == "NO HYPE AI"
|
|
|
|
def test_default_copyright(self, home_page):
|
|
site = Site.objects.get(is_default_site=True)
|
|
settings, _ = SiteSettings.objects.get_or_create(site=site)
|
|
assert settings.copyright_text == "No Hype AI. All rights reserved."
|
|
|
|
def test_default_tagline(self, home_page):
|
|
site = Site.objects.get(is_default_site=True)
|
|
settings, _ = SiteSettings.objects.get_or_create(site=site)
|
|
assert settings.tagline == "Honest AI tool reviews for developers."
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestNavRendering:
|
|
def test_header_shows_nav_items(self, client, site_with_nav):
|
|
resp = client.get("/")
|
|
content = resp.content.decode()
|
|
assert "Home" in content
|
|
assert "Articles" in content
|
|
assert "About" in content
|
|
|
|
def test_unpublished_page_not_in_header(self, client, site_with_nav):
|
|
about_item = site_with_nav.navigation_items.get(link_title="About")
|
|
about_item.link_page.unpublish()
|
|
resp = client.get("/")
|
|
content = resp.content.decode()
|
|
# About should not appear as a nav link (but might appear elsewhere on page)
|
|
assert 'href="/about/"' not in content
|
|
|
|
def test_footer_shows_nav_items(self, client, site_with_nav):
|
|
resp = client.get("/")
|
|
content = resp.content.decode()
|
|
# Footer should have social links
|
|
assert "Twitter (X)" in content
|
|
assert "RSS Feed" in content
|
|
|
|
def test_footer_shows_branding(self, client, site_with_nav):
|
|
site_with_nav.site_name = "TEST SITE"
|
|
site_with_nav.save()
|
|
resp = client.get("/")
|
|
content = resp.content.decode()
|
|
assert "TEST SITE" in content
|
|
|
|
def test_footer_shows_copyright(self, client, site_with_nav):
|
|
resp = client.get("/")
|
|
content = resp.content.decode()
|
|
assert "No Hype AI. All rights reserved." in content
|