import enum import uuid from datetime import datetime from sqlalchemy import DateTime, Enum, Float, ForeignKey, Integer, String, Text from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from api.models.base import Base, TimestampMixin, new_uuid class SuggestionStage(str, enum.Enum): suggestion = "suggestion" requirement = "requirement" promoted = "promoted" declined = "declined" OPEN_SUGGESTION_STAGES = (SuggestionStage.suggestion, SuggestionStage.requirement) class Suggestion(Base, TimestampMixin): __tablename__ = "suggestions" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=new_uuid ) domain_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("domains.id", ondelete="RESTRICT"), nullable=False, index=True, ) topic_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("topics.id", ondelete="SET NULL"), nullable=True ) workplan_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("workplans.id", ondelete="SET NULL"), nullable=True ) title: Mapped[str] = mapped_column(String(500), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) origin: Mapped[str | None] = mapped_column(String(200), nullable=True) origin_ref: Mapped[str | None] = mapped_column(String(200), nullable=True, index=True) stage: Mapped[SuggestionStage] = mapped_column( Enum(SuggestionStage, name="suggestionstage"), nullable=False, default=SuggestionStage.suggestion, index=True, ) relevance: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0") relevance_events: Mapped[int] = mapped_column( Integer, nullable=False, default=0, server_default="0" ) last_requested_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) base_value: Mapped[float] = mapped_column(Float, nullable=False, default=3.0, server_default="3") job_size: Mapped[float] = mapped_column(Float, nullable=False, default=3.0, server_default="3") relevance_weight: Mapped[float] = mapped_column( Float, nullable=False, default=1.0, server_default="1" ) promoted_task_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True ) domain: Mapped["Domain"] = relationship("Domain", lazy="selectin") # noqa: F821 topic: Mapped["Topic | None"] = relationship("Topic", lazy="selectin") # noqa: F821 workplan: Mapped["Workplan | None"] = relationship("Workplan", lazy="selectin") # noqa: F821 promoted_task: Mapped["Task | None"] = relationship("Task", lazy="selectin") # noqa: F821 notes: Mapped[list["SuggestionNote"]] = relationship( "SuggestionNote", back_populates="suggestion", lazy="selectin", order_by="SuggestionNote.created_at", ) @property def domain_slug(self) -> str: return self.domain.slug if self.domain is not None else "" class SuggestionNote(Base): __tablename__ = "suggestion_notes" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=new_uuid) suggestion_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("suggestions.id", ondelete="CASCADE"), nullable=False, index=True, ) stage: Mapped[str] = mapped_column(String(30), nullable=False) author: Mapped[str | None] = mapped_column(String(100), nullable=True) content: Mapped[str] = mapped_column(Text, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) suggestion: Mapped["Suggestion"] = relationship("Suggestion", back_populates="notes") class SuggestionRelevanceBump(Base): """Audit trail for relevance bumps; supports debounce lookups.""" __tablename__ = "suggestion_relevance_bumps" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=new_uuid) suggestion_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("suggestions.id", ondelete="CASCADE"), nullable=False, index=True, ) source: Mapped[str] = mapped_column(String(50), nullable=False) source_key: Mapped[str] = mapped_column(String(200), nullable=False) reason: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False )