refactor: migrate templates and static files to self-contained modules

Templates Migration:
- Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.)
- Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.)
- Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms)
- Migrate public templates to modules (billing, marketplace, cms)
- Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/)
- Migrate letzshop partials to marketplace module

Static Files Migration:
- Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file)
- Migrate vendor JS to modules: tenancy (4 files), core (2 files)
- Migrate shared JS: vendor-selector.js to core, media-picker.js to cms
- Migrate storefront JS: storefront-layout.js to core
- Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/)
- Update all template references to use module_static paths

Naming Consistency:
- Rename static/platform/ to static/public/
- Rename app/templates/platform/ to app/templates/public/
- Update all extends and static references

Documentation:
- Update module-system.md with shared templates documentation
- Update frontend-structure.md with new module JS organization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 14:34:16 +01:00
parent 843703258f
commit 4e28d91a78
542 changed files with 11603 additions and 9037 deletions

View File

@@ -0,0 +1,16 @@
# app/modules/core/utils/__init__.py
"""Core module utilities."""
from .page_context import (
get_admin_context,
get_vendor_context,
get_storefront_context,
get_public_context,
)
__all__ = [
"get_admin_context",
"get_vendor_context",
"get_storefront_context",
"get_public_context",
]

View File

@@ -0,0 +1,328 @@
# app/modules/core/utils/page_context.py
"""
Shared page context helpers for HTML page routes.
These functions build template contexts that include common variables
needed across different frontends (admin, vendor, storefront, public).
"""
import logging
from fastapi import Request
from sqlalchemy.orm import Session
from app.core.config import settings
from app.modules.core.services.platform_settings_service import platform_settings_service
from app.utils.i18n import get_jinja2_globals
from models.database.user import User
from models.database.vendor import Vendor
logger = logging.getLogger(__name__)
def get_admin_context(
request: Request,
current_user: User,
db: Session | None = None,
**extra_context,
) -> dict:
"""
Build template context for admin dashboard pages.
Args:
request: FastAPI request object
current_user: Authenticated admin user
db: Optional database session
**extra_context: Additional variables for template
Returns:
Dictionary with request, user, and extra context
"""
context = {
"request": request,
"user": current_user,
}
if extra_context:
context.update(extra_context)
return context
def get_vendor_context(
request: Request,
db: Session,
current_user: User,
vendor_code: str,
**extra_context,
) -> dict:
"""
Build template context for vendor dashboard pages.
Resolves locale/currency using the platform settings service with
vendor override support:
1. Vendor's storefront_locale (if set)
2. Platform's default from PlatformSettingsService
3. Environment variable
4. Hardcoded fallback
Args:
request: FastAPI request object
db: Database session
current_user: Authenticated vendor user
vendor_code: Vendor subdomain/code
**extra_context: Additional variables for template
Returns:
Dictionary with request, user, vendor, resolved locale/currency, and extra context
"""
# Load vendor from database
vendor = db.query(Vendor).filter(Vendor.subdomain == vendor_code).first()
# Get platform defaults
platform_config = platform_settings_service.get_storefront_config(db)
# Resolve with vendor override
storefront_locale = platform_config["locale"]
storefront_currency = platform_config["currency"]
if vendor and vendor.storefront_locale:
storefront_locale = vendor.storefront_locale
context = {
"request": request,
"user": current_user,
"vendor": vendor,
"vendor_code": vendor_code,
"storefront_locale": storefront_locale,
"storefront_currency": storefront_currency,
"dashboard_language": vendor.dashboard_language if vendor else "en",
}
# Add any extra context
if extra_context:
context.update(extra_context)
logger.debug(
"[VENDOR_CONTEXT] Context built",
extra={
"vendor_id": vendor.id if vendor else None,
"vendor_code": vendor_code,
"storefront_locale": storefront_locale,
"storefront_currency": storefront_currency,
"extra_keys": list(extra_context.keys()) if extra_context else [],
},
)
return context
def get_storefront_context(
request: Request,
db: Session | None = None,
**extra_context,
) -> dict:
"""
Build template context for storefront (customer shop) pages.
Automatically includes vendor and theme from middleware request.state.
Additional context can be passed as keyword arguments.
Args:
request: FastAPI request object with vendor/theme in state
db: Optional database session for loading navigation pages
**extra_context: Additional variables for template (user, product_id, etc.)
Returns:
Dictionary with request, vendor, theme, navigation pages, and extra context
"""
# Import here to avoid circular imports
from app.modules.cms.services import content_page_service
# Extract from middleware state
vendor = getattr(request.state, "vendor", None)
platform = getattr(request.state, "platform", None)
theme = getattr(request.state, "theme", None)
clean_path = getattr(request.state, "clean_path", request.url.path)
vendor_context = getattr(request.state, "vendor_context", None)
# Get platform_id (default to 1 for OMS if not set)
platform_id = platform.id if platform else 1
# Get detection method from vendor_context
access_method = (
vendor_context.get("detection_method", "unknown")
if vendor_context
else "unknown"
)
if vendor is None:
logger.warning(
"[STOREFRONT_CONTEXT] Vendor not found in request.state",
extra={
"path": request.url.path,
"host": request.headers.get("host", ""),
"has_vendor": False,
},
)
# Calculate base URL for links
# - Domain/subdomain access: base_url = "/"
# - Path-based access: base_url = "/vendor/{vendor_code}/" or "/vendors/{vendor_code}/"
base_url = "/"
if access_method == "path" and vendor:
# Use the full_prefix from vendor_context to determine which pattern was used
full_prefix = (
vendor_context.get("full_prefix", "/vendor/")
if vendor_context
else "/vendor/"
)
base_url = f"{full_prefix}{vendor.subdomain}/"
# Load footer and header navigation pages from CMS if db session provided
footer_pages = []
header_pages = []
if db and vendor:
try:
vendor_id = vendor.id
# Get pages configured to show in footer
footer_pages = content_page_service.list_pages_for_vendor(
db,
platform_id=platform_id,
vendor_id=vendor_id,
footer_only=True,
include_unpublished=False,
)
# Get pages configured to show in header
header_pages = content_page_service.list_pages_for_vendor(
db,
platform_id=platform_id,
vendor_id=vendor_id,
header_only=True,
include_unpublished=False,
)
except Exception as e:
logger.error(
"[STOREFRONT_CONTEXT] Failed to load navigation pages",
extra={"error": str(e), "vendor_id": vendor.id if vendor else None},
)
# Resolve storefront locale and currency
storefront_config = {"locale": "fr-LU", "currency": "EUR"} # defaults
if db and vendor:
platform_config = platform_settings_service.get_storefront_config(db)
storefront_config["locale"] = platform_config["locale"]
storefront_config["currency"] = platform_config["currency"]
if vendor.storefront_locale:
storefront_config["locale"] = vendor.storefront_locale
context = {
"request": request,
"vendor": vendor,
"theme": theme,
"clean_path": clean_path,
"access_method": access_method,
"base_url": base_url,
"footer_pages": footer_pages,
"header_pages": header_pages,
"storefront_locale": storefront_config["locale"],
"storefront_currency": storefront_config["currency"],
}
# Add any extra context (user, product_id, category_slug, etc.)
if extra_context:
context.update(extra_context)
logger.debug(
"[STOREFRONT_CONTEXT] Context built",
extra={
"vendor_id": vendor.id if vendor else None,
"vendor_name": vendor.name if vendor else None,
"vendor_subdomain": vendor.subdomain if vendor else None,
"has_theme": theme is not None,
"access_method": access_method,
"base_url": base_url,
"storefront_locale": storefront_config["locale"],
"storefront_currency": storefront_config["currency"],
"footer_pages_count": len(footer_pages),
"header_pages_count": len(header_pages),
"extra_keys": list(extra_context.keys()) if extra_context else [],
},
)
return context
def get_public_context(
request: Request,
db: Session,
**extra_context,
) -> dict:
"""
Build context for public/marketing pages.
Includes platform info, i18n globals, and CMS navigation pages.
Args:
request: FastAPI request object
db: Database session
**extra_context: Additional variables for template
Returns:
Dictionary with request, platform info, i18n globals, and extra context
"""
# Import here to avoid circular imports
from app.modules.cms.services import content_page_service
# Get language from request state (set by middleware)
language = getattr(request.state, "language", "fr")
# Get platform from middleware (default to OMS platform_id=1)
platform = getattr(request.state, "platform", None)
platform_id = platform.id if platform else 1
# Get translation function
i18n_globals = get_jinja2_globals(language)
context = {
"request": request,
"platform": platform,
"platform_name": "Wizamart",
"platform_domain": settings.platform_domain,
"stripe_publishable_key": settings.stripe_publishable_key,
"trial_days": settings.stripe_trial_days,
}
# Add i18n globals (_, t, current_language, SUPPORTED_LANGUAGES, etc.)
context.update(i18n_globals)
# Load CMS pages for header, footer, and legal navigation
header_pages = []
footer_pages = []
legal_pages = []
try:
# Platform marketing pages (is_platform_page=True)
header_pages = content_page_service.list_platform_pages(
db, platform_id=platform_id, header_only=True, include_unpublished=False
)
footer_pages = content_page_service.list_platform_pages(
db, platform_id=platform_id, footer_only=True, include_unpublished=False
)
# For legal pages, we need to add footer support or use a different approach
# For now, legal pages come from footer pages with show_in_legal flag
legal_pages = [] # Will be handled separately if needed
logger.debug(
f"Loaded CMS pages: {len(header_pages)} header, {len(footer_pages)} footer, {len(legal_pages)} legal"
)
except Exception as e:
logger.error(f"Failed to load CMS navigation pages: {e}")
context["header_pages"] = header_pages
context["footer_pages"] = footer_pages
context["legal_pages"] = legal_pages
# Add any extra context
if extra_context:
context.update(extra_context)
return context