Add 3 roles, settings for marathons

This commit is contained in:
2025-12-14 20:21:56 +07:00
parent bb9e9a6e1d
commit d0b8eca600
28 changed files with 1679 additions and 290 deletions

View File

@@ -2,8 +2,8 @@ from fastapi import APIRouter, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from app.api.deps import DbSession, CurrentUser
from app.models import Marathon, MarathonStatus, Game, Challenge, Participant
from app.api.deps import DbSession, CurrentUser, require_participant, require_organizer, get_participant
from app.models import Marathon, MarathonStatus, Game, GameStatus, Challenge
from app.schemas import (
ChallengeCreate,
ChallengeUpdate,
@@ -33,21 +33,9 @@ async def get_challenge_or_404(db, challenge_id: int) -> Challenge:
return challenge
async def check_participant(db, user_id: int, marathon_id: int) -> Participant:
result = await db.execute(
select(Participant).where(
Participant.user_id == user_id,
Participant.marathon_id == marathon_id,
)
)
participant = result.scalar_one_or_none()
if not participant:
raise HTTPException(status_code=403, detail="You are not a participant of this marathon")
return participant
@router.get("/games/{game_id}/challenges", response_model=list[ChallengeResponse])
async def list_challenges(game_id: int, current_user: CurrentUser, db: DbSession):
"""List challenges for a game. Participants can view challenges for approved games only."""
# Get game and check access
result = await db.execute(
select(Game).where(Game.id == game_id)
@@ -56,7 +44,16 @@ async def list_challenges(game_id: int, current_user: CurrentUser, db: DbSession
if not game:
raise HTTPException(status_code=404, detail="Game not found")
await check_participant(db, current_user.id, game.marathon_id)
participant = await get_participant(db, current_user.id, game.marathon_id)
# Check access
if not current_user.is_admin:
if not participant:
raise HTTPException(status_code=403, detail="You are not a participant of this marathon")
# Regular participants can only see challenges for approved games or their own games
if not participant.is_organizer:
if game.status != GameStatus.APPROVED.value and game.proposed_by_id != current_user.id:
raise HTTPException(status_code=403, detail="Game not accessible")
result = await db.execute(
select(Challenge)
@@ -91,6 +88,7 @@ async def create_challenge(
current_user: CurrentUser,
db: DbSession,
):
"""Create a challenge for a game. Organizers only."""
# Get game and check access
result = await db.execute(
select(Game).where(Game.id == game_id)
@@ -105,7 +103,12 @@ async def create_challenge(
if marathon.status != MarathonStatus.PREPARING.value:
raise HTTPException(status_code=400, detail="Cannot add challenges to active or finished marathon")
await check_participant(db, current_user.id, game.marathon_id)
# Only organizers can add challenges
await require_organizer(db, current_user, game.marathon_id)
# Can only add challenges to approved games
if game.status != GameStatus.APPROVED.value:
raise HTTPException(status_code=400, detail="Can only add challenges to approved games")
challenge = Challenge(
game_id=game_id,
@@ -141,7 +144,7 @@ async def create_challenge(
@router.post("/marathons/{marathon_id}/preview-challenges", response_model=ChallengesPreviewResponse)
async def preview_challenges(marathon_id: int, current_user: CurrentUser, db: DbSession):
"""Generate challenges preview for all games in marathon using GPT (without saving)"""
"""Generate challenges preview for approved games in marathon using GPT (without saving). Organizers only."""
# Check marathon
result = await db.execute(select(Marathon).where(Marathon.id == marathon_id))
marathon = result.scalar_one_or_none()
@@ -151,16 +154,20 @@ async def preview_challenges(marathon_id: int, current_user: CurrentUser, db: Db
if marathon.status != MarathonStatus.PREPARING.value:
raise HTTPException(status_code=400, detail="Cannot generate challenges for active or finished marathon")
await check_participant(db, current_user.id, marathon_id)
# Only organizers can generate challenges
await require_organizer(db, current_user, marathon_id)
# Get all games
# Get only APPROVED games
result = await db.execute(
select(Game).where(Game.marathon_id == marathon_id)
select(Game).where(
Game.marathon_id == marathon_id,
Game.status == GameStatus.APPROVED.value,
)
)
games = result.scalars().all()
if not games:
raise HTTPException(status_code=400, detail="No games in marathon")
raise HTTPException(status_code=400, detail="No approved games in marathon")
preview_challenges = []
for game in games:
@@ -202,7 +209,7 @@ async def save_challenges(
current_user: CurrentUser,
db: DbSession,
):
"""Save previewed challenges to database"""
"""Save previewed challenges to database. Organizers only."""
# Check marathon
result = await db.execute(select(Marathon).where(Marathon.id == marathon_id))
marathon = result.scalar_one_or_none()
@@ -212,18 +219,22 @@ async def save_challenges(
if marathon.status != MarathonStatus.PREPARING.value:
raise HTTPException(status_code=400, detail="Cannot add challenges to active or finished marathon")
await check_participant(db, current_user.id, marathon_id)
# Only organizers can save challenges
await require_organizer(db, current_user, marathon_id)
# Verify all games belong to this marathon
# Verify all games belong to this marathon AND are approved
result = await db.execute(
select(Game.id).where(Game.marathon_id == marathon_id)
select(Game.id).where(
Game.marathon_id == marathon_id,
Game.status == GameStatus.APPROVED.value,
)
)
valid_game_ids = set(row[0] for row in result.fetchall())
saved_count = 0
for ch_data in data.challenges:
if ch_data.game_id not in valid_game_ids:
continue # Skip challenges for invalid games
continue # Skip challenges for invalid/unapproved games
# Validate type
ch_type = ch_data.type
@@ -267,6 +278,7 @@ async def update_challenge(
current_user: CurrentUser,
db: DbSession,
):
"""Update a challenge. Organizers only."""
challenge = await get_challenge_or_404(db, challenge_id)
# Check marathon is in preparing state
@@ -275,7 +287,8 @@ async def update_challenge(
if marathon.status != MarathonStatus.PREPARING.value:
raise HTTPException(status_code=400, detail="Cannot update challenges in active or finished marathon")
await check_participant(db, current_user.id, challenge.game.marathon_id)
# Only organizers can update challenges
await require_organizer(db, current_user, challenge.game.marathon_id)
if data.title is not None:
challenge.title = data.title
@@ -316,6 +329,7 @@ async def update_challenge(
@router.delete("/challenges/{challenge_id}", response_model=MessageResponse)
async def delete_challenge(challenge_id: int, current_user: CurrentUser, db: DbSession):
"""Delete a challenge. Organizers only."""
challenge = await get_challenge_or_404(db, challenge_id)
# Check marathon is in preparing state
@@ -324,7 +338,8 @@ async def delete_challenge(challenge_id: int, current_user: CurrentUser, db: DbS
if marathon.status != MarathonStatus.PREPARING.value:
raise HTTPException(status_code=400, detail="Cannot delete challenges from active or finished marathon")
await check_participant(db, current_user.id, challenge.game.marathon_id)
# Only organizers can delete challenges
await require_organizer(db, current_user, challenge.game.marathon_id)
await db.delete(challenge)
await db.commit()