Добавлен 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

@@ -9,7 +9,8 @@ from app.api.deps import (
from app.core.config import settings
from app.models import (
Marathon, MarathonStatus, GameProposalMode, Game, GameStatus, GameType,
Challenge, Activity, ActivityType, Assignment, AssignmentStatus, Participant, User
Challenge, Activity, ActivityType, Assignment, AssignmentStatus, Participant, User,
ExiledGame
)
from app.schemas import GameCreate, GameUpdate, GameResponse, MessageResponse, UserPublic
from app.schemas.assignment import AvailableGamesCount
@@ -519,9 +520,23 @@ async def get_available_games_for_participant(
)
completed_challenge_ids = set(challenges_result.scalars().all())
# Получаем изгнанные игры (is_active=True означает что игра изгнана)
exiled_result = await db.execute(
select(ExiledGame.game_id)
.where(
ExiledGame.participant_id == participant.id,
ExiledGame.is_active == True,
)
)
exiled_game_ids = set(exiled_result.scalars().all())
# Фильтруем доступные игры
available_games = []
for game in games_with_content:
# Исключаем изгнанные игры
if game.id in exiled_game_ids:
continue
if game.game_type == GameType.PLAYTHROUGH.value:
# Исключаем если игра уже завершена/дропнута
if game.id not in finished_playthrough_game_ids: