Добавлен 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:
@@ -19,6 +19,7 @@ from app.models.coin_transaction import CoinTransaction, CoinTransactionType
|
||||
from app.models.consumable_usage import ConsumableUsage
|
||||
from app.models.promo_code import PromoCode, PromoCodeRedemption
|
||||
from app.models.widget_token import WidgetToken
|
||||
from app.models.exiled_game import ExiledGame
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
@@ -67,4 +68,5 @@ __all__ = [
|
||||
"PromoCode",
|
||||
"PromoCodeRedemption",
|
||||
"WidgetToken",
|
||||
"ExiledGame",
|
||||
]
|
||||
|
||||
@@ -20,6 +20,7 @@ class ActivityType(str, Enum):
|
||||
EVENT_END = "event_end"
|
||||
SWAP = "swap"
|
||||
GAME_CHOICE = "game_choice"
|
||||
MODERATION = "moderation"
|
||||
|
||||
|
||||
class Activity(Base):
|
||||
|
||||
37
backend/app/models/exiled_game.py
Normal file
37
backend/app/models/exiled_game.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Boolean, Integer, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class ExiledGame(Base):
|
||||
"""Изгнанные игры участника - не будут выпадать при спине"""
|
||||
__tablename__ = "exiled_games"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("participant_id", "game_id", name="unique_participant_game_exile"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
participant_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("participants.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
game_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("games.id", ondelete="CASCADE")
|
||||
)
|
||||
assignment_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("assignments.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
exiled_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
exiled_by: Mapped[str] = mapped_column(String(20)) # "user" | "organizer" | "admin"
|
||||
reason: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
|
||||
# Soft-delete для истории
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
|
||||
unexiled_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
unexiled_by: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
|
||||
# Relationships
|
||||
participant: Mapped["Participant"] = relationship("Participant")
|
||||
game: Mapped["Game"] = relationship("Game")
|
||||
assignment: Mapped["Assignment"] = relationship("Assignment")
|
||||
@@ -28,6 +28,7 @@ class ItemRarity(str, Enum):
|
||||
|
||||
class ConsumableType(str, Enum):
|
||||
SKIP = "skip"
|
||||
SKIP_EXILE = "skip_exile" # Скип с изгнанием игры из пула
|
||||
BOOST = "boost"
|
||||
WILD_CARD = "wild_card"
|
||||
LUCKY_DICE = "lucky_dice"
|
||||
|
||||
Reference in New Issue
Block a user