This commit is contained in:
2026-01-05 07:15:50 +07:00
parent 65b2512d8c
commit 6a7717a474
44 changed files with 5678 additions and 183 deletions

View File

@@ -7,7 +7,7 @@ from typing import Optional
from app.api.deps import DbSession, CurrentUser, require_admin_with_2fa
from app.models import (
User, UserRole, Marathon, MarathonStatus, Participant, Game, AdminLog, AdminActionType, StaticContent,
User, UserRole, Marathon, MarathonStatus, CertificationStatus, Participant, Game, AdminLog, AdminActionType, StaticContent,
Dispute, DisputeStatus, Assignment, AssignmentStatus, Challenge, BonusAssignment, BonusAssignmentStatus
)
from app.schemas import (
@@ -37,6 +37,8 @@ class AdminMarathonResponse(BaseModel):
start_date: str | None
end_date: str | None
created_at: str
certification_status: str = "none"
is_certified: bool = False
class Config:
from_attributes = True
@@ -219,7 +221,12 @@ async def list_marathons(
query = (
select(Marathon)
.options(selectinload(Marathon.creator))
.options(
selectinload(Marathon.creator).selectinload(User.equipped_frame),
selectinload(Marathon.creator).selectinload(User.equipped_title),
selectinload(Marathon.creator).selectinload(User.equipped_name_color),
selectinload(Marathon.creator).selectinload(User.equipped_background),
)
.order_by(Marathon.created_at.desc())
)
@@ -248,6 +255,8 @@ async def list_marathons(
start_date=marathon.start_date.isoformat() if marathon.start_date else None,
end_date=marathon.end_date.isoformat() if marathon.end_date else None,
created_at=marathon.created_at.isoformat(),
certification_status=marathon.certification_status,
is_certified=marathon.is_certified,
))
return response
@@ -1102,3 +1111,75 @@ async def resolve_dispute(
return MessageResponse(
message=f"Dispute resolved as {'valid' if data.is_valid else 'invalid'}"
)
# ============ Marathon Certification ============
@router.post("/marathons/{marathon_id}/certify", response_model=MessageResponse)
async def certify_marathon(
request: Request,
marathon_id: int,
current_user: CurrentUser,
db: DbSession,
):
"""Certify (verify) a marathon. Admin only."""
require_admin_with_2fa(current_user)
result = await db.execute(select(Marathon).where(Marathon.id == marathon_id))
marathon = result.scalar_one_or_none()
if not marathon:
raise HTTPException(status_code=404, detail="Marathon not found")
if marathon.certification_status == CertificationStatus.CERTIFIED.value:
raise HTTPException(status_code=400, detail="Marathon is already certified")
marathon.certification_status = CertificationStatus.CERTIFIED.value
marathon.certified_at = datetime.utcnow()
marathon.certified_by_id = current_user.id
marathon.certification_rejection_reason = None
await db.commit()
# Log action
await log_admin_action(
db, current_user.id, AdminActionType.MARATHON_CERTIFY.value,
"marathon", marathon_id,
{"title": marathon.title},
request.client.host if request.client else None
)
return MessageResponse(message="Marathon certified successfully")
@router.post("/marathons/{marathon_id}/revoke-certification", response_model=MessageResponse)
async def revoke_marathon_certification(
request: Request,
marathon_id: int,
current_user: CurrentUser,
db: DbSession,
):
"""Revoke certification from a marathon. Admin only."""
require_admin_with_2fa(current_user)
result = await db.execute(select(Marathon).where(Marathon.id == marathon_id))
marathon = result.scalar_one_or_none()
if not marathon:
raise HTTPException(status_code=404, detail="Marathon not found")
if marathon.certification_status != CertificationStatus.CERTIFIED.value:
raise HTTPException(status_code=400, detail="Marathon is not certified")
marathon.certification_status = CertificationStatus.NONE.value
marathon.certified_at = None
marathon.certified_by_id = None
await db.commit()
# Log action
await log_admin_action(
db, current_user.id, AdminActionType.MARATHON_REVOKE_CERTIFICATION.value,
"marathon", marathon_id,
{"title": marathon.title},
request.client.host if request.client else None
)
return MessageResponse(message="Marathon certification revoked")