379 lines
16 KiB
Python
379 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String, Text, func
|
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.core.database import Base
|
|
from app.models.base import TimestampMixin, UUIDPrimaryKeyMixin
|
|
|
|
|
|
class DocumentFormat(Base, UUIDPrimaryKeyMixin, TimestampMixin):
|
|
"""Reusable document template format."""
|
|
|
|
__tablename__ = "document_formats"
|
|
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
page_width: Mapped[float] = mapped_column(Float, nullable=False)
|
|
page_height: Mapped[float] = mapped_column(Float, nullable=False)
|
|
page_count: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
|
margin_top: Mapped[float] = mapped_column(Float, default=72.0, nullable=False)
|
|
margin_right: Mapped[float] = mapped_column(Float, default=72.0, nullable=False)
|
|
margin_bottom: Mapped[float] = mapped_column(Float, default=72.0, nullable=False)
|
|
margin_left: Mapped[float] = mapped_column(Float, default=72.0, nullable=False)
|
|
fingerprint: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
source_document_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("documents.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
)
|
|
version: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True)
|
|
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("users.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
)
|
|
|
|
cells: Mapped[list[DocumentCell]] = relationship(
|
|
"DocumentCell",
|
|
back_populates="format",
|
|
cascade="all, delete-orphan",
|
|
order_by="DocumentCell.sequence",
|
|
lazy="selectin",
|
|
)
|
|
regions: Mapped[list[DocumentRegion]] = relationship(
|
|
"DocumentRegion",
|
|
back_populates="format",
|
|
cascade="all, delete-orphan",
|
|
order_by="DocumentRegion.sequence",
|
|
lazy="selectin",
|
|
)
|
|
table_formats: Mapped[list[TableFormat]] = relationship(
|
|
"TableFormat",
|
|
back_populates="format",
|
|
cascade="all, delete-orphan",
|
|
lazy="selectin",
|
|
)
|
|
watermarks: Mapped[list[Watermark]] = relationship(
|
|
"Watermark",
|
|
back_populates="format",
|
|
cascade="all, delete-orphan",
|
|
lazy="selectin",
|
|
)
|
|
image_regions: Mapped[list[ImageRegion]] = relationship(
|
|
"ImageRegion",
|
|
back_populates="format",
|
|
cascade="all, delete-orphan",
|
|
lazy="selectin",
|
|
)
|
|
fingerprint_record: Mapped[TemplateFingerprint | None] = relationship(
|
|
"TemplateFingerprint",
|
|
back_populates="format",
|
|
uselist=False,
|
|
cascade="all, delete-orphan",
|
|
lazy="selectin",
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<DocumentFormat(id={self.id}, name={self.name}, v{self.version})>"
|
|
|
|
|
|
class DocumentCell(Base, UUIDPrimaryKeyMixin):
|
|
"""Cell definition within a document template."""
|
|
|
|
__tablename__ = "document_cells"
|
|
|
|
format_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("document_formats.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
page_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
x: Mapped[float] = mapped_column(Float, nullable=False)
|
|
y: Mapped[float] = mapped_column(Float, nullable=False)
|
|
width: Mapped[float] = mapped_column(Float, nullable=False)
|
|
height: Mapped[float] = mapped_column(Float, nullable=False)
|
|
row_no: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
column_no: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
data_type: Mapped[str] = mapped_column(String(50), default="text", nullable=False)
|
|
font_family: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
font_size: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
font_style: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
font_color: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
background_color: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
border_top: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
border_right: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
border_bottom: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
border_left: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
padding_top: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
|
|
padding_right: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
|
|
padding_bottom: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
|
|
padding_left: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
|
|
alignment: Mapped[str] = mapped_column(String(20), default="left", nullable=False)
|
|
vertical_alignment: Mapped[str] = mapped_column(String(20), default="top", nullable=False)
|
|
rowspan: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
|
colspan: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
|
static_text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
field_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
sequence: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
is_dynamic: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=func.now(),
|
|
server_default=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="cells")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<DocumentCell(id={self.id}, page={self.page_number}, row={self.row_no}, col={self.column_no})>"
|
|
|
|
|
|
class DocumentRegion(Base, UUIDPrimaryKeyMixin):
|
|
"""Region definition within a document template."""
|
|
|
|
__tablename__ = "document_regions"
|
|
|
|
format_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("document_formats.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
page_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
region_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
x: Mapped[float] = mapped_column(Float, nullable=False)
|
|
y: Mapped[float] = mapped_column(Float, nullable=False)
|
|
width: Mapped[float] = mapped_column(Float, nullable=False)
|
|
height: Mapped[float] = mapped_column(Float, nullable=False)
|
|
content: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
sequence: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=func.now(),
|
|
server_default=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="regions")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<DocumentRegion(id={self.id}, type={self.region_type}, page={self.page_number})>"
|
|
|
|
|
|
class TableFormat(Base, UUIDPrimaryKeyMixin):
|
|
"""Table definition within a document template."""
|
|
|
|
__tablename__ = "table_formats"
|
|
|
|
format_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("document_formats.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
page_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
x: Mapped[float] = mapped_column(Float, nullable=False)
|
|
y: Mapped[float] = mapped_column(Float, nullable=False)
|
|
width: Mapped[float] = mapped_column(Float, nullable=False)
|
|
height: Mapped[float] = mapped_column(Float, nullable=False)
|
|
rows: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
columns: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
border_style: Mapped[str] = mapped_column(String(50), default="solid", nullable=False)
|
|
border_width: Mapped[float] = mapped_column(Float, default=1.0, nullable=False)
|
|
border_color: Mapped[str] = mapped_column(String(50), default="#000000", nullable=False)
|
|
header_rows: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=func.now(),
|
|
server_default=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="table_formats")
|
|
table_columns: Mapped[list[TableColumn]] = relationship(
|
|
"TableColumn",
|
|
back_populates="table_format",
|
|
cascade="all, delete-orphan",
|
|
order_by="TableColumn.column_index",
|
|
lazy="selectin",
|
|
)
|
|
table_rows: Mapped[list[TableRow]] = relationship(
|
|
"TableRow",
|
|
back_populates="table_format",
|
|
cascade="all, delete-orphan",
|
|
order_by="TableRow.row_index",
|
|
lazy="selectin",
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<TableFormat(id={self.id}, rows={self.rows}, cols={self.columns})>"
|
|
|
|
|
|
class TableColumn(Base, UUIDPrimaryKeyMixin):
|
|
"""Column definition within a table format."""
|
|
|
|
__tablename__ = "table_columns"
|
|
|
|
table_format_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("table_formats.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
column_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
width: Mapped[float] = mapped_column(Float, nullable=False)
|
|
header_text: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
data_type: Mapped[str] = mapped_column(String(50), default="text", nullable=False)
|
|
alignment: Mapped[str] = mapped_column(String(20), default="left", nullable=False)
|
|
font_family: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
font_size: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=func.now(),
|
|
server_default=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
table_format: Mapped[TableFormat] = relationship("TableFormat", back_populates="table_columns")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<TableColumn(id={self.id}, index={self.column_index}, header={self.header_text})>"
|
|
|
|
|
|
class TableRow(Base, UUIDPrimaryKeyMixin):
|
|
"""Row definition within a table format."""
|
|
|
|
__tablename__ = "table_rows"
|
|
|
|
table_format_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("table_formats.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
row_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
height: Mapped[float] = mapped_column(Float, default=20.0, nullable=False)
|
|
is_header: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
background_color: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=func.now(),
|
|
server_default=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
table_format: Mapped[TableFormat] = relationship("TableFormat", back_populates="table_rows")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<TableRow(id={self.id}, index={self.row_index}, is_header={self.is_header})>"
|
|
|
|
|
|
class Watermark(Base, UUIDPrimaryKeyMixin):
|
|
"""Watermark definition within a document template."""
|
|
|
|
__tablename__ = "watermarks"
|
|
|
|
format_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("document_formats.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
page_number: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
text: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
image_path: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
|
x: Mapped[float] = mapped_column(Float, nullable=False)
|
|
y: Mapped[float] = mapped_column(Float, nullable=False)
|
|
width: Mapped[float] = mapped_column(Float, nullable=False)
|
|
height: Mapped[float] = mapped_column(Float, nullable=False)
|
|
opacity: Mapped[float] = mapped_column(Float, default=0.3, nullable=False)
|
|
rotation: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
|
|
font_family: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
font_size: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
font_color: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=func.now(),
|
|
server_default=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="watermarks")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Watermark(id={self.id}, text={self.text})>"
|
|
|
|
|
|
class ImageRegion(Base, UUIDPrimaryKeyMixin):
|
|
"""Image region within a document template."""
|
|
|
|
__tablename__ = "image_regions"
|
|
|
|
format_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("document_formats.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
page_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
x: Mapped[float] = mapped_column(Float, nullable=False)
|
|
y: Mapped[float] = mapped_column(Float, nullable=False)
|
|
width: Mapped[float] = mapped_column(Float, nullable=False)
|
|
height: Mapped[float] = mapped_column(Float, nullable=False)
|
|
image_path: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
|
image_type: Mapped[str] = mapped_column(String(50), default="figure", nullable=False)
|
|
is_static: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
field_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=func.now(),
|
|
server_default=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="image_regions")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<ImageRegion(id={self.id}, type={self.image_type}, page={self.page_number})>"
|
|
|
|
|
|
class TemplateFingerprint(Base, UUIDPrimaryKeyMixin):
|
|
"""Layout fingerprint for template matching."""
|
|
|
|
__tablename__ = "template_fingerprints"
|
|
|
|
format_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("document_formats.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
unique=True,
|
|
index=True,
|
|
)
|
|
page_dimensions: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
logo_coordinates: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
header_coordinates: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
footer_coordinates: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
table_coordinates: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
cell_coordinates: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
fingerprint_hash: Mapped[str] = mapped_column(String(256), nullable=False, index=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
default=func.now(),
|
|
server_default=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="fingerprint_record")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<TemplateFingerprint(id={self.id}, format_id={self.format_id}, hash={self.fingerprint_hash[:16]})>"
|