Common enemy rework
This commit is contained in:
@@ -10,12 +10,17 @@ from app.models import (
|
||||
Event, EventType, Activity, ActivityType, Assignment, AssignmentStatus, Challenge,
|
||||
SwapRequest as SwapRequestModel, SwapRequestStatus, User,
|
||||
)
|
||||
from fastapi import UploadFile, File, Form
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
from app.schemas import (
|
||||
EventCreate, EventResponse, ActiveEventResponse, EventEffects,
|
||||
MessageResponse, SwapRequest, ChallengeResponse, GameShort, SwapCandidate,
|
||||
SwapRequestCreate, SwapRequestResponse, SwapRequestChallengeInfo, MySwapRequests,
|
||||
CommonEnemyLeaderboard,
|
||||
CommonEnemyLeaderboard, EventAssignmentResponse, AssignmentResponse, CompleteResult,
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.schemas.event import EVENT_INFO, COMMON_ENEMY_BONUSES
|
||||
from app.schemas.user import UserPublic
|
||||
from app.services.events import event_service
|
||||
@@ -864,3 +869,259 @@ async def get_common_enemy_leaderboard(
|
||||
)
|
||||
|
||||
return leaderboard
|
||||
|
||||
|
||||
# ==================== Event Assignment Endpoints ====================
|
||||
|
||||
|
||||
def assignment_to_response(assignment: Assignment) -> AssignmentResponse:
|
||||
"""Convert Assignment model to AssignmentResponse"""
|
||||
challenge = assignment.challenge
|
||||
game = challenge.game
|
||||
return AssignmentResponse(
|
||||
id=assignment.id,
|
||||
challenge=ChallengeResponse(
|
||||
id=challenge.id,
|
||||
title=challenge.title,
|
||||
description=challenge.description,
|
||||
type=challenge.type,
|
||||
difficulty=challenge.difficulty,
|
||||
points=challenge.points,
|
||||
estimated_time=challenge.estimated_time,
|
||||
proof_type=challenge.proof_type,
|
||||
proof_hint=challenge.proof_hint,
|
||||
game=GameShort(
|
||||
id=game.id,
|
||||
title=game.title,
|
||||
cover_url=f"/uploads/covers/{game.cover_path.split('/')[-1]}" if game.cover_path else None,
|
||||
),
|
||||
is_generated=challenge.is_generated,
|
||||
created_at=challenge.created_at,
|
||||
),
|
||||
status=assignment.status,
|
||||
proof_url=f"/uploads/proofs/{assignment.proof_path.split('/')[-1]}" if assignment.proof_path else assignment.proof_url,
|
||||
proof_comment=assignment.proof_comment,
|
||||
points_earned=assignment.points_earned,
|
||||
streak_at_completion=assignment.streak_at_completion,
|
||||
started_at=assignment.started_at,
|
||||
completed_at=assignment.completed_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/marathons/{marathon_id}/event-assignment", response_model=EventAssignmentResponse)
|
||||
async def get_event_assignment(
|
||||
marathon_id: int,
|
||||
current_user: CurrentUser,
|
||||
db: DbSession,
|
||||
):
|
||||
"""Get current user's event assignment (Common Enemy)"""
|
||||
await get_marathon_or_404(db, marathon_id)
|
||||
participant = await require_participant(db, current_user.id, marathon_id)
|
||||
|
||||
# Get active common enemy event
|
||||
event = await event_service.get_active_event(db, marathon_id)
|
||||
|
||||
# Find event assignment for this participant
|
||||
result = await db.execute(
|
||||
select(Assignment)
|
||||
.options(
|
||||
selectinload(Assignment.challenge).selectinload(Challenge.game)
|
||||
)
|
||||
.where(
|
||||
Assignment.participant_id == participant.id,
|
||||
Assignment.is_event_assignment == True,
|
||||
)
|
||||
.order_by(Assignment.started_at.desc())
|
||||
)
|
||||
assignment = result.scalar_one_or_none()
|
||||
|
||||
# Check if completed
|
||||
is_completed = assignment.status == AssignmentStatus.COMPLETED.value if assignment else False
|
||||
|
||||
# If no active event but we have an assignment, it might be from a past event
|
||||
# Only return it if the event is still active
|
||||
if not event or event.type != EventType.COMMON_ENEMY.value:
|
||||
# Check if assignment belongs to an inactive event
|
||||
if assignment and assignment.event_id:
|
||||
result = await db.execute(
|
||||
select(Event).where(Event.id == assignment.event_id)
|
||||
)
|
||||
assignment_event = result.scalar_one_or_none()
|
||||
if assignment_event and not assignment_event.is_active:
|
||||
# Event ended, don't return the assignment
|
||||
return EventAssignmentResponse(
|
||||
assignment=None,
|
||||
event_id=None,
|
||||
challenge_id=None,
|
||||
is_completed=False,
|
||||
)
|
||||
|
||||
return EventAssignmentResponse(
|
||||
assignment=assignment_to_response(assignment) if assignment else None,
|
||||
event_id=event.id if event else None,
|
||||
challenge_id=event.data.get("challenge_id") if event and event.data else None,
|
||||
is_completed=is_completed,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/event-assignments/{assignment_id}/complete", response_model=CompleteResult)
|
||||
async def complete_event_assignment(
|
||||
assignment_id: int,
|
||||
current_user: CurrentUser,
|
||||
db: DbSession,
|
||||
proof_url: str | None = Form(None),
|
||||
comment: str | None = Form(None),
|
||||
proof_file: UploadFile | None = File(None),
|
||||
):
|
||||
"""Complete an event assignment (Common Enemy) with proof"""
|
||||
from app.services.points import PointsService
|
||||
points_service = PointsService()
|
||||
|
||||
# Get assignment
|
||||
result = await db.execute(
|
||||
select(Assignment)
|
||||
.options(
|
||||
selectinload(Assignment.participant),
|
||||
selectinload(Assignment.challenge).selectinload(Challenge.game),
|
||||
)
|
||||
.where(Assignment.id == assignment_id)
|
||||
)
|
||||
assignment = result.scalar_one_or_none()
|
||||
|
||||
if not assignment:
|
||||
raise HTTPException(status_code=404, detail="Assignment not found")
|
||||
|
||||
if assignment.participant.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="This is not your assignment")
|
||||
|
||||
if assignment.status != AssignmentStatus.ACTIVE.value:
|
||||
raise HTTPException(status_code=400, detail="Assignment is not active")
|
||||
|
||||
# Must be event assignment
|
||||
if not assignment.is_event_assignment:
|
||||
raise HTTPException(status_code=400, detail="This is not an event assignment")
|
||||
|
||||
# Need either file or URL
|
||||
if not proof_file and not proof_url:
|
||||
raise HTTPException(status_code=400, detail="Proof is required (file or URL)")
|
||||
|
||||
# Handle file upload
|
||||
if proof_file:
|
||||
contents = await proof_file.read()
|
||||
if len(contents) > settings.MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"File too large. Maximum size is {settings.MAX_UPLOAD_SIZE // 1024 // 1024} MB",
|
||||
)
|
||||
|
||||
ext = proof_file.filename.split(".")[-1].lower() if proof_file.filename else "jpg"
|
||||
if ext not in settings.ALLOWED_EXTENSIONS:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid file type. Allowed: {settings.ALLOWED_EXTENSIONS}",
|
||||
)
|
||||
|
||||
filename = f"{assignment_id}_{uuid.uuid4().hex}.{ext}"
|
||||
filepath = Path(settings.UPLOAD_DIR) / "proofs" / filename
|
||||
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(contents)
|
||||
|
||||
assignment.proof_path = str(filepath)
|
||||
else:
|
||||
assignment.proof_url = proof_url
|
||||
|
||||
assignment.proof_comment = comment
|
||||
|
||||
# Get marathon_id
|
||||
marathon_id = assignment.challenge.game.marathon_id
|
||||
|
||||
# Get active event for bonus calculation
|
||||
active_event = await event_service.get_active_event(db, marathon_id)
|
||||
|
||||
# Calculate base points (no streak bonus for event assignments)
|
||||
participant = assignment.participant
|
||||
challenge = assignment.challenge
|
||||
base_points = challenge.points
|
||||
|
||||
# Handle common enemy bonus
|
||||
common_enemy_bonus = 0
|
||||
common_enemy_closed = False
|
||||
common_enemy_winners = None
|
||||
|
||||
if active_event and active_event.type == EventType.COMMON_ENEMY.value:
|
||||
common_enemy_bonus, common_enemy_closed, common_enemy_winners = await event_service.record_common_enemy_completion(
|
||||
db, active_event, participant.id, current_user.id
|
||||
)
|
||||
|
||||
total_points = base_points + common_enemy_bonus
|
||||
|
||||
# Update assignment
|
||||
assignment.status = AssignmentStatus.COMPLETED.value
|
||||
assignment.points_earned = total_points
|
||||
assignment.completed_at = datetime.utcnow()
|
||||
|
||||
# Update participant points (event assignments add to total but don't affect streak)
|
||||
participant.total_points += total_points
|
||||
|
||||
# Log activity
|
||||
activity_data = {
|
||||
"game": challenge.game.title,
|
||||
"challenge": challenge.title,
|
||||
"difficulty": challenge.difficulty,
|
||||
"points": total_points,
|
||||
"event_type": EventType.COMMON_ENEMY.value,
|
||||
"is_event_assignment": True,
|
||||
}
|
||||
if common_enemy_bonus:
|
||||
activity_data["common_enemy_bonus"] = common_enemy_bonus
|
||||
|
||||
activity = Activity(
|
||||
marathon_id=marathon_id,
|
||||
user_id=current_user.id,
|
||||
type=ActivityType.COMPLETE.value,
|
||||
data=activity_data,
|
||||
)
|
||||
db.add(activity)
|
||||
|
||||
# If common enemy event auto-closed, log the event end with winners
|
||||
if common_enemy_closed and common_enemy_winners:
|
||||
# Load winner nicknames
|
||||
winner_user_ids = [w["user_id"] for w in common_enemy_winners]
|
||||
users_result = await db.execute(
|
||||
select(User).where(User.id.in_(winner_user_ids))
|
||||
)
|
||||
users_map = {u.id: u.nickname for u in users_result.scalars().all()}
|
||||
|
||||
winners_data = [
|
||||
{
|
||||
"user_id": w["user_id"],
|
||||
"nickname": users_map.get(w["user_id"], "Unknown"),
|
||||
"rank": w["rank"],
|
||||
"bonus_points": COMMON_ENEMY_BONUSES.get(w["rank"], 0),
|
||||
}
|
||||
for w in common_enemy_winners
|
||||
]
|
||||
|
||||
event_end_activity = Activity(
|
||||
marathon_id=marathon_id,
|
||||
user_id=current_user.id,
|
||||
type=ActivityType.EVENT_END.value,
|
||||
data={
|
||||
"event_type": EventType.COMMON_ENEMY.value,
|
||||
"event_name": EVENT_INFO.get(EventType.COMMON_ENEMY, {}).get("name", "Общий враг"),
|
||||
"auto_closed": True,
|
||||
"winners": winners_data,
|
||||
},
|
||||
)
|
||||
db.add(event_end_activity)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return CompleteResult(
|
||||
points_earned=total_points,
|
||||
streak_bonus=0, # Event assignments don't give streak bonus
|
||||
total_points=participant.total_points,
|
||||
new_streak=participant.current_streak, # Streak unchanged
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user