v0.1 - макет интерфейса, аутентификация через логин, аккаунт админа, создание партии в 2 этапа, базовые настройки профиля и группы, переключение между группами, статистика
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
"""Pydantic-схемы (граница HTTP). Из них генерируется OpenAPI → типы фронта."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
WinReason = Literal["objectives", "worlds", "plastic", "resources"]
|
||||
|
||||
|
||||
# ─── Auth ────────────────────────────────────────────────────────────────────
|
||||
|
||||
class AuthConfig(BaseModel):
|
||||
# Доступные методы входа: ["telegram"] в проде, ["telegram","stub"] в деве.
|
||||
methods: list[str] = []
|
||||
telegram_bot_username: str | None = None
|
||||
|
||||
|
||||
class TelegramAuthPayload(BaseModel):
|
||||
# Полезная нагрузка Telegram Login Widget (проверяется по HMAC).
|
||||
model_config = ConfigDict(extra="allow")
|
||||
id: int
|
||||
auth_date: int
|
||||
hash: str
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
username: str | None = None
|
||||
photo_url: str | None = None
|
||||
|
||||
|
||||
class DevLogin(BaseModel):
|
||||
nickname: str
|
||||
|
||||
|
||||
class DevUserCreate(BaseModel):
|
||||
nickname: str
|
||||
|
||||
|
||||
class DevUserRead(BaseModel):
|
||||
id: int
|
||||
nickname: str
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class OkResponse(BaseModel):
|
||||
ok: bool = True
|
||||
|
||||
|
||||
# ─── Справочники ─────────────────────────────────────────────────────────────
|
||||
|
||||
class ExpansionRead(BaseModel):
|
||||
id: int
|
||||
code: str
|
||||
name_ru: str
|
||||
is_base: bool
|
||||
|
||||
|
||||
class FactionRead(BaseModel):
|
||||
id: int
|
||||
code: str
|
||||
name_ru: str
|
||||
expansion_id: int
|
||||
|
||||
|
||||
class FactionRename(BaseModel):
|
||||
name_ru: str
|
||||
|
||||
|
||||
# ─── Пользователь ────────────────────────────────────────────────────────────
|
||||
|
||||
class GroupBrief(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
role: str
|
||||
|
||||
|
||||
class UserRead(BaseModel):
|
||||
id: int
|
||||
nickname: str
|
||||
role: str
|
||||
auth_provider: str
|
||||
telegram_id: int | None = None
|
||||
active_group_id: int | None = None
|
||||
|
||||
|
||||
class MeRead(UserRead):
|
||||
groups: list[GroupBrief] = []
|
||||
|
||||
|
||||
class NicknameUpdate(BaseModel):
|
||||
nickname: str
|
||||
|
||||
|
||||
class ActiveGroupUpdate(BaseModel):
|
||||
group_id: int | None = None
|
||||
|
||||
|
||||
class NicknameAvailable(BaseModel):
|
||||
available: bool
|
||||
|
||||
|
||||
# ─── Группы и членство ───────────────────────────────────────────────────────
|
||||
|
||||
class GroupCreate(BaseModel):
|
||||
name: str
|
||||
expansion_ids: list[int] = []
|
||||
|
||||
|
||||
class GroupRename(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class GroupExpansionsUpdate(BaseModel):
|
||||
expansion_ids: list[int] = []
|
||||
|
||||
|
||||
class GroupDetail(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
owner_id: int
|
||||
my_role: str
|
||||
expansion_ids: list[int] = []
|
||||
|
||||
|
||||
class MemberRead(BaseModel):
|
||||
user_id: int
|
||||
nickname: str
|
||||
role: str
|
||||
|
||||
|
||||
class MemberAdd(BaseModel):
|
||||
nickname: str
|
||||
|
||||
|
||||
class MemberRoleUpdate(BaseModel):
|
||||
role: str
|
||||
|
||||
|
||||
# ─── Партии ──────────────────────────────────────────────────────────────────
|
||||
|
||||
class RandomizeRequest(BaseModel):
|
||||
group_id: int
|
||||
exclude_faction_ids: list[int] = []
|
||||
|
||||
|
||||
class RandomizeResponse(BaseModel):
|
||||
faction: FactionRead
|
||||
|
||||
|
||||
# Этап 1 (старт): только ростер, без мест.
|
||||
class RosterParticipant(BaseModel):
|
||||
user_id: int
|
||||
faction_id: int
|
||||
was_random: bool = False
|
||||
|
||||
|
||||
class MatchCreate(BaseModel):
|
||||
group_id: int
|
||||
participants: list[RosterParticipant]
|
||||
|
||||
|
||||
# Этап 2 (завершение): места, комментарии, причина победы.
|
||||
class MatchFinishParticipant(BaseModel):
|
||||
user_id: int
|
||||
place: int = Field(ge=1)
|
||||
comment: str | None = None
|
||||
faction_id: int | None = None # опц. смена фракции при завершении
|
||||
|
||||
|
||||
class MatchFinish(BaseModel):
|
||||
participants: list[MatchFinishParticipant]
|
||||
win_reason: WinReason
|
||||
overall_comment: str | None = None
|
||||
|
||||
|
||||
# Полный участник (правка завершённой партии админом).
|
||||
class ParticipantInput(BaseModel):
|
||||
user_id: int
|
||||
faction_id: int
|
||||
place: int = Field(ge=1)
|
||||
was_random: bool = False
|
||||
comment: str | None = None
|
||||
|
||||
|
||||
class MatchUpdate(BaseModel):
|
||||
played_at: date | None = None
|
||||
overall_comment: str | None = None
|
||||
win_reason: WinReason | None = None
|
||||
participants: list[ParticipantInput] | None = None
|
||||
|
||||
|
||||
class MatchParticipantRead(BaseModel):
|
||||
user_id: int
|
||||
nickname: str
|
||||
faction_id: int
|
||||
faction_name: str
|
||||
place: int | None = None
|
||||
was_random: bool
|
||||
comment: str | None = None
|
||||
|
||||
|
||||
class MatchRead(BaseModel):
|
||||
id: int
|
||||
group_id: int
|
||||
status: str
|
||||
played_at: date
|
||||
started_at: str | None = None
|
||||
finished_at: str | None = None
|
||||
duration_minutes: int | None = None
|
||||
win_reason: WinReason | None = None
|
||||
player_count: int
|
||||
overall_comment: str | None = None
|
||||
created_by: int
|
||||
can_modify: bool = False # может ли текущий зритель править/завершать партию
|
||||
participants: list[MatchParticipantRead] = []
|
||||
attachments: list[Any] = [] # задел под вложения (всегда пусто в v1)
|
||||
|
||||
|
||||
# ─── Статистика ──────────────────────────────────────────────────────────────
|
||||
|
||||
class OverallStats(BaseModel):
|
||||
games: int
|
||||
wins: int
|
||||
win_rate: float
|
||||
avg_place: float | None = None
|
||||
score: float | None = None
|
||||
|
||||
|
||||
class LeaderboardEntry(OverallStats):
|
||||
user_id: int
|
||||
nickname: str
|
||||
rank: int | None = None
|
||||
|
||||
|
||||
class Leaderboard(BaseModel):
|
||||
entries: list[LeaderboardEntry] = []
|
||||
provisional: list[LeaderboardEntry] = []
|
||||
min_games: int
|
||||
|
||||
|
||||
class FactionStat(BaseModel):
|
||||
faction_id: int
|
||||
code: str
|
||||
name_ru: str
|
||||
expansion_code: str
|
||||
games: int
|
||||
wins: int
|
||||
win_rate: float
|
||||
avg_place: float | None = None
|
||||
score: float | None = None
|
||||
|
||||
|
||||
class RecentFormItem(BaseModel):
|
||||
place: int
|
||||
player_count: int
|
||||
played_at: str
|
||||
|
||||
|
||||
class ProfileStats(BaseModel):
|
||||
user_id: int
|
||||
overall: OverallStats
|
||||
factions: list[FactionStat] = []
|
||||
best_faction: FactionStat | None = None
|
||||
worst_faction: FactionStat | None = None
|
||||
most_played_faction: FactionStat | None = None
|
||||
recent_form: list[RecentFormItem] = []
|
||||
|
||||
|
||||
class FactionMeta(BaseModel):
|
||||
faction_id: int
|
||||
code: str
|
||||
name_ru: str
|
||||
games: int
|
||||
wins: int
|
||||
available: bool
|
||||
|
||||
|
||||
class GroupStats(BaseModel):
|
||||
group_id: int
|
||||
total_matches: int
|
||||
last_match_at: str | None = None
|
||||
leaderboard: list[LeaderboardEntry] = []
|
||||
provisional: list[LeaderboardEntry] = []
|
||||
faction_meta: list[FactionMeta] = []
|
||||
min_games: int
|
||||
|
||||
|
||||
class MatchListParticipant(BaseModel):
|
||||
user_id: int
|
||||
nickname: str
|
||||
faction_id: int
|
||||
faction_name: str
|
||||
place: int | None = None
|
||||
was_random: bool
|
||||
comment: str | None = None
|
||||
|
||||
|
||||
class MatchListItem(BaseModel):
|
||||
id: int
|
||||
status: str
|
||||
played_at: str
|
||||
started_at: str | None = None
|
||||
finished_at: str | None = None
|
||||
duration_minutes: int | None = None
|
||||
win_reason: WinReason | None = None
|
||||
player_count: int
|
||||
overall_comment: str | None = None
|
||||
created_by: int
|
||||
participants: list[MatchListParticipant] = []
|
||||
|
||||
|
||||
class MatchList(BaseModel):
|
||||
items: list[MatchListItem] = []
|
||||
total: int
|
||||
limit: int
|
||||
offset: int
|
||||
|
||||
|
||||
class GroupBriefStats(OverallStats):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
|
||||
class HomeInProgressMatch(BaseModel):
|
||||
id: int
|
||||
group_id: int
|
||||
group_name: str
|
||||
started_at: str | None = None
|
||||
player_count: int
|
||||
participants: list[MatchListParticipant] = []
|
||||
|
||||
|
||||
class HomeResponse(BaseModel):
|
||||
leaderboard: list[LeaderboardEntry] = []
|
||||
provisional: list[LeaderboardEntry] = []
|
||||
profile: ProfileStats
|
||||
active_group: GroupBriefStats | None = None
|
||||
in_progress: list[HomeInProgressMatch] = []
|
||||
min_games: int
|
||||
|
||||
|
||||
# ─── Админ ───────────────────────────────────────────────────────────────────
|
||||
|
||||
class AdminLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class AdminMe(BaseModel):
|
||||
id: int
|
||||
nickname: str
|
||||
role: str
|
||||
|
||||
|
||||
class AdminUserRead(BaseModel):
|
||||
id: int
|
||||
nickname: str
|
||||
role: str
|
||||
is_active: bool
|
||||
auth_provider: str
|
||||
telegram_id: int | None = None
|
||||
created_at: str
|
||||
|
||||
|
||||
class AdminUserUpdate(BaseModel):
|
||||
nickname: str | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class AdminGroupRead(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
owner_id: int
|
||||
created_at: str
|
||||
|
||||
|
||||
class AdminMatchRead(BaseModel):
|
||||
id: int
|
||||
group_id: int
|
||||
group_name: str | None = None
|
||||
status: str
|
||||
played_at: str
|
||||
duration_minutes: int | None = None
|
||||
win_reason: WinReason | None = None
|
||||
player_count: int
|
||||
created_by: int
|
||||
created_at: str
|
||||
|
||||
|
||||
class AuditLogItem(BaseModel):
|
||||
id: int
|
||||
actor_id: int | None = None
|
||||
action: str
|
||||
entity_type: str
|
||||
entity_id: int | None = None
|
||||
payload: dict | None = None
|
||||
ip: str | None = None
|
||||
user_agent: str | None = None
|
||||
created_at: str
|
||||
|
||||
|
||||
class AuditLogList(BaseModel):
|
||||
items: list[AuditLogItem] = []
|
||||
limit: int
|
||||
offset: int
|
||||
Reference in New Issue
Block a user