refactor: implement module-driven permissions and relocate business logic
File Relocations: - Delete app/config/ folder (empty after menu_registry removal) - Move feature_gate.py → app/modules/billing/dependencies/ - Move theme_presets.py → app/modules/cms/services/ Module-Driven Permissions System: - Add PermissionDefinition dataclass to app/modules/base.py - Create PermissionDiscoveryService in tenancy module - Update module definitions to declare their own permissions: - core: dashboard.view, settings.* - catalog: products.* - orders: orders.* - inventory: stock.* - customers: customers.* - tenancy: team.* - Update app/core/permissions.py to use discovery service - Role presets (owner, manager, staff, etc.) now use module permissions This follows the same pattern as module-driven menus: - Each module defines its permissions in definition.py - PermissionDiscoveryService aggregates all permissions at runtime - Tenancy module handles role-to-permission assignment Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
249
app/modules/cms/services/theme_presets.py
Normal file
249
app/modules/cms/services/theme_presets.py
Normal file
@@ -0,0 +1,249 @@
|
||||
# app/core/theme_presets.py
|
||||
"""
|
||||
Theme presets for vendor shops.
|
||||
|
||||
Presets define default color schemes, fonts, and layouts that vendors can choose from.
|
||||
Each preset provides a complete theme configuration that can be customized further.
|
||||
"""
|
||||
|
||||
from app.modules.cms.models import VendorTheme
|
||||
|
||||
THEME_PRESETS = {
|
||||
"default": {
|
||||
"colors": {
|
||||
"primary": "#6366f1", # Indigo
|
||||
"secondary": "#8b5cf6", # Purple
|
||||
"accent": "#ec4899", # Pink
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb", # Gray-200
|
||||
},
|
||||
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
"modern": {
|
||||
"colors": {
|
||||
"primary": "#6366f1", # Indigo - Modern tech look
|
||||
"secondary": "#8b5cf6", # Purple
|
||||
"accent": "#ec4899", # Pink
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb", # Gray-200
|
||||
},
|
||||
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
"classic": {
|
||||
"colors": {
|
||||
"primary": "#1e40af", # Dark blue - Traditional
|
||||
"secondary": "#7c3aed", # Purple
|
||||
"accent": "#dc2626", # Red
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#d1d5db", # Gray-300
|
||||
},
|
||||
"fonts": {"heading": "Georgia, serif", "body": "Arial, sans-serif"},
|
||||
"layout": {"style": "list", "header": "static", "product_card": "classic"},
|
||||
},
|
||||
"minimal": {
|
||||
"colors": {
|
||||
"primary": "#000000", # Black - Ultra minimal
|
||||
"secondary": "#404040", # Dark gray
|
||||
"accent": "#666666", # Medium gray
|
||||
"background": "#ffffff", # White
|
||||
"text": "#000000", # Black
|
||||
"border": "#e5e7eb", # Light gray
|
||||
},
|
||||
"fonts": {"heading": "Helvetica, sans-serif", "body": "Helvetica, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "transparent", "product_card": "minimal"},
|
||||
},
|
||||
"vibrant": {
|
||||
"colors": {
|
||||
"primary": "#f59e0b", # Orange - Bold & energetic
|
||||
"secondary": "#ef4444", # Red
|
||||
"accent": "#8b5cf6", # Purple
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#fbbf24", # Yellow
|
||||
},
|
||||
"fonts": {"heading": "Poppins, sans-serif", "body": "Open Sans, sans-serif"},
|
||||
"layout": {"style": "masonry", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
"elegant": {
|
||||
"colors": {
|
||||
"primary": "#6b7280", # Gray - Sophisticated
|
||||
"secondary": "#374151", # Dark gray
|
||||
"accent": "#d97706", # Amber
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb", # Gray-200
|
||||
},
|
||||
"fonts": {"heading": "Playfair Display, serif", "body": "Lato, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "classic"},
|
||||
},
|
||||
"nature": {
|
||||
"colors": {
|
||||
"primary": "#059669", # Green - Natural & eco
|
||||
"secondary": "#10b981", # Emerald
|
||||
"accent": "#f59e0b", # Amber
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#d1fae5", # Light green
|
||||
},
|
||||
"fonts": {"heading": "Montserrat, sans-serif", "body": "Open Sans, sans-serif"},
|
||||
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_preset(preset_name: str) -> dict:
|
||||
"""
|
||||
Get a theme preset by name.
|
||||
|
||||
Args:
|
||||
preset_name: Name of the preset (e.g., 'modern', 'classic')
|
||||
|
||||
Returns:
|
||||
dict: Theme configuration
|
||||
|
||||
Raises:
|
||||
ValueError: If preset name is unknown
|
||||
"""
|
||||
if preset_name not in THEME_PRESETS:
|
||||
available = ", ".join(THEME_PRESETS.keys())
|
||||
raise ValueError(f"Unknown preset: {preset_name}. Available: {available}")
|
||||
|
||||
return THEME_PRESETS[preset_name]
|
||||
|
||||
|
||||
def apply_preset(theme: VendorTheme, preset_name: str) -> VendorTheme:
|
||||
"""
|
||||
Apply a preset to a vendor theme.
|
||||
|
||||
Args:
|
||||
theme: VendorTheme instance to update
|
||||
preset_name: Name of the preset to apply
|
||||
|
||||
Returns:
|
||||
VendorTheme: Updated theme instance
|
||||
|
||||
Raises:
|
||||
ValueError: If preset name is unknown
|
||||
|
||||
Example:
|
||||
theme = VendorTheme(vendor_id=1)
|
||||
apply_preset(theme, "modern")
|
||||
db.add(theme)
|
||||
db.commit()
|
||||
"""
|
||||
preset = get_preset(preset_name)
|
||||
|
||||
# Set theme name
|
||||
theme.theme_name = preset_name
|
||||
|
||||
# Apply colors (all of them!)
|
||||
theme.colors = preset["colors"]
|
||||
|
||||
# Apply fonts
|
||||
theme.font_family_heading = preset["fonts"]["heading"]
|
||||
theme.font_family_body = preset["fonts"]["body"]
|
||||
|
||||
# Apply layout settings
|
||||
theme.layout_style = preset["layout"]["style"]
|
||||
theme.header_style = preset["layout"]["header"]
|
||||
theme.product_card_style = preset["layout"]["product_card"]
|
||||
|
||||
# Mark as active
|
||||
theme.is_active = True
|
||||
|
||||
return theme
|
||||
|
||||
|
||||
def get_available_presets() -> list[str]:
|
||||
"""
|
||||
Get list of available preset names.
|
||||
|
||||
Returns:
|
||||
list: Available preset names
|
||||
"""
|
||||
return list(THEME_PRESETS.keys())
|
||||
|
||||
|
||||
def get_preset_preview(preset_name: str) -> dict:
|
||||
"""
|
||||
Get preview information for a preset (for UI display).
|
||||
|
||||
Args:
|
||||
preset_name: Name of the preset
|
||||
|
||||
Returns:
|
||||
dict: Preview info with colors, fonts, description
|
||||
"""
|
||||
preset = get_preset(preset_name)
|
||||
|
||||
descriptions = {
|
||||
"default": "Clean and professional - perfect for getting started",
|
||||
"modern": "Contemporary tech-inspired design with vibrant colors",
|
||||
"classic": "Traditional and trustworthy with serif typography",
|
||||
"minimal": "Ultra-clean black and white aesthetic",
|
||||
"vibrant": "Bold and energetic with bright accent colors",
|
||||
"elegant": "Sophisticated gray tones with refined typography",
|
||||
"nature": "Fresh and eco-friendly green color palette",
|
||||
}
|
||||
|
||||
return {
|
||||
"name": preset_name,
|
||||
"description": descriptions.get(preset_name, ""),
|
||||
"primary_color": preset["colors"]["primary"],
|
||||
"secondary_color": preset["colors"]["secondary"],
|
||||
"accent_color": preset["colors"]["accent"],
|
||||
"heading_font": preset["fonts"]["heading"],
|
||||
"body_font": preset["fonts"]["body"],
|
||||
"layout_style": preset["layout"]["style"],
|
||||
}
|
||||
|
||||
|
||||
def create_custom_preset(
|
||||
colors: dict, fonts: dict, layout: dict, name: str = "custom"
|
||||
) -> dict:
|
||||
"""
|
||||
Create a custom preset from provided settings.
|
||||
|
||||
Args:
|
||||
colors: Dict with primary, secondary, accent, background, text, border
|
||||
fonts: Dict with heading and body fonts
|
||||
layout: Dict with style, header, product_card
|
||||
name: Name for the custom preset
|
||||
|
||||
Returns:
|
||||
dict: Custom preset configuration
|
||||
|
||||
Example:
|
||||
custom = create_custom_preset(
|
||||
colors={"primary": "#ff0000", "secondary": "#00ff00", ...},
|
||||
fonts={"heading": "Arial", "body": "Arial"},
|
||||
layout={"style": "grid", "header": "fixed", "product_card": "modern"},
|
||||
name="my_custom_theme"
|
||||
)
|
||||
"""
|
||||
# Validate colors
|
||||
required_colors = ["primary", "secondary", "accent", "background", "text", "border"]
|
||||
for color_key in required_colors:
|
||||
if color_key not in colors:
|
||||
colors[color_key] = THEME_PRESETS["default"]["colors"][color_key]
|
||||
|
||||
# Validate fonts
|
||||
if "heading" not in fonts:
|
||||
fonts["heading"] = "Inter, sans-serif"
|
||||
if "body" not in fonts:
|
||||
fonts["body"] = "Inter, sans-serif"
|
||||
|
||||
# Validate layout
|
||||
if "style" not in layout:
|
||||
layout["style"] = "grid"
|
||||
if "header" not in layout:
|
||||
layout["header"] = "fixed"
|
||||
if "product_card" not in layout:
|
||||
layout["product_card"] = "modern"
|
||||
|
||||
return {"colors": colors, "fonts": fonts, "layout": layout}
|
||||
@@ -11,7 +11,7 @@ import re
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.theme_presets import (
|
||||
from app.modules.cms.services.theme_presets import (
|
||||
THEME_PRESETS,
|
||||
apply_preset,
|
||||
get_available_presets,
|
||||
|
||||
Reference in New Issue
Block a user