evg_ultimate_team / backend /app /models /challenge.py
clementpep's picture
chore: first code base version
453520f
"""
Challenge database model for EVG Ultimate Team.
Represents challenges that participants can complete to earn points.
"""
from sqlalchemy import Column, Integer, String, Enum, DateTime, ForeignKey, Table
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
import enum
# =============================================================================
# Enums
# =============================================================================
class ChallengeType(str, enum.Enum):
"""
Types of challenges available in the game.
- INDIVIDUAL: Challenge for individual participants
- TEAM: Challenge for teams (points shared among team members)
- SECRET: Secret challenge revealed at specific times
"""
INDIVIDUAL = "individual"
TEAM = "team"
SECRET = "secret"
class ChallengeStatus(str, enum.Enum):
"""
Status of a challenge.
- PENDING: Challenge available but not yet attempted
- ACTIVE: Challenge is currently being attempted
- COMPLETED: Challenge successfully completed and validated
- FAILED: Challenge attempted but failed
"""
PENDING = "pending"
ACTIVE = "active"
COMPLETED = "completed"
FAILED = "failed"
# =============================================================================
# Association Tables (Many-to-Many Relationships)
# =============================================================================
# Association table for participants assigned to challenges
challenge_assignments = Table(
"challenge_assignments",
Base.metadata,
Column(
"challenge_id",
Integer,
ForeignKey("challenges.id", ondelete="CASCADE"),
primary_key=True,
comment="Challenge ID"
),
Column(
"participant_id",
Integer,
ForeignKey("participants.id", ondelete="CASCADE"),
primary_key=True,
comment="Participant ID assigned to the challenge"
),
comment="Associates participants with challenges they're assigned to"
)
# Association table for participants who completed challenges
challenge_completions = Table(
"challenge_completions",
Base.metadata,
Column(
"challenge_id",
Integer,
ForeignKey("challenges.id", ondelete="CASCADE"),
primary_key=True,
comment="Challenge ID"
),
Column(
"participant_id",
Integer,
ForeignKey("participants.id", ondelete="CASCADE"),
primary_key=True,
comment="Participant ID who completed the challenge"
),
comment="Associates participants with challenges they've completed"
)
# =============================================================================
# Challenge Model
# =============================================================================
class Challenge(Base):
"""
Challenge model representing a task participants can complete for points.
Challenges can be individual, team-based, or secret.
Admins create challenges and validate completions.
Attributes:
id: Primary key, auto-incrementing
title: Short title of the challenge
description: Detailed description of what participants must do
type: Type of challenge (individual, team, secret)
points: Points awarded for completing the challenge
status: Current status (pending, active, completed, failed)
validated_by: ID of admin who validated the challenge completion
created_at: Timestamp when challenge was created
completed_at: Timestamp when challenge was completed (None if not completed)
updated_at: Timestamp when challenge was last updated
"""
__tablename__ = "challenges"
# Primary Key
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
# Basic Information
title = Column(
String(200),
nullable=False,
index=True,
comment="Short title of the challenge"
)
description = Column(
String(1000),
nullable=False,
comment="Detailed description of the challenge"
)
# Challenge Configuration
type = Column(
Enum(ChallengeType),
nullable=False,
default=ChallengeType.INDIVIDUAL,
index=True,
comment="Type of challenge (individual, team, secret)"
)
points = Column(
Integer,
nullable=False,
default=20,
comment="Points awarded for completing the challenge"
)
status = Column(
Enum(ChallengeStatus),
nullable=False,
default=ChallengeStatus.PENDING,
index=True,
comment="Current status of the challenge"
)
# Validation
validated_by = Column(
Integer,
nullable=True,
comment="ID of admin who validated the challenge completion"
)
# Timestamps
created_at = Column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
index=True,
comment="Timestamp when challenge was created"
)
completed_at = Column(
DateTime(timezone=True),
nullable=True,
comment="Timestamp when challenge was completed"
)
updated_at = Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
comment="Timestamp when challenge was last updated"
)
# ==========================================================================
# Relationships
# ==========================================================================
# Participants assigned to this challenge (many-to-many)
assigned_participants = relationship(
"Participant",
secondary=challenge_assignments,
backref="assigned_challenges",
lazy="dynamic"
)
# Participants who completed this challenge (many-to-many)
completed_by_participants = relationship(
"Participant",
secondary=challenge_completions,
backref="completed_challenges",
lazy="dynamic"
)
# ==========================================================================
# Methods
# ==========================================================================
def assign_to_participant(self, participant) -> None:
"""
Assign this challenge to a participant.
Args:
participant: Participant instance to assign the challenge to
"""
if participant not in self.assigned_participants:
self.assigned_participants.append(participant)
def mark_as_active(self) -> None:
"""
Mark challenge as active (being attempted).
Raises:
ValueError: If challenge is already completed or failed
"""
if self.status in [ChallengeStatus.COMPLETED, ChallengeStatus.FAILED]:
raise ValueError(f"Cannot activate a {self.status.value} challenge")
self.status = ChallengeStatus.ACTIVE
def mark_as_completed(self, participant, admin_id: int) -> None:
"""
Mark challenge as completed by a participant.
Args:
participant: Participant who completed the challenge
admin_id: ID of admin validating the completion
Raises:
ValueError: If challenge is already completed
"""
if self.status == ChallengeStatus.COMPLETED:
raise ValueError("Challenge is already completed")
self.status = ChallengeStatus.COMPLETED
self.completed_at = func.now()
self.validated_by = admin_id
# Add participant to completed_by list if not already there
if participant not in self.completed_by_participants:
self.completed_by_participants.append(participant)
def mark_as_failed(self) -> None:
"""
Mark challenge as failed.
Raises:
ValueError: If challenge is already completed
"""
if self.status == ChallengeStatus.COMPLETED:
raise ValueError("Cannot fail a completed challenge")
self.status = ChallengeStatus.FAILED
def get_assigned_participant_ids(self) -> list[int]:
"""
Get list of participant IDs assigned to this challenge.
Returns:
List of participant IDs
"""
return [p.id for p in self.assigned_participants.all()]
def get_completed_participant_ids(self) -> list[int]:
"""
Get list of participant IDs who completed this challenge.
Returns:
List of participant IDs
"""
return [p.id for p in self.completed_by_participants.all()]
def __repr__(self) -> str:
"""String representation of the challenge."""
return (
f"<Challenge(id={self.id}, title='{self.title}', "
f"type={self.type.value}, points={self.points}, status={self.status.value})>"
)
def __str__(self) -> str:
"""Human-readable string representation."""
return f"{self.title} ({self.type.value}) - {self.points} pts - {self.status.value}"