Улучшение системы оспариваний и исправления
- Оспаривания теперь требуют решения админа после 24ч голосования - Можно повторно оспаривать после разрешённых споров - Исправлены бонусные очки при перепрохождении после оспаривания - Сброс серии при невалидном пруфе - Колесо показывает только доступные игры - Rate limiting только через backend (RATE_LIMIT_ENABLED)
This commit is contained in:
@@ -7,8 +7,12 @@ from app.api.deps import (
|
||||
require_participant, require_organizer, get_participant,
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.models import Marathon, MarathonStatus, GameProposalMode, Game, GameStatus, Challenge, Activity, ActivityType
|
||||
from app.models import (
|
||||
Marathon, MarathonStatus, GameProposalMode, Game, GameStatus, GameType,
|
||||
Challenge, Activity, ActivityType, Assignment, AssignmentStatus, Participant
|
||||
)
|
||||
from app.schemas import GameCreate, GameUpdate, GameResponse, MessageResponse, UserPublic
|
||||
from app.schemas.assignment import AvailableGamesCount
|
||||
from app.services.storage import storage_service
|
||||
from app.services.telegram_notifier import telegram_notifier
|
||||
|
||||
@@ -43,6 +47,12 @@ def game_to_response(game: Game, challenges_count: int = 0) -> GameResponse:
|
||||
approved_by=UserPublic.model_validate(game.approved_by) if game.approved_by else None,
|
||||
challenges_count=challenges_count,
|
||||
created_at=game.created_at,
|
||||
# Поля для типа игры
|
||||
game_type=game.game_type,
|
||||
playthrough_points=game.playthrough_points,
|
||||
playthrough_description=game.playthrough_description,
|
||||
playthrough_proof_type=game.playthrough_proof_type,
|
||||
playthrough_proof_hint=game.playthrough_proof_hint,
|
||||
)
|
||||
|
||||
|
||||
@@ -145,6 +155,12 @@ async def add_game(
|
||||
proposed_by_id=current_user.id,
|
||||
status=game_status,
|
||||
approved_by_id=current_user.id if is_organizer else None,
|
||||
# Поля для типа игры
|
||||
game_type=data.game_type.value,
|
||||
playthrough_points=data.playthrough_points,
|
||||
playthrough_description=data.playthrough_description,
|
||||
playthrough_proof_type=data.playthrough_proof_type.value if data.playthrough_proof_type else None,
|
||||
playthrough_proof_hint=data.playthrough_proof_hint,
|
||||
)
|
||||
db.add(game)
|
||||
|
||||
@@ -171,6 +187,12 @@ async def add_game(
|
||||
approved_by=UserPublic.model_validate(current_user) if is_organizer else None,
|
||||
challenges_count=0,
|
||||
created_at=game.created_at,
|
||||
# Поля для типа игры
|
||||
game_type=game.game_type,
|
||||
playthrough_points=game.playthrough_points,
|
||||
playthrough_description=game.playthrough_description,
|
||||
playthrough_proof_type=game.playthrough_proof_type,
|
||||
playthrough_proof_hint=game.playthrough_proof_hint,
|
||||
)
|
||||
|
||||
|
||||
@@ -227,6 +249,18 @@ async def update_game(
|
||||
if data.genre is not None:
|
||||
game.genre = data.genre
|
||||
|
||||
# Поля для типа игры
|
||||
if data.game_type is not None:
|
||||
game.game_type = data.game_type.value
|
||||
if data.playthrough_points is not None:
|
||||
game.playthrough_points = data.playthrough_points
|
||||
if data.playthrough_description is not None:
|
||||
game.playthrough_description = data.playthrough_description
|
||||
if data.playthrough_proof_type is not None:
|
||||
game.playthrough_proof_type = data.playthrough_proof_type.value
|
||||
if data.playthrough_proof_hint is not None:
|
||||
game.playthrough_proof_hint = data.playthrough_proof_hint
|
||||
|
||||
await db.commit()
|
||||
|
||||
return await get_game(game_id, current_user, db)
|
||||
@@ -398,3 +432,159 @@ async def upload_cover(
|
||||
await db.commit()
|
||||
|
||||
return await get_game(game_id, current_user, db)
|
||||
|
||||
|
||||
async def get_available_games_for_participant(
|
||||
db, participant: Participant, marathon_id: int
|
||||
) -> tuple[list[Game], int]:
|
||||
"""
|
||||
Получить список игр, доступных для спина участника.
|
||||
|
||||
Возвращает кортеж (доступные игры, всего игр).
|
||||
|
||||
Логика исключения:
|
||||
- playthrough: игра исключается если участник завершил ИЛИ дропнул прохождение
|
||||
- challenges: игра исключается если участник выполнил ВСЕ челленджи
|
||||
"""
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
# Получаем все одобренные игры с челленджами
|
||||
result = await db.execute(
|
||||
select(Game)
|
||||
.options(selectinload(Game.challenges))
|
||||
.where(
|
||||
Game.marathon_id == marathon_id,
|
||||
Game.status == GameStatus.APPROVED.value
|
||||
)
|
||||
)
|
||||
all_games = list(result.scalars().all())
|
||||
|
||||
# Фильтруем игры с челленджами (для типа challenges)
|
||||
# или игры с заполненными playthrough полями (для типа playthrough)
|
||||
games_with_content = []
|
||||
for game in all_games:
|
||||
if game.game_type == GameType.PLAYTHROUGH.value:
|
||||
# Для playthrough не нужны челленджи
|
||||
if game.playthrough_points and game.playthrough_description:
|
||||
games_with_content.append(game)
|
||||
else:
|
||||
# Для challenges нужны челленджи
|
||||
if game.challenges:
|
||||
games_with_content.append(game)
|
||||
|
||||
total_games = len(games_with_content)
|
||||
if total_games == 0:
|
||||
return [], 0
|
||||
|
||||
# Получаем завершённые/дропнутые assignments участника
|
||||
finished_statuses = [AssignmentStatus.COMPLETED.value, AssignmentStatus.DROPPED.value]
|
||||
|
||||
# Для playthrough: получаем game_id завершённых/дропнутых прохождений
|
||||
playthrough_result = await db.execute(
|
||||
select(Assignment.game_id)
|
||||
.where(
|
||||
Assignment.participant_id == participant.id,
|
||||
Assignment.is_playthrough == True,
|
||||
Assignment.status.in_(finished_statuses)
|
||||
)
|
||||
)
|
||||
finished_playthrough_game_ids = set(playthrough_result.scalars().all())
|
||||
|
||||
# Для challenges: получаем challenge_id завершённых заданий
|
||||
challenges_result = await db.execute(
|
||||
select(Assignment.challenge_id)
|
||||
.where(
|
||||
Assignment.participant_id == participant.id,
|
||||
Assignment.is_playthrough == False,
|
||||
Assignment.status == AssignmentStatus.COMPLETED.value
|
||||
)
|
||||
)
|
||||
completed_challenge_ids = set(challenges_result.scalars().all())
|
||||
|
||||
# Фильтруем доступные игры
|
||||
available_games = []
|
||||
for game in games_with_content:
|
||||
if game.game_type == GameType.PLAYTHROUGH.value:
|
||||
# Исключаем если игра уже завершена/дропнута
|
||||
if game.id not in finished_playthrough_game_ids:
|
||||
available_games.append(game)
|
||||
else:
|
||||
# Для challenges: исключаем если все челленджи выполнены
|
||||
game_challenge_ids = {c.id for c in game.challenges}
|
||||
if not game_challenge_ids.issubset(completed_challenge_ids):
|
||||
available_games.append(game)
|
||||
|
||||
return available_games, total_games
|
||||
|
||||
|
||||
@router.get("/marathons/{marathon_id}/available-games-count", response_model=AvailableGamesCount)
|
||||
async def get_available_games_count(
|
||||
marathon_id: int,
|
||||
current_user: CurrentUser,
|
||||
db: DbSession,
|
||||
):
|
||||
"""
|
||||
Получить количество игр, доступных для спина.
|
||||
|
||||
Возвращает { available: X, total: Y } где:
|
||||
- available: количество игр, которые могут выпасть
|
||||
- total: общее количество игр в марафоне
|
||||
"""
|
||||
participant = await get_participant(db, current_user.id, marathon_id)
|
||||
if not participant:
|
||||
raise HTTPException(status_code=403, detail="You are not a participant of this marathon")
|
||||
|
||||
available_games, total_games = await get_available_games_for_participant(
|
||||
db, participant, marathon_id
|
||||
)
|
||||
|
||||
return AvailableGamesCount(
|
||||
available=len(available_games),
|
||||
total=total_games
|
||||
)
|
||||
|
||||
|
||||
@router.get("/marathons/{marathon_id}/available-games", response_model=list[GameResponse])
|
||||
async def get_available_games(
|
||||
marathon_id: int,
|
||||
current_user: CurrentUser,
|
||||
db: DbSession,
|
||||
):
|
||||
"""
|
||||
Получить список игр, доступных для спина.
|
||||
|
||||
Возвращает только те игры, которые могут выпасть участнику:
|
||||
- Для playthrough: исключаются игры которые уже завершены/дропнуты
|
||||
- Для challenges: исключаются игры где все челленджи выполнены
|
||||
"""
|
||||
participant = await get_participant(db, current_user.id, marathon_id)
|
||||
if not participant:
|
||||
raise HTTPException(status_code=403, detail="You are not a participant of this marathon")
|
||||
|
||||
available_games, _ = await get_available_games_for_participant(
|
||||
db, participant, marathon_id
|
||||
)
|
||||
|
||||
# Convert to response with challenges count
|
||||
result = []
|
||||
for game in available_games:
|
||||
challenges_count = len(game.challenges) if game.challenges else 0
|
||||
result.append(GameResponse(
|
||||
id=game.id,
|
||||
title=game.title,
|
||||
cover_url=storage_service.get_url(game.cover_path, "covers"),
|
||||
download_url=game.download_url,
|
||||
genre=game.genre,
|
||||
status=game.status,
|
||||
proposed_by=None,
|
||||
approved_by=None,
|
||||
challenges_count=challenges_count,
|
||||
created_at=game.created_at,
|
||||
game_type=game.game_type,
|
||||
playthrough_points=game.playthrough_points,
|
||||
playthrough_description=game.playthrough_description,
|
||||
playthrough_proof_type=game.playthrough_proof_type,
|
||||
playthrough_proof_hint=game.playthrough_proof_hint,
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user