"""Модель 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() )