feat: implement module-driven context providers for dynamic page context

Introduces a module-driven context provider system that allows modules to
dynamically contribute template context variables without hardcoding imports.

Key changes:
- Add context_providers field to ModuleDefinition in app/modules/base.py
- Create unified get_context_for_frontend() that queries enabled modules only
- Add context providers to CMS module (PLATFORM, STOREFRONT)
- Add context providers to billing module (PLATFORM)
- Fix SQLAlchemy cross-module relationship resolution (Order, AdminMenuConfig,
  MarketplaceImportJob) by ensuring models are imported before referencing
- Document the entire system in docs/architecture/module-system.md

Benefits:
- Zero coupling: adding/removing modules requires no route handler changes
- Lazy loading: module code only imported when that module is enabled
- Per-platform customization: each platform loads only what it needs
- Graceful degradation: one failing module doesn't break entire page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 19:22:52 +01:00
parent fb8cb14506
commit b03406db45
8 changed files with 746 additions and 241 deletions

View File

@@ -371,6 +371,26 @@ class ModuleDefinition:
tasks_path: str | None = None # Python import path, e.g., "app.modules.billing.tasks"
scheduled_tasks: list[ScheduledTask] = field(default_factory=list)
# =========================================================================
# Context Providers (Module-Driven Page Context)
# =========================================================================
# Callables that provide context data for page templates per frontend type.
# Each provider receives (request, db, platform) and returns a dict.
# This enables modules to contribute context only when enabled for a platform.
#
# Example:
# def get_billing_platform_context(request, db, platform):
# from app.modules.billing.models import TIER_LIMITS
# return {"tiers": get_tiers_data()}
#
# billing_module = ModuleDefinition(
# code="billing",
# context_providers={
# FrontendType.PLATFORM: get_billing_platform_context,
# },
# )
context_providers: dict[FrontendType, Callable[..., dict[str, Any]]] = field(default_factory=dict)
# =========================================================================
# Menu Item Methods (Legacy - uses menu_items dict of IDs)
# =========================================================================
@@ -695,6 +715,46 @@ class ModuleDefinition:
else:
return "optional"
# =========================================================================
# Context Provider Methods
# =========================================================================
def has_context_provider(self, frontend_type: FrontendType) -> bool:
"""Check if this module has a context provider for a frontend type."""
return frontend_type in self.context_providers
def get_context_contribution(
self,
frontend_type: FrontendType,
request: Any,
db: Any,
platform: Any,
) -> dict[str, Any]:
"""
Get context contribution from this module for a frontend type.
Args:
frontend_type: The frontend type (PLATFORM, ADMIN, VENDOR, STOREFRONT)
request: FastAPI Request object
db: Database session
platform: Platform object (may be None for some contexts)
Returns:
Dict of context variables, or empty dict if no provider
Note:
Exceptions are caught and logged by the caller. This method
may raise if the provider fails.
"""
provider = self.context_providers.get(frontend_type)
if provider is None:
return {}
return provider(request, db, platform)
def get_supported_frontend_types(self) -> list[FrontendType]:
"""Get list of frontend types this module provides context for."""
return list(self.context_providers.keys())
# =========================================================================
# Magic Methods
# =========================================================================