Добавлен 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>
This commit is contained in:
2026-01-10 23:01:23 +03:00
parent cf0df928b1
commit f78eacb1a5
24 changed files with 2194 additions and 14 deletions

View File

@@ -23,6 +23,8 @@ from app.schemas.marathon import (
JoinMarathon,
LeaderboardEntry,
SetParticipantRole,
OrganizerSkipRequest,
ExiledGameResponse,
)
from app.schemas.game import (
GameCreate,
@@ -124,6 +126,7 @@ from app.schemas.shop import (
CertificationReviewRequest,
CertificationStatusResponse,
ConsumablesStatusResponse,
AdminGrantItemRequest,
)
from app.schemas.promo_code import (
PromoCodeCreate,
@@ -170,6 +173,8 @@ __all__ = [
"JoinMarathon",
"LeaderboardEntry",
"SetParticipantRole",
"OrganizerSkipRequest",
"ExiledGameResponse",
# Game
"GameCreate",
"GameUpdate",
@@ -262,6 +267,7 @@ __all__ = [
"CertificationReviewRequest",
"CertificationStatusResponse",
"ConsumablesStatusResponse",
"AdminGrantItemRequest",
# Promo
"PromoCodeCreate",
"PromoCodeUpdate",

View File

@@ -128,3 +128,23 @@ class LeaderboardEntry(BaseModel):
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

View File

@@ -192,6 +192,7 @@ class CertificationStatusResponse(BaseModel):
class ConsumablesStatusResponse(BaseModel):
"""Schema for participant's consumables status in a marathon"""
skips_available: int # From inventory
skip_exiles_available: int = 0 # From inventory (skip with exile)
skips_used: int # In this marathon
skips_remaining: int | None # Based on marathon limit
boosts_available: int # From inventory
@@ -204,3 +205,12 @@ class ConsumablesStatusResponse(BaseModel):
copycats_available: int # From inventory
undos_available: int # From inventory
can_undo: bool # Has drop data to undo
# === Admin Item Granting ===
class AdminGrantItemRequest(BaseModel):
"""Schema for admin granting item to user"""
item_id: int
quantity: int = Field(default=1, ge=1, le=100)
reason: str = Field(..., min_length=1, max_length=500)