Files
vidconf/backend/schemas/admin.py
Max Ronzhin 10a3f8b3b4 feat(room): очередь поднятых рук видна всем + отключаемый модуль
Раньше HandQueueMenu.tsx рендерился только организатору — теперь очередь
видит любой участник, но опустить чужую руку по-прежнему может только
организатор (сервер это уже проверял, менял только фронт). Кнопка
«Опустить» показывается у записи, только если это своя рука или
пользователь — организатор.

Модуль «поднятие руки» (кнопка «Рука» + очередь целиком) — отключаемый
в админке (instance_settings.hand_queue, дефолт enabled=true, как у
chat_enabled). Настройка едет участнику в JoinOut ещё до входа в
комнату; выключенный модуль гасит кнопки и на фронте, и на бэке —
raise_hand/lower_hand отклоняются кодом hand_queue_disabled, если
модуль выключен, даже если у клиента на руках старый JoinOut.
2026-08-04 18:27:14 +03:00

189 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
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