90 lines
3.1 KiB
Python
90 lines
3.1 KiB
Python
import enum
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, Enum, ForeignKey, String, Text, UniqueConstraint, func
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.core.database import Base
|
|
|
|
|
|
class FitLabel(enum.StrEnum):
|
|
POSSIBLE = "possible"
|
|
EXACT = "exact"
|
|
NONE = "none"
|
|
|
|
|
|
class Fit(Base):
|
|
"""A candidate or confirmed fit between two attachment points on different parts."""
|
|
|
|
__tablename__ = "fits"
|
|
__table_args__ = (
|
|
UniqueConstraint("attachment_point_a_id", "attachment_point_b_id"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
|
|
# Canonical ordering (attachment_point_a_id < attachment_point_b_id) is enforced
|
|
# by app.crud.fit so an unordered pair only ever has one row.
|
|
attachment_point_a_id: Mapped[int] = mapped_column(
|
|
ForeignKey("attachment_points.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
attachment_point_b_id: Mapped[int] = mapped_column(
|
|
ForeignKey("attachment_points.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
|
|
# Denormalized from the attachment points' augmented_part_id, for querying
|
|
# "fits for this part" without joining through attachment_points.
|
|
augmented_part_a_id: Mapped[int] = mapped_column(
|
|
ForeignKey("augmented_parts.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
augmented_part_b_id: Mapped[int] = mapped_column(
|
|
ForeignKey("augmented_parts.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
|
|
label: Mapped[FitLabel] = mapped_column(
|
|
Enum(FitLabel, values_callable=lambda enum_cls: [e.value for e in enum_cls]),
|
|
nullable=False,
|
|
default=FitLabel.POSSIBLE,
|
|
)
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now()
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
attachment_point_a = relationship(
|
|
"AttachmentPoint", foreign_keys=[attachment_point_a_id], lazy="selectin"
|
|
)
|
|
attachment_point_b = relationship(
|
|
"AttachmentPoint", foreign_keys=[attachment_point_b_id], lazy="selectin"
|
|
)
|
|
notes: Mapped[list["FitNote"]] = relationship(
|
|
back_populates="fit",
|
|
lazy="selectin",
|
|
order_by="FitNote.created_at",
|
|
cascade="all, delete-orphan",
|
|
)
|
|
|
|
|
|
class FitNote(Base):
|
|
__tablename__ = "fit_notes"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
fit_id: Mapped[int] = mapped_column(
|
|
ForeignKey("fits.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
user_id: Mapped[int | None] = mapped_column(
|
|
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
# Denormalized snapshot of who wrote the note (mirrors AuditLog.user_email),
|
|
# so the note stays attributable even if the user is later deleted.
|
|
username: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
body: Mapped[str] = mapped_column(Text, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now()
|
|
)
|
|
|
|
fit: Mapped[Fit] = relationship(back_populates="notes")
|