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>
174 lines
4.7 KiB
Python
174 lines
4.7 KiB
Python
from django.db import models
|
|
from django.db.models import SET_NULL
|
|
from modelcluster.fields import ParentalKey
|
|
from modelcluster.models import ClusterableModel
|
|
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
|
|
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting
|
|
from wagtail.models import Orderable
|
|
|
|
|
|
SOCIAL_ICON_CHOICES = [
|
|
("twitter", "Twitter / X"),
|
|
("github", "GitHub"),
|
|
("rss", "RSS Feed"),
|
|
("linkedin", "LinkedIn"),
|
|
("youtube", "YouTube"),
|
|
("mastodon", "Mastodon"),
|
|
("bluesky", "Bluesky"),
|
|
]
|
|
|
|
|
|
@register_setting
|
|
class SiteSettings(ClusterableModel, BaseSiteSetting):
|
|
default_og_image = models.ForeignKey(
|
|
"wagtailimages.Image",
|
|
null=True,
|
|
blank=True,
|
|
on_delete=SET_NULL,
|
|
related_name="+",
|
|
)
|
|
privacy_policy_page = models.ForeignKey(
|
|
"wagtailcore.Page",
|
|
null=True,
|
|
blank=True,
|
|
on_delete=SET_NULL,
|
|
related_name="+",
|
|
)
|
|
|
|
# Branding
|
|
site_name = models.CharField(max_length=100, default="NO HYPE AI")
|
|
tagline = models.CharField(
|
|
max_length=200,
|
|
default="Honest AI tool reviews for developers.",
|
|
)
|
|
footer_description = models.TextField(
|
|
default="In-depth reviews and benchmarks of the latest AI coding tools.\nHonest analysis for developers.",
|
|
blank=True,
|
|
)
|
|
copyright_text = models.CharField(
|
|
max_length=200,
|
|
default="No Hype AI. All rights reserved.",
|
|
)
|
|
|
|
panels = [
|
|
MultiFieldPanel(
|
|
[
|
|
FieldPanel("site_name"),
|
|
FieldPanel("tagline"),
|
|
FieldPanel("footer_description"),
|
|
FieldPanel("copyright_text"),
|
|
],
|
|
heading="Branding",
|
|
),
|
|
MultiFieldPanel(
|
|
[
|
|
FieldPanel("default_og_image"),
|
|
FieldPanel("privacy_policy_page"),
|
|
],
|
|
heading="SEO & Legal",
|
|
),
|
|
InlinePanel("navigation_items", label="Navigation Menu Items"),
|
|
InlinePanel("social_links", label="Social Media Links"),
|
|
]
|
|
|
|
|
|
class NavigationMenuItem(Orderable):
|
|
settings = ParentalKey(
|
|
SiteSettings,
|
|
on_delete=models.CASCADE,
|
|
related_name="navigation_items",
|
|
)
|
|
link_page = models.ForeignKey(
|
|
"wagtailcore.Page",
|
|
null=True,
|
|
blank=True,
|
|
on_delete=SET_NULL,
|
|
related_name="+",
|
|
help_text="Link to an internal page. If unpublished, the link is hidden automatically.",
|
|
)
|
|
link_url = models.URLField(
|
|
blank=True,
|
|
default="",
|
|
help_text="External URL (used only when no page is selected).",
|
|
)
|
|
link_title = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
default="",
|
|
help_text="Override the display text. If blank, the page title is used.",
|
|
)
|
|
open_in_new_tab = models.BooleanField(default=False)
|
|
show_in_header = models.BooleanField(default=True)
|
|
show_in_footer = models.BooleanField(default=True)
|
|
|
|
panels = [
|
|
FieldPanel("link_page"),
|
|
FieldPanel("link_url"),
|
|
FieldPanel("link_title"),
|
|
FieldPanel("open_in_new_tab"),
|
|
FieldPanel("show_in_header"),
|
|
FieldPanel("show_in_footer"),
|
|
]
|
|
|
|
@property
|
|
def title(self):
|
|
if self.link_title:
|
|
return self.link_title
|
|
if self.link_page:
|
|
return self.link_page.title
|
|
return ""
|
|
|
|
@property
|
|
def url(self):
|
|
if self.link_page:
|
|
return self.link_page.url
|
|
return self.link_url
|
|
|
|
@property
|
|
def is_live(self):
|
|
"""Return False if linked to an unpublished/non-live page."""
|
|
if self.link_page_id:
|
|
return self.link_page.live
|
|
return bool(self.link_url)
|
|
|
|
class Meta(Orderable.Meta):
|
|
pass
|
|
|
|
|
|
class SocialMediaLink(Orderable):
|
|
settings = ParentalKey(
|
|
SiteSettings,
|
|
on_delete=models.CASCADE,
|
|
related_name="social_links",
|
|
)
|
|
platform = models.CharField(
|
|
max_length=30,
|
|
choices=SOCIAL_ICON_CHOICES,
|
|
)
|
|
url = models.URLField()
|
|
label = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
default="",
|
|
help_text="Display label. If blank, the platform name is used.",
|
|
)
|
|
|
|
panels = [
|
|
FieldPanel("platform"),
|
|
FieldPanel("url"),
|
|
FieldPanel("label"),
|
|
]
|
|
|
|
@property
|
|
def display_label(self):
|
|
if self.label:
|
|
return self.label
|
|
return dict(SOCIAL_ICON_CHOICES).get(self.platform, self.platform)
|
|
|
|
@property
|
|
def icon_template(self):
|
|
return f"components/icons/{self.platform}.html"
|
|
|
|
class Meta(Orderable.Meta):
|
|
pass
|