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

@@ -608,6 +608,57 @@ class TelegramNotifier:
reply_markup=reply_markup
)
async def notify_assignment_skipped_by_moderator(
self,
db,
user,
marathon_title: str,
game_title: str,
exiled: bool,
reason: str | None,
moderator_nickname: str,
) -> bool:
"""Notify participant that their assignment was skipped by organizer"""
if not user.telegram_id or not user.notify_moderation:
return False
exile_text = "\n🚫 Игра исключена из вашего пула" if exiled else ""
reason_text = f"\n📝 Причина: {reason}" if reason else ""
message = (
f"⏭️ <b>Задание пропущено</b>\n\n"
f"Марафон: {marathon_title}\n"
f"Игра: {game_title}\n"
f"Организатор: {moderator_nickname}"
f"{exile_text}"
f"{reason_text}\n\n"
f"Вы можете крутить колесо заново."
)
return await self.send_message(user.telegram_id, message)
async def notify_item_granted(
self,
user,
item_name: str,
quantity: int,
reason: str,
admin_nickname: str,
) -> bool:
"""Notify user that they received an item from admin"""
if not user.telegram_id:
return False
message = (
f"🎁 <b>Вы получили подарок!</b>\n\n"
f"Предмет: {item_name}\n"
f"Количество: {quantity}\n"
f"От: {admin_nickname}\n"
f"Причина: {reason}"
)
return await self.send_message(user.telegram_id, message)
# Global instance
telegram_notifier = TelegramNotifier()