Multitenant implementation with custom Domain, theme per vendor
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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}')>"
|
||||
|
||||
Reference in New Issue
Block a user