diff --git a/backend/app/api/v1/events.py b/backend/app/api/v1/events.py index 390d600..c962ed9 100644 --- a/backend/app/api/v1/events.py +++ b/backend/app/api/v1/events.py @@ -10,6 +10,7 @@ from app.models import ( Event, EventType, Activity, ActivityType, Assignment, AssignmentStatus, Challenge, Game, SwapRequest as SwapRequestModel, SwapRequestStatus, User, ) +from app.models.bonus_assignment import BonusAssignment from fastapi import UploadFile, File, Form from app.schemas import ( @@ -275,6 +276,26 @@ async def stop_event( return MessageResponse(message="Event stopped") +def build_assignment_info(assignment: Assignment) -> SwapRequestChallengeInfo: + """Build SwapRequestChallengeInfo from assignment (challenge or playthrough)""" + if assignment.is_playthrough: + return SwapRequestChallengeInfo( + is_playthrough=True, + playthrough_description=assignment.game.playthrough_description, + playthrough_points=assignment.game.playthrough_points, + game_title=assignment.game.title, + ) + else: + return SwapRequestChallengeInfo( + is_playthrough=False, + title=assignment.challenge.title, + description=assignment.challenge.description, + points=assignment.challenge.points, + difficulty=assignment.challenge.difficulty, + game_title=assignment.challenge.game.title, + ) + + def build_swap_request_response( swap_req: SwapRequestModel, ) -> SwapRequestResponse: @@ -298,20 +319,8 @@ def build_swap_request_response( role=swap_req.to_participant.user.role, created_at=swap_req.to_participant.user.created_at, ), - from_challenge=SwapRequestChallengeInfo( - title=swap_req.from_assignment.challenge.title, - description=swap_req.from_assignment.challenge.description, - points=swap_req.from_assignment.challenge.points, - difficulty=swap_req.from_assignment.challenge.difficulty, - game_title=swap_req.from_assignment.challenge.game.title, - ), - to_challenge=SwapRequestChallengeInfo( - title=swap_req.to_assignment.challenge.title, - description=swap_req.to_assignment.challenge.description, - points=swap_req.to_assignment.challenge.points, - difficulty=swap_req.to_assignment.challenge.difficulty, - game_title=swap_req.to_assignment.challenge.game.title, - ), + from_challenge=build_assignment_info(swap_req.from_assignment), + to_challenge=build_assignment_info(swap_req.to_assignment), created_at=swap_req.created_at, responded_at=swap_req.responded_at, ) @@ -349,11 +358,12 @@ async def create_swap_request( if target.id == participant.id: raise HTTPException(status_code=400, detail="Cannot swap with yourself") - # Get both active assignments + # Get both active assignments (with challenge.game or game for playthrough) result = await db.execute( select(Assignment) .options( - selectinload(Assignment.challenge).selectinload(Challenge.game) + selectinload(Assignment.challenge).selectinload(Challenge.game), + selectinload(Assignment.game), # For playthrough ) .where( Assignment.participant_id == participant.id, @@ -365,7 +375,8 @@ async def create_swap_request( result = await db.execute( select(Assignment) .options( - selectinload(Assignment.challenge).selectinload(Challenge.game) + selectinload(Assignment.challenge).selectinload(Challenge.game), + selectinload(Assignment.game), # For playthrough ) .where( Assignment.participant_id == target.id, @@ -417,7 +428,7 @@ async def create_swap_request( await db.commit() await db.refresh(swap_request) - # Load relationships for response + # Load relationships for response (including game for playthrough) result = await db.execute( select(SwapRequestModel) .options( @@ -426,9 +437,13 @@ async def create_swap_request( selectinload(SwapRequestModel.from_assignment) .selectinload(Assignment.challenge) .selectinload(Challenge.game), + selectinload(SwapRequestModel.from_assignment) + .selectinload(Assignment.game), selectinload(SwapRequestModel.to_assignment) .selectinload(Assignment.challenge) .selectinload(Challenge.game), + selectinload(SwapRequestModel.to_assignment) + .selectinload(Assignment.game), ) .where(SwapRequestModel.id == swap_request.id) ) @@ -461,9 +476,13 @@ async def get_my_swap_requests( selectinload(SwapRequestModel.from_assignment) .selectinload(Assignment.challenge) .selectinload(Challenge.game), + selectinload(SwapRequestModel.from_assignment) + .selectinload(Assignment.game), selectinload(SwapRequestModel.to_assignment) .selectinload(Assignment.challenge) .selectinload(Challenge.game), + selectinload(SwapRequestModel.to_assignment) + .selectinload(Assignment.game), ) .where( SwapRequestModel.event_id == event.id, @@ -553,10 +572,39 @@ async def accept_swap_request( await db.commit() raise HTTPException(status_code=400, detail="One or both assignments are no longer active") - # Perform the swap + # Perform the swap (swap challenge_id, game_id, and is_playthrough) from_challenge_id = from_assignment.challenge_id + from_game_id = from_assignment.game_id + from_is_playthrough = from_assignment.is_playthrough + from_assignment.challenge_id = to_assignment.challenge_id + from_assignment.game_id = to_assignment.game_id + from_assignment.is_playthrough = to_assignment.is_playthrough + to_assignment.challenge_id = from_challenge_id + to_assignment.game_id = from_game_id + to_assignment.is_playthrough = from_is_playthrough + + # Swap bonus assignments between the two assignments + from sqlalchemy import update as sql_update + + # Get bonus assignments for both + from_bonus_result = await db.execute( + select(BonusAssignment).where(BonusAssignment.main_assignment_id == from_assignment.id) + ) + from_bonus_assignments = from_bonus_result.scalars().all() + + to_bonus_result = await db.execute( + select(BonusAssignment).where(BonusAssignment.main_assignment_id == to_assignment.id) + ) + to_bonus_assignments = to_bonus_result.scalars().all() + + # Move bonus assignments: from -> to, to -> from + for bonus in from_bonus_assignments: + bonus.main_assignment_id = to_assignment.id + + for bonus in to_bonus_assignments: + bonus.main_assignment_id = from_assignment.id # Update request status swap_request.status = SwapRequestStatus.ACCEPTED.value @@ -865,8 +913,10 @@ async def get_swap_candidates( if not event or event.type != EventType.SWAP.value: raise HTTPException(status_code=400, detail="No active swap event") - # Get all participants except current user with active assignments from app.models import Game + candidates = [] + + # Get challenge-based assignments result = await db.execute( select(Participant, Assignment, Challenge, Game) .join(Assignment, Assignment.participant_id == Participant.id) @@ -877,12 +927,11 @@ async def get_swap_candidates( Participant.marathon_id == marathon_id, Participant.id != participant.id, Assignment.status == AssignmentStatus.ACTIVE.value, + Assignment.is_playthrough == False, ) ) - rows = result.all() - - return [ - SwapCandidate( + for p, assignment, challenge, game in result.all(): + candidates.append(SwapCandidate( participant_id=p.id, user=UserPublic( id=p.user.id, @@ -892,14 +941,45 @@ async def get_swap_candidates( role=p.user.role, created_at=p.user.created_at, ), + is_playthrough=False, challenge_title=challenge.title, challenge_description=challenge.description, challenge_points=challenge.points, challenge_difficulty=challenge.difficulty, game_title=game.title, + )) + + # Get playthrough-based assignments + result = await db.execute( + select(Participant, Assignment, Game) + .join(Assignment, Assignment.participant_id == Participant.id) + .join(Game, Assignment.game_id == Game.id) + .options(selectinload(Participant.user)) + .where( + Participant.marathon_id == marathon_id, + Participant.id != participant.id, + Assignment.status == AssignmentStatus.ACTIVE.value, + Assignment.is_playthrough == True, ) - for p, assignment, challenge, game in rows - ] + ) + for p, assignment, game in result.all(): + candidates.append(SwapCandidate( + participant_id=p.id, + user=UserPublic( + id=p.user.id, + login=p.user.login, + nickname=p.user.nickname, + avatar_url=None, + role=p.user.role, + created_at=p.user.created_at, + ), + is_playthrough=True, + playthrough_description=game.playthrough_description, + playthrough_points=game.playthrough_points, + game_title=game.title, + )) + + return candidates @router.get("/marathons/{marathon_id}/common-enemy-leaderboard", response_model=list[CommonEnemyLeaderboard]) diff --git a/backend/app/schemas/event.py b/backend/app/schemas/event.py index b0a1e87..d0c9e51 100644 --- a/backend/app/schemas/event.py +++ b/backend/app/schemas/event.py @@ -128,10 +128,16 @@ class SwapCandidate(BaseModel): """Participant available for assignment swap""" participant_id: int user: UserPublic - challenge_title: str - challenge_description: str - challenge_points: int - challenge_difficulty: str + is_playthrough: bool = False + # Challenge fields (used when is_playthrough=False) + challenge_title: str | None = None + challenge_description: str | None = None + challenge_points: int | None = None + challenge_difficulty: str | None = None + # Playthrough fields (used when is_playthrough=True) + playthrough_description: str | None = None + playthrough_points: int | None = None + # Common field game_title: str @@ -145,11 +151,17 @@ class SwapRequestCreate(BaseModel): class SwapRequestChallengeInfo(BaseModel): - """Challenge info for swap request display""" - title: str - description: str - points: int - difficulty: str + """Challenge or playthrough info for swap request display""" + is_playthrough: bool = False + # Challenge fields (used when is_playthrough=False) + title: str | None = None + description: str | None = None + points: int | None = None + difficulty: str | None = None + # Playthrough fields (used when is_playthrough=True) + playthrough_description: str | None = None + playthrough_points: int | None = None + # Common field game_title: str diff --git a/frontend/src/pages/PlayPage.tsx b/frontend/src/pages/PlayPage.tsx index 8b9addc..87012c7 100644 --- a/frontend/src/pages/PlayPage.tsx +++ b/frontend/src/pages/PlayPage.tsx @@ -1054,7 +1054,14 @@ export function PlayPage() {
Нет доступных заданий для копирования
) : (- Вы получите: {request.from_challenge.title} + Вы получите: {challengeTitle}
- {request.from_challenge.game_title} • {request.from_challenge.points} очков + {challengeDetails}
- Вы получите: {request.to_challenge.title} + Вы получите: {challengeTitle}
Ожидание подтверждения... @@ -1915,7 +1935,8 @@ export function PlayPage() {
- {candidate.challenge_title} + {displayTitle}
- {candidate.game_title} • {candidate.challenge_points} очков • {candidate.challenge_difficulty} + {displayDetails}