state-hub/api/models/suggestion.py
tegwick cb1b028fd1
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 24s
feat(identifiers): add reversible projection migration
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
2026-08-22 00:27:48 +02:00

124 lines
4.8 KiB
Python

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", onupdate="CASCADE"),
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", onupdate="CASCADE"),
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
)