This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
125 lines
3.7 KiB
Python
125 lines
3.7 KiB
Python
# app/modules/cms/models/media.py
|
|
"""
|
|
CORE media file model for vendor media library.
|
|
|
|
This is a CORE framework model used across multiple modules.
|
|
MediaFile provides vendor-uploaded media files (images, documents, videos).
|
|
|
|
For product-media associations, use:
|
|
from app.modules.catalog.models import ProductMedia
|
|
|
|
Files are stored in vendor-specific directories:
|
|
uploads/vendors/{vendor_id}/{folder}/{filename}
|
|
"""
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
Column,
|
|
ForeignKey,
|
|
Index,
|
|
Integer,
|
|
String,
|
|
Text,
|
|
)
|
|
from sqlalchemy.dialects.sqlite import JSON
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from app.core.database import Base
|
|
from models.database.base import TimestampMixin
|
|
|
|
|
|
class MediaFile(Base, TimestampMixin):
|
|
"""Vendor media file record.
|
|
|
|
Stores metadata about uploaded files. Actual files are stored
|
|
in the filesystem at uploads/vendors/{vendor_id}/{folder}/
|
|
"""
|
|
|
|
__tablename__ = "media_files"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
|
|
|
# File identification
|
|
filename = Column(String(255), nullable=False) # Stored filename (UUID-based)
|
|
original_filename = Column(String(255)) # Original uploaded filename
|
|
file_path = Column(String(500), nullable=False) # Relative path from uploads/
|
|
|
|
# File properties
|
|
media_type = Column(String(20), nullable=False) # image, video, document
|
|
mime_type = Column(String(100))
|
|
file_size = Column(Integer) # bytes
|
|
|
|
# Image/video dimensions
|
|
width = Column(Integer)
|
|
height = Column(Integer)
|
|
|
|
# Thumbnail (for images/videos)
|
|
thumbnail_path = Column(String(500))
|
|
|
|
# Metadata
|
|
alt_text = Column(String(500))
|
|
description = Column(Text)
|
|
folder = Column(String(100), default="general") # products, general, etc.
|
|
tags = Column(JSON) # List of tags for categorization
|
|
extra_metadata = Column(JSON) # Additional metadata (EXIF, etc.)
|
|
|
|
# Status
|
|
is_optimized = Column(Boolean, default=False)
|
|
optimized_size = Column(Integer) # Size after optimization
|
|
|
|
# Usage tracking
|
|
usage_count = Column(Integer, default=0) # How many times used
|
|
|
|
# Relationships
|
|
vendor = relationship("Vendor", back_populates="media_files")
|
|
# ProductMedia relationship uses string reference to avoid circular import
|
|
product_associations = relationship(
|
|
"ProductMedia",
|
|
back_populates="media",
|
|
cascade="all, delete-orphan",
|
|
)
|
|
|
|
__table_args__ = (
|
|
Index("idx_media_vendor_id", "vendor_id"),
|
|
Index("idx_media_vendor_folder", "vendor_id", "folder"),
|
|
Index("idx_media_vendor_type", "vendor_id", "media_type"),
|
|
Index("idx_media_filename", "filename"),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"<MediaFile(id={self.id}, vendor_id={self.vendor_id}, "
|
|
f"filename='{self.filename}', type='{self.media_type}')>"
|
|
)
|
|
|
|
@property
|
|
def file_url(self) -> str:
|
|
"""Get the public URL for this file."""
|
|
return f"/uploads/{self.file_path}"
|
|
|
|
@property
|
|
def thumbnail_url(self) -> str | None:
|
|
"""Get the thumbnail URL if available."""
|
|
if self.thumbnail_path:
|
|
return f"/uploads/{self.thumbnail_path}"
|
|
return None
|
|
|
|
@property
|
|
def is_image(self) -> bool:
|
|
"""Check if this is an image file."""
|
|
return self.media_type == "image"
|
|
|
|
@property
|
|
def is_video(self) -> bool:
|
|
"""Check if this is a video file."""
|
|
return self.media_type == "video"
|
|
|
|
@property
|
|
def is_document(self) -> bool:
|
|
"""Check if this is a document file."""
|
|
return self.media_type == "document"
|
|
|
|
|
|
__all__ = ["MediaFile"]
|