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}')>"
|
||||
|
||||
@@ -66,9 +66,6 @@ class VendorCreate(BaseModel):
|
||||
letzshop_csv_url_en: Optional[str] = Field(None, description="English CSV URL")
|
||||
letzshop_csv_url_de: Optional[str] = Field(None, description="German CSV URL")
|
||||
|
||||
# Theme Configuration
|
||||
theme_config: Optional[Dict] = Field(default_factory=dict, description="Theme settings")
|
||||
|
||||
@field_validator("owner_email", "contact_email")
|
||||
@classmethod
|
||||
def validate_emails(cls, v):
|
||||
@@ -122,9 +119,6 @@ class VendorUpdate(BaseModel):
|
||||
letzshop_csv_url_en: Optional[str] = None
|
||||
letzshop_csv_url_de: Optional[str] = None
|
||||
|
||||
# Theme Configuration
|
||||
theme_config: Optional[Dict] = None
|
||||
|
||||
# Status (Admin only)
|
||||
is_active: Optional[bool] = None
|
||||
is_verified: Optional[bool] = None
|
||||
@@ -171,9 +165,6 @@ class VendorResponse(BaseModel):
|
||||
letzshop_csv_url_en: Optional[str]
|
||||
letzshop_csv_url_de: Optional[str]
|
||||
|
||||
# Theme Configuration
|
||||
theme_config: Dict
|
||||
|
||||
# Status Flags
|
||||
is_active: bool
|
||||
is_verified: bool
|
||||
|
||||
84
models/schema/vendor_theme.py
Normal file
84
models/schema/vendor_theme.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# models/schema/vendor_theme.py
|
||||
"""
|
||||
Pydantic schemas for vendor theme operations.
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class VendorThemeColors(BaseModel):
|
||||
"""Color scheme for vendor theme."""
|
||||
primary: Optional[str] = Field(None, description="Primary brand color")
|
||||
secondary: Optional[str] = Field(None, description="Secondary color")
|
||||
accent: Optional[str] = Field(None, description="Accent/CTA color")
|
||||
background: Optional[str] = Field(None, description="Background color")
|
||||
text: Optional[str] = Field(None, description="Text color")
|
||||
border: Optional[str] = Field(None, description="Border color")
|
||||
|
||||
|
||||
class VendorThemeFonts(BaseModel):
|
||||
"""Typography settings for vendor theme."""
|
||||
heading: Optional[str] = Field(None, description="Font for headings")
|
||||
body: Optional[str] = Field(None, description="Font for body text")
|
||||
|
||||
|
||||
class VendorThemeBranding(BaseModel):
|
||||
"""Branding assets for vendor theme."""
|
||||
logo: Optional[str] = Field(None, description="Logo URL")
|
||||
logo_dark: Optional[str] = Field(None, description="Dark mode logo URL")
|
||||
favicon: Optional[str] = Field(None, description="Favicon URL")
|
||||
banner: Optional[str] = Field(None, description="Banner image URL")
|
||||
|
||||
|
||||
class VendorThemeLayout(BaseModel):
|
||||
"""Layout settings for vendor theme."""
|
||||
style: Optional[str] = Field(None, description="Product layout style (grid, list, masonry)")
|
||||
header: Optional[str] = Field(None, description="Header style (fixed, static, transparent)")
|
||||
product_card: Optional[str] = Field(None, description="Product card style (modern, classic, minimal)")
|
||||
|
||||
|
||||
class VendorThemeUpdate(BaseModel):
|
||||
"""Schema for updating vendor theme (partial updates allowed)."""
|
||||
theme_name: Optional[str] = Field(None, description="Theme preset name")
|
||||
colors: Optional[Dict[str, str]] = Field(None, description="Color scheme")
|
||||
fonts: Optional[Dict[str, str]] = Field(None, description="Font settings")
|
||||
branding: Optional[Dict[str, Optional[str]]] = Field(None, description="Branding assets")
|
||||
layout: Optional[Dict[str, str]] = Field(None, description="Layout settings")
|
||||
custom_css: Optional[str] = Field(None, description="Custom CSS rules")
|
||||
social_links: Optional[Dict[str, str]] = Field(None, description="Social media links")
|
||||
|
||||
|
||||
class VendorThemeResponse(BaseModel):
|
||||
"""Schema for vendor theme response."""
|
||||
theme_name: str = Field(..., description="Theme name")
|
||||
colors: Dict[str, str] = Field(..., description="Color scheme")
|
||||
fonts: Dict[str, str] = Field(..., description="Font settings")
|
||||
branding: Dict[str, Optional[str]] = Field(..., description="Branding assets")
|
||||
layout: Dict[str, str] = Field(..., description="Layout settings")
|
||||
social_links: Optional[Dict[str, str]] = Field(default_factory=dict, description="Social links")
|
||||
custom_css: Optional[str] = Field(None, description="Custom CSS")
|
||||
css_variables: Optional[Dict[str, str]] = Field(None, description="CSS custom properties")
|
||||
|
||||
|
||||
class ThemePresetPreview(BaseModel):
|
||||
"""Preview information for a theme preset."""
|
||||
name: str = Field(..., description="Preset name")
|
||||
description: str = Field(..., description="Preset description")
|
||||
primary_color: str = Field(..., description="Primary color")
|
||||
secondary_color: str = Field(..., description="Secondary color")
|
||||
accent_color: str = Field(..., description="Accent color")
|
||||
heading_font: str = Field(..., description="Heading font")
|
||||
body_font: str = Field(..., description="Body font")
|
||||
layout_style: str = Field(..., description="Layout style")
|
||||
|
||||
|
||||
class ThemePresetResponse(BaseModel):
|
||||
"""Response after applying a preset."""
|
||||
message: str = Field(..., description="Success message")
|
||||
theme: VendorThemeResponse = Field(..., description="Applied theme")
|
||||
|
||||
|
||||
class ThemePresetListResponse(BaseModel):
|
||||
"""List of available theme presets."""
|
||||
presets: List[ThemePresetPreview] = Field(..., description="Available presets")
|
||||
Reference in New Issue
Block a user