Files
game-marathon/backend/app/schemas/marathon.py
mamonov.ep f78eacb1a5 Добавлен Skip with Exile, модерация марафонов и выдача предметов
## Skip with Exile (новый расходник)
- Новая модель ExiledGame для хранения изгнанных игр
- Расходник skip_exile: пропуск без штрафа + игра исключается из пула навсегда
- Фильтрация изгнанных игр при выдаче заданий
- UI кнопка в PlayPage для использования skip_exile

## Модерация марафонов (для организаторов)
- Эндпоинты: skip-assignment, exiled-games, restore-exiled-game
- UI в LeaderboardPage: кнопка скипа у каждого участника
- Выбор типа скипа (обычный/с изгнанием) + причина
- Telegram уведомления о модерации

## Админская выдача предметов
- Эндпоинты: admin grant/remove items, get user inventory
- Новая страница AdminGrantItemPage (как магазин)
- Telegram уведомление при получении подарка

## Исправления миграций
- Миграции 029/030 теперь идемпотентны (проверка существования таблиц)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-10 23:02:37 +03:00

151 lines
3.8 KiB
Python

from datetime import datetime
from pydantic import BaseModel, Field
from app.schemas.user import UserPublic
class MarathonBase(BaseModel):
title: str = Field(..., min_length=1, max_length=100)
description: str | None = None
class MarathonCreate(MarathonBase):
start_date: datetime
duration_days: int = Field(default=30, ge=1, le=365)
is_public: bool = False
game_proposal_mode: str = Field(default="all_participants", pattern="^(all_participants|organizer_only)$")
# Shop/Consumables settings
allow_skips: bool = True
max_skips_per_participant: int | None = Field(None, ge=1, le=100)
allow_consumables: bool = True
class MarathonUpdate(BaseModel):
title: str | None = Field(None, min_length=1, max_length=100)
description: str | None = None
start_date: datetime | None = None
is_public: bool | None = None
game_proposal_mode: str | None = Field(None, pattern="^(all_participants|organizer_only)$")
auto_events_enabled: bool | None = None
# Shop/Consumables settings
allow_skips: bool | None = None
max_skips_per_participant: int | None = Field(None, ge=1, le=100)
allow_consumables: bool | None = None
class ParticipantInfo(BaseModel):
id: int
role: str = "participant"
total_points: int
current_streak: int
drop_count: int
joined_at: datetime
# Shop: coins and consumables status
coins_earned: int = 0
skips_used: int = 0
has_active_boost: bool = False
has_lucky_dice: bool = False
lucky_dice_multiplier: float | None = None
can_undo: bool = False
class Config:
from_attributes = True
class ParticipantWithUser(ParticipantInfo):
user: UserPublic
class MarathonResponse(MarathonBase):
id: int
creator: UserPublic
status: str
invite_code: str
is_public: bool
game_proposal_mode: str
auto_events_enabled: bool
cover_url: str | None
start_date: datetime | None
end_date: datetime | None
participants_count: int
games_count: int
created_at: datetime
my_participation: ParticipantInfo | None = None
# Certification
certification_status: str = "none"
is_certified: bool = False
# Shop/Consumables settings
allow_skips: bool = True
max_skips_per_participant: int | None = None
allow_consumables: bool = True
class Config:
from_attributes = True
class SetParticipantRole(BaseModel):
role: str = Field(..., pattern="^(participant|organizer)$")
class MarathonListItem(BaseModel):
id: int
title: str
status: str
is_public: bool
cover_url: str | None
participants_count: int
start_date: datetime | None
end_date: datetime | None
# Certification badge
is_certified: bool = False
class Config:
from_attributes = True
class JoinMarathon(BaseModel):
invite_code: str
class MarathonPublicInfo(BaseModel):
"""Public info about marathon for invite page (no auth required)"""
id: int
title: str
description: str | None
status: str
cover_url: str | None
participants_count: int
creator_nickname: str
class Config:
from_attributes = True
class LeaderboardEntry(BaseModel):
rank: int
user: UserPublic
total_points: int
current_streak: int
completed_count: int
dropped_count: int
# Moderation schemas
class OrganizerSkipRequest(BaseModel):
"""Request to skip a participant's assignment by organizer"""
exile: bool = False # If true, also exile the game from participant's pool
reason: str | None = None
class ExiledGameResponse(BaseModel):
"""Exiled game info"""
id: int
game_id: int
game_title: str
exiled_at: datetime
exiled_by: str # "user" | "organizer" | "admin"
reason: str | None
class Config:
from_attributes = True