feat: add CMS database model and migrations

Implement Content Management System database layer:

Database Model:
- ContentPage model with two-tier architecture
- Platform defaults (vendor_id=NULL)
- Vendor-specific overrides (vendor_id=123)
- SEO fields (meta_description, meta_keywords)
- Publishing workflow (is_published, published_at)
- Navigation flags (show_in_footer, show_in_header)
- Display ordering and timestamps

Migrations:
- Create content_pages table with all columns
- Add indexes for performance (vendor_id, slug, published status)
- Add unique constraint on (vendor_id, slug)
- Add foreign key relationships with cascade delete

Model Registration:
- Add ContentPage to Vendor relationship
- Import model in alembic/env.py for migration detection

This provides the foundation for managing static content pages
(About, FAQ, Contact, etc.) with platform defaults and vendor overrides.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-22 15:54:29 +01:00
parent 2dfda3e312
commit c219f5b5f8
5 changed files with 245 additions and 0 deletions

View File

@@ -93,6 +93,16 @@ try:
except ImportError as e:
print(f" ✗ VendorTheme model failed: {e}")
# ----------------------------------------------------------------------------
# CONTENT PAGE MODEL (CMS)
# ----------------------------------------------------------------------------
try:
from models.database.content_page import ContentPage
print(" ✓ ContentPage model imported")
except ImportError as e:
print(f" ✗ ContentPage model failed: {e}")
# ----------------------------------------------------------------------------
# PRODUCT MODELS
# ----------------------------------------------------------------------------

View File

@@ -0,0 +1,63 @@
"""Ensure content_pages table with all columns
Revision ID: 72aa309d4007
Revises: fef1d20ce8b4
Create Date: 2025-11-22 15:16:13.213613
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '72aa309d4007'
down_revision: Union[str, None] = 'fef1d20ce8b4'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('content_pages',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('vendor_id', sa.Integer(), nullable=True),
sa.Column('slug', sa.String(length=100), nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('content', sa.Text(), nullable=False),
sa.Column('content_format', sa.String(length=20), nullable=True),
sa.Column('meta_description', sa.String(length=300), nullable=True),
sa.Column('meta_keywords', sa.String(length=300), nullable=True),
sa.Column('is_published', sa.Boolean(), nullable=False),
sa.Column('published_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('display_order', sa.Integer(), nullable=True),
sa.Column('show_in_footer', sa.Boolean(), nullable=True),
sa.Column('show_in_header', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=True),
sa.Column('updated_by', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['updated_by'], ['users.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('vendor_id', 'slug', name='uq_vendor_slug')
)
op.create_index('idx_slug_published', 'content_pages', ['slug', 'is_published'], unique=False)
op.create_index('idx_vendor_published', 'content_pages', ['vendor_id', 'is_published'], unique=False)
op.create_index(op.f('ix_content_pages_id'), 'content_pages', ['id'], unique=False)
op.create_index(op.f('ix_content_pages_slug'), 'content_pages', ['slug'], unique=False)
op.create_index(op.f('ix_content_pages_vendor_id'), 'content_pages', ['vendor_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_content_pages_vendor_id'), table_name='content_pages')
op.drop_index(op.f('ix_content_pages_slug'), table_name='content_pages')
op.drop_index(op.f('ix_content_pages_id'), table_name='content_pages')
op.drop_index('idx_vendor_published', table_name='content_pages')
op.drop_index('idx_slug_published', table_name='content_pages')
op.drop_table('content_pages')
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""Add content_pages table for CMS
Revision ID: fef1d20ce8b4
Revises: fa7d4d10e358
Create Date: 2025-11-22 13:41:18.069674
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'fef1d20ce8b4'
down_revision: Union[str, None] = 'fa7d4d10e358'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('idx_roles_vendor_name', table_name='roles')
op.drop_index('idx_vendor_users_invitation_token', table_name='vendor_users')
op.create_index(op.f('ix_vendor_users_invitation_token'), 'vendor_users', ['invitation_token'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_vendor_users_invitation_token'), table_name='vendor_users')
op.create_index('idx_vendor_users_invitation_token', 'vendor_users', ['invitation_token'], unique=False)
op.create_index('idx_roles_vendor_name', 'roles', ['vendor_id', 'name'], unique=False)
# ### end Alembic commands ###