"""Pydantic-схемы админ-API (`/api/v1/admin/*`).""" import uuid from datetime import datetime from typing import Literal from pydantic import BaseModel, ConfigDict, EmailStr, Field from core.plugins.config import AiLevel, PublishQualityCap, StageMaxTiles, SummaryRecipientsMode from schemas.conferences import ConferenceOut from services.ai_levels import AiLevelStatus class AdminConferenceOut(ConferenceOut): """Конференция в ответах админ-API — `ConferenceOut` + данные владельца. `owner_name`/`owner_email` — `None` для конференции без владельца (`conferences.owner_id IS NULL`, ADR-001) — колонка «Владелец» в таблице админки (`frontend/src/api/admin.ts`). """ owner_name: str | None = None owner_email: str | None = None class AdminConferenceListOut(BaseModel): """Страница списка конференций (`GET /admin/conferences`).""" items: list[AdminConferenceOut] total: int class AdminUserOut(BaseModel): """Пользователь в ответах админ-API (профиль + служебные поля модерации). `avatar_url`/`team_name` заполняются явно роутером (не через `from_attributes` — оба поля вычисляемые, не хранятся как атрибут ORM), см. `api/admin.py::_to_admin_user_out`. """ model_config = ConfigDict(from_attributes=True) id: uuid.UUID email: str name_user: str role: str is_blocked: bool email_verified: bool created_at: datetime team_id: uuid.UUID | None = None avatar_url: str | None = None team_name: str | None = None class AdminUserListOut(BaseModel): """Страница списка пользователей (`GET /admin/users`).""" items: list[AdminUserOut] total: int class AdminUserCreateIn(BaseModel): """Тело создания пользователя администратором (`POST /admin/users`). Та же политика пароля, что при самостоятельной регистрации (`RegisterIn.password`, min 8 символов). `team_id` — опциональная привязка к команде, в отличие от публичной регистрации не завязана на настройку `registration_team_choice` (админ назначает команду всегда). """ name_user: str = Field(min_length=1, max_length=255) email: EmailStr password: str = Field(min_length=8) team_id: uuid.UUID | None = None class AdminUserUpdateIn(BaseModel): """Тело правки пользователя администратором — роль, блокировка, ФИО и/или команда. `team_id` не различает «поле не передано» и «явный `null`» через сравнение с `None` — роутер читает `model_fields_set`, чтобы явный сброс команды (`{"team_id": null}`) отличался от отсутствия поля в запросе (тот же паттерн, что `summary_recipients` в `schemas/conferences.py`). `name_user` — та же правка, что доступна пользователю в своём профиле (редактирование по тем же правилам). """ role: Literal["admin", "user"] | None = None is_blocked: bool | None = None team_id: uuid.UUID | None = None name_user: str | None = Field(default=None, min_length=1, max_length=255) class TeamOut(BaseModel): """Команда в ответах админ-API.""" model_config = ConfigDict(from_attributes=True) id: uuid.UUID name: str created_at: datetime class TeamListOut(BaseModel): """Список команд (`GET /admin/teams`), отсортирован по названию.""" items: list[TeamOut] total: int class TeamCreateIn(BaseModel): """Тело создания команды; `name` обрезается от пробелов до проверки длины.""" model_config = ConfigDict(str_strip_whitespace=True) name: str = Field(min_length=1, max_length=255) class TeamUpdateIn(BaseModel): """Тело переименования команды; `name` обрезается от пробелов до проверки длины.""" model_config = ConfigDict(str_strip_whitespace=True) name: str = Field(min_length=1, max_length=255) class InvitationsSendIn(BaseModel): """Тело ручной рассылки .ics-приглашений (`POST /admin/conferences/{id}/invitations`). Пустой список получателей не отличается от отсутствующего поля — оба трактуются как «получатели по умолчанию» (см. `workers/tasks/invitations.py`). """ emails: list[EmailStr] | None = None class SettingsOut(BaseModel): """Эффективные настройки инстанса для отображения в админке. `transcription_queue_served` — обслуживается ли очередь `transcription` хотя бы одним воркером Celery прямо сейчас (`services.pipeline_producer.transcription_queue_served`); `False` при включённой транскрибации — сигнал админке показать предупреждение рядом с чекбоксом AI («модуль включён, но задачи некому обрабатывать»), не связано с доступностью уровня AI (`ai_levels`, которая смотрит только на железо/скачанные модели). """ chat_enabled: bool hand_queue_enabled: bool transcription_enabled: bool ai_level: AiLevel ai_levels: list[AiLevelStatus] transcription_queue_served: bool summary_recipients: SummaryRecipientsMode display_timezone: str registration_team_choice: bool registration_email_domain_enabled: bool registration_email_domains: list[str] = Field(default_factory=list) contact_email_enabled: bool contact_email: str | None = None # Рычаги нагрузки медиа (`InstanceConfig.media_limits`) — потолок # качества публикации и максимум плиток сцены, см. `core/plugins/config.py`. publish_quality_cap: PublishQualityCap stage_max_tiles: StageMaxTiles # Согласие на обработку персональных данных при регистрации — см. # `core/plugins/config.py::InstanceConfig`. consent_required: bool consent_policy_text: str consent_policy_version: int class TestEmailIn(BaseModel): """Тело запроса тестовой отправки (`POST /admin/settings/test-email`). `to` не задан — получатель по умолчанию: email текущего администратора (см. `api/admin.py::send_test_email`). """ to: EmailStr | None = None class TestEmailOut(BaseModel): """Результат тестовой отправки — понятный админу успех/ошибка транспорта. `smtp_host`/`smtp_port` — только хост и порт (без логина/пароля, они не покидают `.env`), заполняются лишь при `EMAIL_BACKEND=smtp`. """ success: bool message: str smtp_host: str | None = None smtp_port: int | None = None