feat: add platform detail/edit admin UI and service enhancements

- Add platform detail and edit admin pages with templates and JS
- Add ContentPageService methods: list_all_platform_pages, list_all_vendor_defaults
- Deprecate /admin/platform-homepage route (redirects to /admin/platforms)
- Add migration to fix content_page nullable columns
- Refine platform and vendor context middleware
- Add platform context middleware unit tests
- Update platforms.js with improved functionality
- Add section-based homepage plan documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-23 14:08:02 +01:00
parent d70a9f38d4
commit 3d3b8cae22
25 changed files with 3233 additions and 95 deletions

View File

@@ -20,12 +20,12 @@ import logging
from fastapi import Request
from sqlalchemy.orm import Session
# Note: We use pure ASGI middleware (not BaseHTTPMiddleware) to enable path rewriting
from app.core.config import settings
from app.core.database import get_db
from models.database.platform import Platform
# Note: We use pure ASGI middleware (not BaseHTTPMiddleware) to enable path rewriting
logger = logging.getLogger(__name__)
# Default platform code for main marketing site
@@ -139,7 +139,7 @@ class PlatformContextManager:
platform = (
db.query(Platform)
.filter(Platform.domain == domain)
.filter(Platform.is_active == True)
.filter(Platform.is_active.is_(True))
.first()
)
@@ -148,8 +148,7 @@ class PlatformContextManager:
f"[PLATFORM] Platform found via domain: {domain}{platform.name}"
)
return platform
else:
logger.debug(f"[PLATFORM] No platform found for domain: {domain}")
logger.debug(f"[PLATFORM] No platform found for domain: {domain}")
# Method 2: Path-prefix lookup
if context.get("detection_method") == "path":
@@ -161,7 +160,7 @@ class PlatformContextManager:
.filter(
(Platform.path_prefix == path_prefix) | (Platform.code == path_prefix)
)
.filter(Platform.is_active == True)
.filter(Platform.is_active.is_(True))
.first()
)
@@ -170,15 +169,14 @@ class PlatformContextManager:
f"[PLATFORM] Platform found via path prefix: {path_prefix}{platform.name}"
)
return platform
else:
logger.debug(f"[PLATFORM] No platform found for path prefix: {path_prefix}")
logger.debug(f"[PLATFORM] No platform found for path prefix: {path_prefix}")
# Method 3: Default lookup
if context.get("detection_method") == "default":
platform = (
db.query(Platform)
.filter(Platform.code == DEFAULT_PLATFORM_CODE)
.filter(Platform.is_active == True)
.filter(Platform.is_active.is_(True))
.first()
)
@@ -220,10 +218,7 @@ class PlatformContextManager:
if host.startswith("admin."):
return True
if path.startswith("/admin"):
return True
return False
return path.startswith("/admin")
@staticmethod
def is_static_file_request(request: Request) -> bool:
@@ -244,10 +239,7 @@ class PlatformContextManager:
if any(path.startswith(static_path) for static_path in static_paths):
return True
if "favicon.ico" in path:
return True
return False
return "favicon.ico" in path
class PlatformContextMiddleware:
@@ -432,18 +424,14 @@ class PlatformContextMiddleware:
return True
if any(path_lower.startswith(p) for p in static_paths):
return True
if "favicon.ico" in path_lower:
return True
return False
return "favicon.ico" in path_lower
def _is_admin_request(self, path: str, host: str) -> bool:
"""Check if request is for admin interface."""
host_without_port = host.split(":")[0] if ":" in host else host
if host_without_port.startswith("admin."):
return True
if path.startswith("/admin"):
return True
return False
return path.startswith("/admin")
def get_current_platform(request: Request) -> Platform | None:

View File

@@ -91,7 +91,7 @@ class VendorContextManager:
# Method 3: Path-based detection (/vendor/vendorname/ or /vendors/vendorname/)
# Support BOTH patterns for flexibility
if path.startswith("/vendor/") or path.startswith("/vendors/"):
if path.startswith(("/vendor/", "/vendors/")):
# Determine which pattern
if path.startswith("/vendors/"):
prefix_len = len("/vendors/")
@@ -133,8 +133,8 @@ class VendorContextManager:
vendor_domain = (
db.query(VendorDomain)
.filter(VendorDomain.domain == domain)
.filter(VendorDomain.is_active == True)
.filter(VendorDomain.is_verified == True)
.filter(VendorDomain.is_active.is_(True))
.filter(VendorDomain.is_verified.is_(True))
.first()
)
@@ -157,7 +157,7 @@ class VendorContextManager:
vendor = (
db.query(Vendor)
.filter(func.lower(Vendor.subdomain) == subdomain.lower())
.filter(Vendor.is_active == True)
.filter(Vendor.is_active.is_(True))
.first()
)
@@ -204,10 +204,7 @@ class VendorContextManager:
if host.startswith("admin."):
return True
if path.startswith("/admin"):
return True
return False
return path.startswith("/admin")
@staticmethod
def is_api_request(request: Request) -> bool:
@@ -263,9 +260,7 @@ class VendorContextManager:
# Method 1: Path-based detection from referer path
# /vendors/wizamart/shop/products → wizamart
if referer_path.startswith("/vendors/") or referer_path.startswith(
"/vendor/"
):
if referer_path.startswith(("/vendors/", "/vendor/")):
prefix = (
"/vendors/" if referer_path.startswith("/vendors/") else "/vendor/"
)
@@ -384,10 +379,7 @@ class VendorContextManager:
if any(path.startswith(static_path) for static_path in static_paths):
return True
if "favicon.ico" in path:
return True
return False
return "favicon.ico" in path
class VendorContextMiddleware(BaseHTTPMiddleware):