test_task_crm/app/models/activity.py

90 lines
2.6 KiB
Python

"""Activity timeline ORM model and schemas."""
from __future__ import annotations
from datetime import datetime
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import DateTime, ForeignKey, Integer, func, text
from sqlalchemy import Enum as SqlEnum
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.engine import Dialect
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.types import JSON as SA_JSON
from sqlalchemy.types import TypeDecorator
from sqlalchemy.sql.type_api import TypeEngine
from app.models.base import Base, enum_values
class ActivityType(StrEnum):
COMMENT = "comment"
STATUS_CHANGED = "status_changed"
STAGE_CHANGED = "stage_changed"
TASK_CREATED = "task_created"
SYSTEM = "system"
class JSONBCompat(TypeDecorator):
"""Uses JSONB on Postgres and plain JSON elsewhere for testability."""
impl = JSONB
cache_ok = True
def load_dialect_impl(self, dialect: Dialect) -> TypeEngine[Any]:
if dialect.name == "sqlite":
from sqlalchemy.dialects.sqlite import JSON as SQLITE_JSON # local import
return dialect.type_descriptor(SQLITE_JSON())
return dialect.type_descriptor(JSONB())
class Activity(Base):
"""Represents a timeline event for a deal."""
__tablename__ = "activities"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
deal_id: Mapped[int] = mapped_column(ForeignKey("deals.id", ondelete="CASCADE"))
author_id: Mapped[int | None] = mapped_column(
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
)
type: Mapped[ActivityType] = mapped_column(
SqlEnum(ActivityType, name="activity_type", values_callable=enum_values),
nullable=False,
)
payload: Mapped[dict[str, Any]] = mapped_column(
JSONBCompat().with_variant(SA_JSON(), "sqlite"),
nullable=False,
server_default=text("'{}'"),
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
deal = relationship("Deal", back_populates="activities")
author = relationship("User", back_populates="activities")
class ActivityBase(BaseModel):
deal_id: int
author_id: int | None = None
type: ActivityType
payload: dict[str, Any] = Field(default_factory=dict)
class ActivityCreate(ActivityBase):
pass
class ActivityRead(ActivityBase):
id: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)