Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
"""Модель 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()
)