feat: add show_in_legal category for bottom bar CMS pages
Add third placement category for content pages that appear in the bottom bar alongside the copyright notice (Privacy Policy, Terms, etc.): Model changes: - Add show_in_legal boolean field to ContentPage model - Add to to_dict() serialization Service changes: - Add legal_only filter to list_pages_for_vendor() Platform changes: - Fetch legal_pages in get_platform_context() - Update base.html to render legal_pages dynamically - Fallback to hardcoded links if no CMS pages configured Migration: - Add column with default=False - Auto-set show_in_legal=True for privacy and terms pages Categories: - show_in_header: Top navigation - show_in_footer: Quick Links column - show_in_legal: Bottom bar with copyright (NEW) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
"""add show_in_legal to content_pages
|
||||||
|
|
||||||
|
Revision ID: ba2c0ce78396
|
||||||
|
Revises: m1b2c3d4e5f6
|
||||||
|
Create Date: 2025-12-28 20:00:24.263518
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'ba2c0ce78396'
|
||||||
|
down_revision: Union[str, None] = 'm1b2c3d4e5f6'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Add show_in_legal column to content_pages table.
|
||||||
|
|
||||||
|
This column controls whether a page appears in the bottom bar
|
||||||
|
alongside the copyright notice (e.g., Privacy Policy, Terms of Service).
|
||||||
|
"""
|
||||||
|
op.add_column(
|
||||||
|
'content_pages',
|
||||||
|
sa.Column('show_in_legal', sa.Boolean(), nullable=True, default=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set default value for existing rows
|
||||||
|
op.execute("UPDATE content_pages SET show_in_legal = 0 WHERE show_in_legal IS NULL")
|
||||||
|
|
||||||
|
# Set privacy and terms pages to show in legal by default
|
||||||
|
op.execute("UPDATE content_pages SET show_in_legal = 1 WHERE slug IN ('privacy', 'terms')")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Remove show_in_legal column from content_pages table."""
|
||||||
|
op.drop_column('content_pages', 'show_in_legal')
|
||||||
@@ -47,9 +47,10 @@ def get_platform_context(request: Request, db: Session) -> dict:
|
|||||||
# Add i18n globals (_, t, current_language, SUPPORTED_LANGUAGES, etc.)
|
# Add i18n globals (_, t, current_language, SUPPORTED_LANGUAGES, etc.)
|
||||||
context.update(i18n_globals)
|
context.update(i18n_globals)
|
||||||
|
|
||||||
# Load CMS pages for header and footer navigation
|
# Load CMS pages for header, footer, and legal navigation
|
||||||
header_pages = []
|
header_pages = []
|
||||||
footer_pages = []
|
footer_pages = []
|
||||||
|
legal_pages = []
|
||||||
try:
|
try:
|
||||||
# Platform pages have vendor_id=None
|
# Platform pages have vendor_id=None
|
||||||
header_pages = content_page_service.list_pages_for_vendor(
|
header_pages = content_page_service.list_pages_for_vendor(
|
||||||
@@ -58,14 +59,18 @@ def get_platform_context(request: Request, db: Session) -> dict:
|
|||||||
footer_pages = content_page_service.list_pages_for_vendor(
|
footer_pages = content_page_service.list_pages_for_vendor(
|
||||||
db, vendor_id=None, footer_only=True, include_unpublished=False
|
db, vendor_id=None, footer_only=True, include_unpublished=False
|
||||||
)
|
)
|
||||||
|
legal_pages = content_page_service.list_pages_for_vendor(
|
||||||
|
db, vendor_id=None, legal_only=True, include_unpublished=False
|
||||||
|
)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Loaded CMS pages: {len(header_pages)} header, {len(footer_pages)} footer"
|
f"Loaded CMS pages: {len(header_pages)} header, {len(footer_pages)} footer, {len(legal_pages)} legal"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load CMS navigation pages: {e}")
|
logger.error(f"Failed to load CMS navigation pages: {e}")
|
||||||
|
|
||||||
context["header_pages"] = header_pages
|
context["header_pages"] = header_pages
|
||||||
context["footer_pages"] = footer_pages
|
context["footer_pages"] = footer_pages
|
||||||
|
context["legal_pages"] = legal_pages
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ class ContentPageService:
|
|||||||
include_unpublished: bool = False,
|
include_unpublished: bool = False,
|
||||||
footer_only: bool = False,
|
footer_only: bool = False,
|
||||||
header_only: bool = False,
|
header_only: bool = False,
|
||||||
|
legal_only: bool = False,
|
||||||
) -> list[ContentPage]:
|
) -> list[ContentPage]:
|
||||||
"""
|
"""
|
||||||
List all available pages for a vendor (includes vendor overrides + platform defaults).
|
List all available pages for a vendor (includes vendor overrides + platform defaults).
|
||||||
@@ -109,6 +110,7 @@ class ContentPageService:
|
|||||||
include_unpublished: Include draft pages
|
include_unpublished: Include draft pages
|
||||||
footer_only: Only pages marked for footer display
|
footer_only: Only pages marked for footer display
|
||||||
header_only: Only pages marked for header display
|
header_only: Only pages marked for header display
|
||||||
|
legal_only: Only pages marked for legal/bottom bar display
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of ContentPage objects
|
List of ContentPage objects
|
||||||
@@ -124,6 +126,9 @@ class ContentPageService:
|
|||||||
if header_only:
|
if header_only:
|
||||||
filters.append(ContentPage.show_in_header == True)
|
filters.append(ContentPage.show_in_header == True)
|
||||||
|
|
||||||
|
if legal_only:
|
||||||
|
filters.append(ContentPage.show_in_legal == True)
|
||||||
|
|
||||||
# Get vendor-specific pages
|
# Get vendor-specific pages
|
||||||
vendor_pages = []
|
vendor_pages = []
|
||||||
if vendor_id:
|
if vendor_id:
|
||||||
|
|||||||
@@ -251,12 +251,21 @@
|
|||||||
{{ _("platform.footer.copyright", year=2025) }}
|
{{ _("platform.footer.copyright", year=2025) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex space-x-6 mt-4 md:mt-0">
|
<div class="flex space-x-6 mt-4 md:mt-0">
|
||||||
<a href="/privacy" class="text-gray-600 dark:text-gray-400 hover:text-primary text-sm transition-colors">
|
{% if legal_pages %}
|
||||||
{{ _("platform.footer.privacy") }}
|
{% for page in legal_pages %}
|
||||||
</a>
|
<a href="/{{ page.slug }}" class="text-gray-600 dark:text-gray-400 hover:text-primary text-sm transition-colors">
|
||||||
<a href="/terms" class="text-gray-600 dark:text-gray-400 hover:text-primary text-sm transition-colors">
|
{{ page.title }}
|
||||||
{{ _("platform.footer.terms") }}
|
</a>
|
||||||
</a>
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{# Fallback to hardcoded links if no CMS pages #}
|
||||||
|
<a href="/privacy" class="text-gray-600 dark:text-gray-400 hover:text-primary text-sm transition-colors">
|
||||||
|
{{ _("platform.footer.privacy") }}
|
||||||
|
</a>
|
||||||
|
<a href="/terms" class="text-gray-600 dark:text-gray-400 hover:text-primary text-sm transition-colors">
|
||||||
|
{{ _("platform.footer.terms") }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class ContentPage(Base):
|
|||||||
display_order = Column(Integer, default=0)
|
display_order = Column(Integer, default=0)
|
||||||
show_in_footer = Column(Boolean, default=True)
|
show_in_footer = Column(Boolean, default=True)
|
||||||
show_in_header = Column(Boolean, default=False)
|
show_in_header = Column(Boolean, default=False)
|
||||||
|
show_in_legal = Column(Boolean, default=False) # Bottom bar with copyright
|
||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at = Column(
|
created_at = Column(
|
||||||
@@ -153,6 +154,7 @@ class ContentPage(Base):
|
|||||||
"display_order": self.display_order,
|
"display_order": self.display_order,
|
||||||
"show_in_footer": self.show_in_footer,
|
"show_in_footer": self.show_in_footer,
|
||||||
"show_in_header": self.show_in_header,
|
"show_in_header": self.show_in_header,
|
||||||
|
"show_in_legal": self.show_in_legal,
|
||||||
"is_platform_default": self.is_platform_default,
|
"is_platform_default": self.is_platform_default,
|
||||||
"is_vendor_override": self.is_vendor_override,
|
"is_vendor_override": self.is_vendor_override,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
|
|||||||
Reference in New Issue
Block a user