Multitenant implementation with custom Domain, theme per vendor

This commit is contained in:
2025-10-26 23:49:29 +01:00
parent c88775134d
commit 1e0cbf5927
24 changed files with 3470 additions and 624 deletions

View File

@@ -1,11 +1,9 @@
# models/database/vendor.py - ENHANCED VERSION
# models/database/vendor.py
"""
Enhanced Vendor model with theme support.
Vendor model with theme support.
Changes from your current version:
1. Keep existing theme_config JSON field
2. Add optional VendorTheme relationship for advanced themes
3. Add helper methods for theme access
A vendor has ONE active theme stored in the vendor_themes table.
Theme presets available: default, modern, classic, minimal, vibrant
"""
from sqlalchemy import (Boolean, Column, ForeignKey, Integer, String, Text, JSON)
from sqlalchemy.orm import relationship
@@ -27,10 +25,6 @@ class Vendor(Base, TimestampMixin):
description = Column(Text)
owner_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Simple theme config (JSON)
# This stores basic theme settings like colors, fonts
theme_config = Column(JSON, default=dict)
# Contact information
contact_email = Column(String)
contact_phone = Column(String)
@@ -49,13 +43,16 @@ class Vendor(Base, TimestampMixin):
is_active = Column(Boolean, default=True)
is_verified = Column(Boolean, default=False)
# ========================================================================
# Relationships
# ========================================================================
owner = relationship("User", back_populates="owned_vendors")
vendor_users = relationship("VendorUser", back_populates="vendor")
products = relationship("Product", back_populates="vendor")
customers = relationship("Customer", back_populates="vendor")
orders = relationship("Order", back_populates="vendor")
marketplace_import_jobs = relationship("MarketplaceImportJob", back_populates="vendor")
domains = relationship(
"VendorDomain",
back_populates="vendor",
@@ -63,16 +60,8 @@ class Vendor(Base, TimestampMixin):
order_by="VendorDomain.is_primary.desc()"
)
theme = relationship(
"VendorTheme",
back_populates="vendor",
uselist=False,
cascade="all, delete-orphan"
)
# Optional advanced theme (for premium vendors)
# This is optional - vendors can use theme_config OR VendorTheme
advanced_theme = relationship(
# Single theme relationship (ONE vendor = ONE theme)
vendor_theme = relationship(
"VendorTheme",
back_populates="vendor",
uselist=False,
@@ -86,81 +75,22 @@ class Vendor(Base, TimestampMixin):
# Theme Helper Methods
# ========================================================================
@property
def active_theme(self):
"""Get vendor's active theme or return default"""
if self.theme and self.theme.is_active:
return self.theme
return None
@property
def theme(self):
def get_effective_theme(self) -> dict:
"""
Get theme configuration for this vendor.
Get active theme for this vendor.
Priority:
1. Advanced theme (VendorTheme) if configured
2. theme_config JSON field
3. Default theme
Returns theme from vendor_themes table, or default theme if not set.
Returns dict with theme configuration.
Returns:
dict: Theme configuration with colors, fonts, layout, etc.
"""
# Priority 1: Advanced theme
if self.advanced_theme and self.advanced_theme.is_active:
return self.advanced_theme.to_dict()
# Check vendor_themes table
if self.vendor_theme and self.vendor_theme.is_active:
return self.vendor_theme.to_dict()
# Priority 2: Basic theme_config
if self.theme_config:
return self._normalize_theme_config(self.theme_config)
# Priority 3: Default theme
# Return default theme
return self._get_default_theme()
def _normalize_theme_config(self, config: dict) -> dict:
"""
Normalize theme_config JSON to standard format.
Ensures backward compatibility with existing theme_config.
"""
return {
"theme_name": config.get("theme_name", "basic"),
"colors": config.get("colors", {
"primary": "#6366f1",
"secondary": "#8b5cf6",
"accent": "#ec4899"
}),
"fonts": config.get("fonts", {
"heading": "Inter, sans-serif",
"body": "Inter, sans-serif"
}),
"branding": config.get("branding", {
"logo": None,
"logo_dark": None,
"favicon": None
}),
"layout": config.get("layout", {
"style": "grid",
"header": "fixed"
}),
"custom_css": config.get("custom_css", None),
"css_variables": self._generate_css_variables(config)
}
def _generate_css_variables(self, config: dict) -> dict:
"""Generate CSS custom properties from theme config"""
colors = config.get("colors", {})
fonts = config.get("fonts", {})
return {
"--color-primary": colors.get("primary", "#6366f1"),
"--color-secondary": colors.get("secondary", "#8b5cf6"),
"--color-accent": colors.get("accent", "#ec4899"),
"--color-background": colors.get("background", "#ffffff"),
"--color-text": colors.get("text", "#1f2937"),
"--color-border": colors.get("border", "#e5e7eb"),
"--font-heading": fonts.get("heading", "Inter, sans-serif"),
"--font-body": fonts.get("body", "Inter, sans-serif"),
}
def _get_default_theme(self) -> dict:
"""Default theme configuration"""
return {
@@ -180,12 +110,15 @@ class Vendor(Base, TimestampMixin):
"branding": {
"logo": None,
"logo_dark": None,
"favicon": None
"favicon": None,
"banner": None
},
"layout": {
"style": "grid",
"header": "fixed"
"header": "fixed",
"product_card": "modern"
},
"social_links": {},
"custom_css": None,
"css_variables": {
"--color-primary": "#6366f1",
@@ -199,42 +132,15 @@ class Vendor(Base, TimestampMixin):
}
}
@property
def primary_color(self):
"""Get primary color from theme"""
return self.theme.get("colors", {}).get("primary", "#6366f1")
def get_primary_color(self) -> str:
"""Get primary color from active theme"""
theme = self.get_effective_theme()
return theme.get("colors", {}).get("primary", "#6366f1")
@property
def logo_url(self):
"""Get logo URL from theme"""
return self.theme.get("branding", {}).get("logo")
def update_theme(self, theme_data: dict):
"""
Update vendor theme configuration.
Args:
theme_data: Dict with theme settings
{colors: {...}, fonts: {...}, etc}
"""
if not self.theme_config:
self.theme_config = {}
# Update theme_config JSON
if "colors" in theme_data:
self.theme_config["colors"] = theme_data["colors"]
if "fonts" in theme_data:
self.theme_config["fonts"] = theme_data["fonts"]
if "branding" in theme_data:
self.theme_config["branding"] = theme_data["branding"]
if "layout" in theme_data:
self.theme_config["layout"] = theme_data["layout"]
if "custom_css" in theme_data:
self.theme_config["custom_css"] = theme_data["custom_css"]
def get_logo_url(self) -> str:
"""Get logo URL from active theme"""
theme = self.get_effective_theme()
return theme.get("branding", {}).get("logo")
# ========================================================================
# Domain Helper Methods
@@ -257,6 +163,7 @@ class Vendor(Base, TimestampMixin):
domains.append(domain.domain)
return domains
# Keep your existing VendorUser and Role models unchanged
class VendorUser(Base, TimestampMixin):
__tablename__ = "vendor_users"

View File

@@ -3,30 +3,37 @@
Vendor Theme Configuration Model
Allows each vendor to customize their shop's appearance
"""
from datetime import datetime, timezone
from sqlalchemy import Column, Integer, String, Boolean, Text, JSON, DateTime, ForeignKey
from sqlalchemy import Column, Integer, String, Boolean, Text, JSON, ForeignKey
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class VendorTheme(Base, TimestampMixin):
"""
Stores theme configuration for each vendor's shop.
Each vendor can have:
Each vendor can have ONE active theme:
- Custom colors (primary, secondary, accent)
- Custom fonts
- Custom logo and favicon
- Custom CSS overrides
- Layout preferences
Theme presets available: default, modern, classic, minimal, vibrant
"""
__tablename__ = "vendor_themes"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id", ondelete="CASCADE"), nullable=False, unique=True)
vendor_id = Column(
Integer,
ForeignKey("vendors.id", ondelete="CASCADE"),
nullable=False,
unique=True # ONE vendor = ONE theme
)
# Basic Theme Settings
theme_name = Column(String(100), default="default") # e.g., "modern", "classic", "minimal"
theme_name = Column(String(100), default="default") # default, modern, classic, minimal, vibrant
is_active = Column(Boolean, default=True)
# Color Scheme (JSON for flexibility)
@@ -64,8 +71,8 @@ class VendorTheme(Base, TimestampMixin):
meta_title_template = Column(String(200), nullable=True) # e.g., "{product_name} - {shop_name}"
meta_description = Column(Text, nullable=True)
# Relationships
vendor = relationship("Vendor", back_populates="theme")
# Relationships - FIXED: back_populates must match the relationship name in Vendor model
vendor = relationship("Vendor", back_populates="vendor_theme")
def __repr__(self):
return f"<VendorTheme(vendor_id={self.vendor_id}, theme_name='{self.theme_name}')>"