209 lines
9.9 KiB
Python
209 lines
9.9 KiB
Python
"""
|
|
SQLAlchemy database models
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
from sqlalchemy import String, Integer, DateTime, Float, ForeignKey, JSON, Text, UniqueConstraint
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
"""Base class for all database models"""
|
|
pass
|
|
|
|
|
|
class User(Base):
|
|
"""User model for authentication and authorization"""
|
|
__tablename__ = "users"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
username: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, index=True)
|
|
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
role: Mapped[str] = mapped_column(String(20), nullable=False) # "Admin" or "User"
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
default=datetime.utcnow,
|
|
onupdate=datetime.utcnow,
|
|
nullable=False
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<User(id={self.id}, username='{self.username}', role='{self.role}')>"
|
|
|
|
def is_admin(self) -> bool:
|
|
"""Check if user has admin role"""
|
|
return self.role == "Admin"
|
|
|
|
|
|
class SiteDeployment(Base):
|
|
"""Site deployment model for bunny.net infrastructure tracking"""
|
|
__tablename__ = "site_deployments"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
site_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
custom_hostname: Mapped[Optional[str]] = mapped_column(String(255), unique=True, nullable=True, index=True)
|
|
storage_zone_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
storage_zone_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
storage_zone_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
storage_zone_region: Mapped[str] = mapped_column(String(10), nullable=False)
|
|
pull_zone_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
pull_zone_bcdn_hostname: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
|
template_name: Mapped[str] = mapped_column(String(50), default="basic", nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
default=datetime.utcnow,
|
|
onupdate=datetime.utcnow,
|
|
nullable=False
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
hostname = self.custom_hostname or self.pull_zone_bcdn_hostname
|
|
return f"<SiteDeployment(id={self.id}, site_name='{self.site_name}', hostname='{hostname}')>"
|
|
|
|
|
|
class Project(Base):
|
|
"""Project model for CORA-ingested SEO data"""
|
|
__tablename__ = "projects"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('users.id'), nullable=False, index=True)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
main_keyword: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
|
tier: Mapped[int] = mapped_column(Integer, nullable=False, default=1, index=True)
|
|
money_site_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, index=True)
|
|
|
|
word_count: Mapped[int] = mapped_column(Integer, nullable=False, default=1250)
|
|
term_frequency: Mapped[int] = mapped_column(Integer, nullable=False, default=3)
|
|
related_search_density: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
entity_density: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
lsi_density: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
|
|
title_exact_match: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
title_related_search: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
meta_exact_match: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
meta_related_search: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
meta_entities: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
|
|
h1_exact: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h1_related_search: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h1_entities: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h1_lsi: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
|
|
h2_total: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h2_exact: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h2_related_search: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h2_entities: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h2_lsi: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
|
|
h3_total: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h3_exact: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h3_related_search: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h3_entities: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
h3_lsi: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
|
|
entities: Mapped[Optional[list]] = mapped_column(JSON, nullable=True)
|
|
related_searches: Mapped[Optional[list]] = mapped_column(JSON, nullable=True)
|
|
custom_anchor_text: Mapped[Optional[list]] = mapped_column(JSON, nullable=True)
|
|
|
|
spintax_related_search_terms: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
default=datetime.utcnow,
|
|
onupdate=datetime.utcnow,
|
|
nullable=False
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Project(id={self.id}, name='{self.name}', main_keyword='{self.main_keyword}', user_id={self.user_id})>"
|
|
|
|
|
|
class GeneratedContent(Base):
|
|
"""Generated content model for AI-created articles"""
|
|
__tablename__ = "generated_content"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
project_id: Mapped[int] = mapped_column(Integer, ForeignKey('projects.id'), nullable=False, index=True)
|
|
tier: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
|
keyword: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
|
title: Mapped[str] = mapped_column(Text, nullable=False)
|
|
outline: Mapped[dict] = mapped_column(JSON, nullable=False)
|
|
content: Mapped[str] = mapped_column(Text, nullable=False)
|
|
word_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
status: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
formatted_html: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
template_used: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
|
site_deployment_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('site_deployments.id'), nullable=True, index=True)
|
|
deployed_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
deployed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, index=True)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
default=datetime.utcnow,
|
|
onupdate=datetime.utcnow,
|
|
nullable=False
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<GeneratedContent(id={self.id}, project_id={self.project_id}, tier='{self.tier}', status='{self.status}')>"
|
|
|
|
|
|
class ArticleLink(Base):
|
|
"""Article link tracking model for tiered linking, wheel links, etc."""
|
|
__tablename__ = "article_links"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
from_content_id: Mapped[int] = mapped_column(
|
|
Integer,
|
|
ForeignKey('generated_content.id', ondelete='CASCADE'),
|
|
nullable=False,
|
|
index=True
|
|
)
|
|
to_content_id: Mapped[Optional[int]] = mapped_column(
|
|
Integer,
|
|
ForeignKey('generated_content.id', ondelete='CASCADE'),
|
|
nullable=True,
|
|
index=True
|
|
)
|
|
to_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
anchor_text: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
link_type: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
|
|
|
def __repr__(self) -> str:
|
|
target = f"content_id={self.to_content_id}" if self.to_content_id else f"url={self.to_url}"
|
|
return f"<ArticleLink(id={self.id}, from={self.from_content_id}, to={target}, type='{self.link_type}')>"
|
|
|
|
|
|
class SitePage(Base):
|
|
"""Boilerplate pages for sites (about, contact, privacy)"""
|
|
__tablename__ = "site_pages"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
site_deployment_id: Mapped[int] = mapped_column(
|
|
Integer,
|
|
ForeignKey('site_deployments.id', ondelete='CASCADE'),
|
|
nullable=False,
|
|
index=True
|
|
)
|
|
page_type: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
|
content: Mapped[str] = mapped_column(Text, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime,
|
|
default=datetime.utcnow,
|
|
onupdate=datetime.utcnow,
|
|
nullable=False
|
|
)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint('site_deployment_id', 'page_type', name='uq_site_page_type'),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<SitePage(id={self.id}, site_id={self.site_deployment_id}, page_type='{self.page_type}')>"
|