Changes committed
This commit is contained in:
378
docengine/app/models/template.py
Normal file
378
docengine/app/models/template.py
Normal file
@@ -0,0 +1,378 @@
|
||||
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]})>"
|
||||
Reference in New Issue
Block a user