90 lines
4.2 KiB
Python
90 lines
4.2 KiB
Python
"""Модель Conference — конференция как пользовательская сущность (ADR-001).
|
||
|
||
Конференция — постоянная сущность с номером и ссылкой; каждый её запуск
|
||
порождает отдельную запись `ConferenceSession` (см. `models/session.py`),
|
||
которая и является единицей AI-пайплайна пост-обработки.
|
||
"""
|
||
|
||
import uuid
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
from sqlalchemy import (
|
||
Boolean,
|
||
CheckConstraint,
|
||
DateTime,
|
||
Enum,
|
||
ForeignKey,
|
||
Integer,
|
||
String,
|
||
Text,
|
||
func,
|
||
text,
|
||
)
|
||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from models.base import Base
|
||
|
||
# Жизненный цикл конференции (ADR-001, п.2): `draft` не нужен — создание
|
||
# атомарно из формы; `is_pinned` — ортогональный флаг, а не статус.
|
||
ConferenceStatus = Enum(
|
||
"scheduled",
|
||
"active",
|
||
"ended",
|
||
name="conference_status",
|
||
)
|
||
|
||
|
||
class Conference(Base):
|
||
"""Конференция — номер, постоянная ссылка, владелец, признаки закрепления/закрытости."""
|
||
|
||
__tablename__ = "conferences"
|
||
__table_args__ = (
|
||
CheckConstraint(
|
||
"is_closed = false OR password_hash IS NOT NULL",
|
||
name="ck_conferences_closed_requires_password",
|
||
),
|
||
CheckConstraint(
|
||
"recurrence IS NULL OR is_pinned = true",
|
||
name="ck_conferences_recurrence_requires_pinned",
|
||
),
|
||
CheckConstraint(
|
||
"summary_recipients IN ('all', 'owner')",
|
||
name="ck_conferences_summary_recipients",
|
||
),
|
||
)
|
||
|
||
id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")
|
||
)
|
||
# 9 десятичных цифр, первая 1..9 (services/conference_ids.py::generate_number).
|
||
number: Mapped[str] = mapped_column(String(9), unique=True, nullable=False)
|
||
# secrets.token_urlsafe(8) — 11 base64url-символов (services/conference_ids.py::generate_slug).
|
||
slug: Mapped[str] = mapped_column(String(22), unique=True, nullable=False)
|
||
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||
owner_id: Mapped[uuid.UUID | None] = mapped_column(
|
||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||
)
|
||
status: Mapped[str] = mapped_column(
|
||
ConferenceStatus, nullable=False, server_default="scheduled"
|
||
)
|
||
is_pinned: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false")
|
||
is_closed: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false")
|
||
password_hash: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
scheduled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||
duration_minutes: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
# Pydantic-модель RecurrenceRule (services/recurrence.py), сериализованная в JSONB.
|
||
recurrence: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
|
||
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||
# Переопределение рассылки саммари конкретной конференции;
|
||
# NULL = использовать дефолт инстанса (`instance_settings['summary_recipients']`).
|
||
summary_recipients: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||
# Счётчик изменений расписания для VEVENT `SEQUENCE` (.ics):
|
||
# растёт при правке `scheduled_at`/`duration_minutes`/`recurrence`/`title`,
|
||
# чтобы календарные клиенты обновляли уже принятое приглашение по UID.
|
||
ics_sequence: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0")
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|