Files
team-tryouts/models.py
T

668 lines
27 KiB
Python

"""Database models for the Team Tryouts application.
This module defines all SQLAlchemy models including User, Tryout, Evaluation,
Team, Match, and Contract entities with their relationships and helper methods.
"""
from extensions import db, login_manager
from flask_login import UserMixin
from datetime import datetime
from urllib.parse import quote
# Available user roles in the system
ROLES = ['president', 'manager', 'coach', 'player', 'scout']
# Popular E-Sports games list for player profiles
ESPORT_GAMES = [
'Valorant',
'League of Legends',
'Counter-Strike 2',
'Apex Legends',
'Overwatch 2',
'Rainbow Six Siege',
'Rocket League',
'Super Smash Bros.'
]
# Game-specific positions for tryouts
GAME_POSITIONS = {
'League of Legends': ['Top Lane', 'Jungle', 'Mid Lane', 'ADC', 'Support'],
'Valorant': ['Controller', 'Initiator', 'Duelist', 'Sentinel'],
'Counter-Strike 2': ['AWPer', 'Entry Fragger', 'Lurker', 'In-Game Leader', 'Support'],
'Rainbow Six Siege': ['Entry', 'Support', 'Breacher', 'Anchor', 'Flex'],
'Overwatch 2': ['Tank', 'Damage', 'Support'],
'Apex Legends': [],
'Rocket League': [],
'Super Smash Bros.': []
}
@login_manager.user_loader
def load_user(user_id):
"""Load a user by ID for Flask-Login session management.
Args:
user_id (int): The user's unique identifier.
Returns:
User: The User object if found, None otherwise.
"""
return User.query.get(int(user_id))
class User(UserMixin, db.Model):
"""User model representing all users in the system.
Users can have different roles: president, manager, coach, player, or scout.
Each user has E-Sports specific fields for competitive gaming profiles.
Attributes:
id: Unique identifier for the user.
username: Unique username for login.
password_hash: Hashed password for authentication.
role: User role determining permissions.
full_name: User's display name.
email: User's email address.
phone: Optional phone number.
team_id: Foreign key to the user's organization team.
is_active_account: Whether the account is active.
games: Comma-separated list of games the user plays.
discord_username: User's Discord handle.
league_os_profile: Link to League OS profile.
"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
role = db.Column(db.String(20), nullable=False, default='player')
full_name = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
phone = db.Column(db.String(20), nullable=True)
team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
is_active_account = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# E-Sports specific fields
games = db.Column(db.Text, nullable=True) # Comma-separated list of games
discord_username = db.Column(db.String(128), nullable=True) # Discord handle
league_os_profile = db.Column(db.String(256), nullable=True) # League OS profile URL or ID
evaluations_given = db.relationship('Evaluation', foreign_keys='Evaluation.evaluator_id', backref='evaluator', lazy='dynamic')
evaluations_received = db.relationship('Evaluation', foreign_keys='Evaluation.player_id', backref='player', lazy='dynamic')
tryout_registrations = db.relationship('TryoutRegistration', backref='player', lazy='dynamic')
team_assignments = db.relationship('TeamMember', foreign_keys='TeamMember.player_id', backref='player_ref', lazy='dynamic')
def get_games_list(self):
"""Return the user's games as a list.
Returns:
list: List of game names the user plays, or empty list if none.
"""
if self.games:
return [g.strip() for g in self.games.split(',') if g.strip()]
return []
def has_role(self, *roles):
"""Check if the user has one of the specified roles.
Args:
*roles: Variable number of role names to check.
Returns:
bool: True if user has any of the specified roles.
"""
return self.role in roles
def can_evaluate(self):
"""Check if user can evaluate other players.
Returns:
bool: True if user is president, manager, or coach.
"""
return self.role in ['president', 'manager', 'coach']
def can_manage_users(self):
"""Check if user can manage (create/delete) other users.
Returns:
bool: True if user is president.
"""
return self.role == 'president'
def can_manage_tryouts(self):
"""Check if user can manage tryouts.
Returns:
bool: True if user is president, manager or coach.
"""
return self.role in ['president', 'manager', 'coach']
def can_manage_teams(self):
"""Check if user can manage organization teams.
Returns:
bool: True if user is president or manager.
"""
return self.role in ['president', 'manager']
def can_schedule_matches(self):
"""Check if user can schedule matches.
Returns:
bool: True if user is president, manager, or coach.
"""
return self.role in ['president', 'manager', 'coach']
def can_manage_this_tryout(self, tryout):
"""Check if user can manage a specific tryout.
Presidents can manage all tryouts. Managers can manage their own tryouts.
Coaches can manage tryouts targeting their coached team.
Args:
tryout: The Tryout object to check permissions for.
Returns:
bool: True if user has permission to manage the tryout.
"""
if self.role == 'president':
return True
if self.role == 'manager' and tryout.created_by == self.id:
return True
if self.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=self.id).first()
if org_team and tryout.target_org_team_id == org_team.id:
return True
return False
def can_manage_this_org_team(self, org_team):
"""Check if user can manage a specific org team.
Presidents can manage all org teams. Managers can manage all org teams.
Coaches can only manage their own coached team.
Args:
org_team: The OrgTeam object to check permissions for.
Returns:
bool: True if user has permission to manage the org team.
"""
if self.role == 'president':
return True
if self.role == 'manager':
return True # Managers can manage all org teams (create/edit/delete)
if self.role == 'coach' and org_team.coach_id == self.id:
return True
return False
def get_gamertags(self):
"""Return gamertags as a dictionary keyed by game.
Returns:
dict: Dictionary with game names as keys and gamertag info as values.
"""
return {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in self.gamertags}
# Platform options for games that require platform specification
GAME_PLATFORMS = {
'Valorant': [], # No platform needed
'League of Legends': [],
'Counter-Strike 2': [],
'Apex Legends': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'],
'Overwatch 2': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'],
'Rainbow Six Siege': ['Ubisoft', 'PlayStation', 'Xbox'], # Ubisoft = Uplay/Steam
'Rocket League': ['Epic', 'PlayStation', 'Xbox', 'Nintendo Switch'], # Epic uses EpicID, others use username
'Super Smash Bros.': ['Nintendo Switch'],
}
# Platform code mapping for TRN URLs (platform -> TRN code)
PLATFORM_CODES = {
'Ubisoft': 'ubi',
'PlayStation': 'psn',
'Xbox': 'xbl',
'Nintendo Switch': 'switch',
'PC': 'pc',
'Steam': 'steam',
'Epic': 'epic',
}
# TRN URL mapping for each game
TRN_URLS = {
'Valorant': 'https://tracker.gg/valorant/profile/riot/{username}',
'League of Legends': 'https://tracker.gg/lol/profile/{username}',
'Counter-Strike 2': 'https://tracker.gg/cs2/profile/steam/{username}',
'Apex Legends': 'https://tracker.gg/apex/profile/{platform}/{username}',
'Overwatch 2': 'https://tracker.gg/overwatch/profile/{platform}/{username}',
'Rainbow Six Siege': 'https://r6.tracker.network/r6siege/profile/{platform_code}/{username}',
'Rocket League': 'https://rocketleague.tracker.network/rocket-league/profile/{platform_code}/{username}',
'Super Smash Bros.': 'https://tracker.gg/smash/profile/{username}',
}
class UserGamertag(db.Model):
"""Store gamertag per game for each user.
Allows users to link their gaming profiles for different games with optional
platform specification for cross-platform games.
Attributes:
id: Unique identifier.
user_id: Foreign key to the user.
game: Name of the game.
gamertag: Player's gamertag/username for the game.
platform: Platform for cross-platform games (optional).
"""
__tablename__ = 'user_gamertags'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
game = db.Column(db.String(50), nullable=False)
gamertag = db.Column(db.String(120), nullable=False)
platform = db.Column(db.String(30), nullable=True) # For games that need platform (e.g., PSN, Xbox Live)
user = db.relationship('User', backref='gamertags')
__table_args__ = (
db.UniqueConstraint('user_id', 'game', name='unique_user_game'),
)
def get_trn_url(self):
"""Generate the TRN (Tracker Network) URL for this gamertag.
Builds the appropriate URL based on the game, handling platform codes
and URL encoding for special characters.
Returns:
str: The Tracker Network URL for the gamertag, or None if game not supported.
"""
if self.game not in TRN_URLS:
return None
url = TRN_URLS[self.game]
# URL-encode the gamertag to handle special characters like #, spaces, etc.
encoded_gamertag = quote(self.gamertag, safe='')
# Check for platform_code placeholder (used for R6 and Rocket League)
if '{platform_code}' in url and '{username}' in url:
platform_code = PLATFORM_CODES.get(self.platform, self.platform.lower().replace(' ', '-')) if self.platform else ''
return url.format(platform_code=platform_code, username=encoded_gamertag)
elif '{platform}' in url and '{username}' in url:
return url.format(platform=self.platform.lower().replace(' ', '-'), username=encoded_gamertag)
elif '{username}' in url:
return url.format(username=encoded_gamertag)
return url
class OrgTeam(db.Model):
"""Persistent organization teams (e.g., Varsity, JV) that exist across tryouts.
These teams are long-term organizational structures that persist beyond
individual tryouts, unlike tryout-specific Team entities.
Attributes:
id: Unique identifier.
name: Team name (e.g., "Varsity", "Junior Varsity").
coach_id: Foreign key to the assigned coach.
created_by: Foreign key to the user who created the team.
created_at: Timestamp of team creation.
"""
__tablename__ = 'org_teams'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
coach = db.relationship('User', foreign_keys=[coach_id], backref='coached_org_team', uselist=False)
creator = db.relationship('User', foreign_keys=[created_by])
players = db.relationship('User', foreign_keys='User.team_id', backref='org_team', lazy='dynamic')
class Tryout(db.Model):
"""Tryout event for player evaluations and team formation.
Represents a scheduled tryout session where players can register
and be evaluated by coaches/managers.
Attributes:
id: Unique identifier.
title: Tryout title/name.
description: Optional description of the tryout.
game: The game this tryout is for.
date: Date of the tryout.
location: Physical or virtual location.
status: Current status (upcoming, in_progress, completed).
max_players: Maximum number of players allowed.
created_by: Foreign key to the creating manager/president.
target_org_team_id: Foreign key to target organization team.
created_at: Timestamp of creation.
"""
__tablename__ = 'tryouts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text, nullable=True)
game = db.Column(db.String(50), nullable=False)
date = db.Column(db.Date, nullable=False)
location = db.Column(db.String(200), nullable=True)
status = db.Column(db.String(20), default='upcoming') # upcoming, in_progress, completed
max_players = db.Column(db.Integer, nullable=True)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
target_org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
creator = db.relationship('User', backref='created_tryouts')
registrations = db.relationship('TryoutRegistration', backref='tryout', lazy='dynamic')
evaluations = db.relationship('Evaluation', backref='tryout', lazy='dynamic')
teams = db.relationship('Team', backref='tryout', lazy='dynamic')
target_org_team = db.relationship('OrgTeam', backref='tryouts', foreign_keys=[target_org_team_id])
class TryoutRegistration(db.Model):
"""Registration linking a player to a tryout.
Tracks which players have registered for which tryouts and their
attendance status.
Attributes:
id: Unique identifier.
tryout_id: Foreign key to the tryout.
player_id: Foreign key to the registered player.
registered_at: Timestamp of registration.
status: Registration status (registered, attended, no_show).
notes: Optional notes about the registration.
"""
__tablename__ = 'tryout_registrations'
id = db.Column(db.Integer, primary_key=True)
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
registered_at = db.Column(db.DateTime, default=datetime.utcnow)
status = db.Column(db.String(20), default='registered') # registered, attended, no_show
notes = db.Column(db.Text, nullable=True)
class Evaluation(db.Model):
"""Player evaluation record.
Contains scored evaluations from coaches/managers for players
during tryouts.
Attributes:
id: Unique identifier.
tryout_id: Foreign key to the tryout.
player_id: Foreign key to the evaluated player.
evaluator_id: Foreign key to the evaluating coach/manager.
speed_score: Speed rating (1-10).
agility_score: Agility rating (1-10).
technique_score: Technique rating (1-10).
teamwork_score: Teamwork rating (1-10).
attitude_score: Attitude rating (1-10).
overall_score: Average of all scores.
comments: Optional evaluator comments.
position_recommendation: Recommended position.
created_at: Timestamp of creation.
updated_at: Timestamp of last update.
"""
__tablename__ = 'evaluations'
id = db.Column(db.Integer, primary_key=True)
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
evaluator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
mecanics_score = db.Column(db.Integer, nullable=True)
cohesion_score = db.Column(db.Integer, nullable=True)
communication_score = db.Column(db.Integer, nullable=True)
gamesense_score = db.Column(db.Integer, nullable=True)
versatility_score = db.Column(db.Integer, nullable=True)
discipline_score = db.Column(db.Integer, nullable=True)
analysis_score = db.Column(db.Integer, nullable=True)
sport_ethics_score = db.Column(db.Integer, nullable=True)
mental_score = db.Column(db.Integer, nullable=True)
overall_score = db.Column(db.Float, nullable=True)
comments = db.Column(db.Text, nullable=True)
position_recommendation = db.Column(db.String(50), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
__table_args__ = (
db.UniqueConstraint('tryout_id', 'player_id', 'evaluator_id', name='unique_evaluation'),
)
class Team(db.Model):
"""Tryout-specific team (e.g., Alpha, Bravo within a single tryout).
Teams created during a tryout for match scheduling purposes.
Different from OrgTeam which is a long-term organizational entity.
Attributes:
id: Unique identifier.
tryout_id: Foreign key to the parent tryout.
name: Team name.
created_by: Foreign key to the creator.
created_at: Timestamp of creation.
"""
__tablename__ = 'teams'
id = db.Column(db.Integer, primary_key=True)
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
name = db.Column(db.String(100), nullable=False)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
creator = db.relationship('User', backref='created_teams')
members = db.relationship('TeamMember', backref='team', lazy='dynamic')
class TeamMember(db.Model):
"""Link between a player and a tryout-specific team.
Attributes:
id: Unique identifier.
team_id: Foreign key to the team.
player_id: Foreign key to the player.
position: Player's position on the team.
added_at: Timestamp when player was added.
"""
__tablename__ = 'team_members'
id = db.Column(db.Integer, primary_key=True)
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
position = db.Column(db.String(50), nullable=True)
added_at = db.Column(db.DateTime, default=datetime.utcnow)
player = db.relationship('User', overlaps="player_ref,team_assignments") # Many-to-one, no dynamic loader
class Match(db.Model):
"""Match/scrimmage scheduled within a tryout.
Can be team vs team or player scrim type.
Attributes:
id: Unique identifier.
tryout_id: Foreign key to the parent tryout.
title: Match title/name.
description: Optional description.
date: Match date.
start_time: Match start time.
end_time: Match end time.
location: Match location.
status: Match status (scheduled, completed, cancelled).
match_type: Type of match (team_vs_team, player_vs_player, player_scrim).
created_by: Foreign key to the creator.
created_at: Timestamp of creation.
team1_id: Foreign key to first team (for team_vs_team).
team2_id: Foreign key to second team (for team_vs_team).
"""
__tablename__ = 'matches'
id = db.Column(db.Integer, primary_key=True)
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text, nullable=True)
date = db.Column(db.Date, nullable=False)
start_time = db.Column(db.Time, nullable=True)
end_time = db.Column(db.Time, nullable=True)
location = db.Column(db.String(200), nullable=True)
status = db.Column(db.String(20), default='scheduled') # scheduled, completed, cancelled
match_type = db.Column(db.String(20), nullable=False) # team_vs_team, player_scrim
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# For team vs team matches
team1_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
team2_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
creator = db.relationship('User', backref='created_matches')
tryout = db.relationship('Tryout', backref='matches')
team1 = db.relationship('Team', foreign_keys=[team1_id], backref='matches_as_team1')
team2 = db.relationship('Team', foreign_keys=[team2_id], backref='matches_as_team2')
participants = db.relationship('MatchParticipant', backref='match', lazy='dynamic')
def get_participating_players(self):
"""Return list of player IDs participating in this match.
Returns:
list: List of unique player IDs.
"""
return [p.player_id for p in self.participants.all()]
class PlayerDisponibility(db.Model):
"""Player availability in 30-minute time blocks for scheduling matches.
Allows players to specify when they're available for matches.
Attributes:
id: Unique identifier.
player_id: Foreign key to the player.
day_of_week: Day of week (0=Monday, 6=Sunday).
start_time: Start time of availability block.
end_time: End time of availability block (always 30 min after start).
created_at: Timestamp of creation.
updated_at: Timestamp of last update.
"""
__tablename__ = 'player_disponibilities'
id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
day_of_week = db.Column(db.Integer, nullable=False) # 0=Monday, 6=Sunday
start_time = db.Column(db.Time, nullable=False)
end_time = db.Column(db.Time, nullable=False) # Always 30 minutes after start_time
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
player = db.relationship('User', backref='disponibilities')
class MatchParticipant(db.Model):
"""Players participating in player scrimmage matches.
Links players to matches for player_scrim and player_vs_player match types.
Attributes:
id: Unique identifier.
match_id: Foreign key to the match.
player_id: Foreign key to the player.
team_side: Team side (1 or 2) for player_vs_player matches.
position: Player's position for this match.
added_at: Timestamp when added.
"""
__tablename__ = 'match_participants'
id = db.Column(db.Integer, primary_key=True)
match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=False)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
team_side = db.Column(db.Integer, nullable=True) # 1 for team 1, 2 for team 2 (for player_vs_player matches)
position = db.Column(db.String(50), nullable=True) # Position for this match
added_at = db.Column(db.DateTime, default=datetime.utcnow)
player = db.relationship('User')
class Contract(db.Model):
"""Contract documents for players to sign.
Tracks contract uploads and signed documents for players.
Attributes:
id: Unique identifier.
player_id: Foreign key to the player.
team_id: Foreign key to the player's team.
uploaded_by_id: Foreign key to the uploader.
original_filename: Original uploaded file name.
stored_filename: Stored file name on disk.
file_path: Full path to the contract file.
signed_filename: Signed file name (if signed).
signed_file_path: Full path to signed contract (if signed).
status: Contract status (pending, signed).
notes: Optional notes.
uploaded_at: Timestamp of upload.
signed_at: Timestamp of signing (if signed).
"""
__tablename__ = 'contracts'
id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
uploaded_by_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
# File information
original_filename = db.Column(db.String(255), nullable=False)
stored_filename = db.Column(db.String(255), nullable=False)
file_path = db.Column(db.String(500), nullable=False)
# Signed contract information
signed_filename = db.Column(db.String(255), nullable=True)
signed_file_path = db.Column(db.String(500), nullable=True)
# Status and metadata
status = db.Column(db.String(20), default='pending') # pending, signed
notes = db.Column(db.Text, nullable=True)
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
signed_at = db.Column(db.DateTime, nullable=True)
# Relationships
player = db.relationship('User', foreign_keys=[player_id], backref='contracts')
team = db.relationship('OrgTeam', foreign_keys=[team_id])
uploader = db.relationship('User', foreign_keys=[uploaded_by_id])
def can_view(self, user):
"""Check if a user can view this contract.
Players can always view their own contracts. Presidents and managers
have full access. Coaches can view contracts for players on their teams.
Args:
user: The User object requesting access.
Returns:
bool: True if user has permission to view the contract.
"""
# Player can always view their own contracts
if user.id == self.player_id:
return True
# President can view all contracts
if user.role == 'president':
return True
# Manager can view contracts for players on their teams
if user.role == 'manager':
player = User.query.get(self.player_id)
if player and player.team_id:
return True
# Coach can view contracts for players on their team
if user.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=user.id).first()
if org_team and (not self.team_id or self.team_id == org_team.id):
return True
return False
def can_upload_signed(self, user):
"""Check if a user can upload a signed contract.
Only the player themselves can upload their signed contract.
Args:
user: The User object requesting to upload.
Returns:
bool: True if user is the player associated with the contract.
"""
# Only the player can upload their signed contract
return user.id == self.player_id