Files
team-tryouts/models.py
T

1012 lines
42 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 = ['admin', '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', 'Flex'],
'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.
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)
is_active_account = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Account lockout fields for brute-force protection
failed_login_attempts = db.Column(db.Integer, default=0)
locked_until = db.Column(db.DateTime, nullable=True)
# 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 (e.g., Username#1234)
discord_user_id = db.Column(db.String(64), nullable=True) # Discord User ID for DMs (numeric, e.g., 123456789012345678)
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 ['admin', '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 == 'admin'
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 ['admin', '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 ['admin', '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 ['admin', '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
or tryouts where they are assigned as manager. Coaches can manage tryouts
targeting their coached team or where they are assigned as coach.
Args:
tryout: The Tryout object to check permissions for.
Returns:
bool: True if user has permission to manage the tryout.
"""
if self.role == 'admin':
return True
if self.role == 'manager' and (tryout.created_by == self.id or tryout.manager_id == self.id):
return True
if self.role == 'coach':
# Check if coach belongs to the target org team (many-to-many)
if tryout.target_org_team_id:
is_coach_of_target = OrgTeam.query.filter(
OrgTeam.id == tryout.target_org_team_id,
OrgTeam.coaches.any(id=self.id)
).first() is not None
if is_coach_of_target:
return True
# Fallback to legacy coach_id
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
if tryout.coach_id == self.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 (via many-to-many or legacy).
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 == 'admin':
return True
if self.role == 'manager':
return True # Managers can manage all org teams (create/edit/delete)
if self.role == 'coach':
# Check many-to-many coaches
if org_team.coaches.filter_by(id=self.id).first():
return True
# Fallback to legacy coach_id
if 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}
def get_org_teams(self):
"""Return all organization teams this player belongs to.
Returns:
list: List of OrgTeam objects the player is assigned to.
"""
return [tp.org_team for tp in self.team_placements]
# 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': [],
'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/battlenet/{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 TeamPlayer(db.Model):
"""Many-to-many relationship between players and organization teams.
Allows a player to be in multiple teams with a status of starter or substitute.
Attributes:
id: Unique identifier.
player_id: Foreign key to the player.
org_team_id: Foreign key to the organization team.
status: Player status on the team ('starter' or 'substitute').
position: Player's position on the team (optional).
added_at: Timestamp when player was added.
"""
__tablename__ = 'team_players'
id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False)
status = db.Column(db.String(20), nullable=False, default='starter') # 'starter' or 'substitute'
position = db.Column(db.String(50), nullable=True)
added_at = db.Column(db.DateTime, default=datetime.utcnow)
player = db.relationship('User', foreign_keys=[player_id], backref='team_placements')
org_team = db.relationship('OrgTeam', foreign_keys=[org_team_id], backref='team_players')
__table_args__ = (
db.UniqueConstraint('player_id', 'org_team_id', name='unique_player_org_team'),
)
# Many-to-many association tables for multiple coaches and managers per team
org_team_coaches = db.Table('org_team_coaches',
db.Column('org_team_id', db.Integer, db.ForeignKey('org_teams.id', ondelete='CASCADE'), primary_key=True),
db.Column('coach_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True)
)
org_team_managers = db.Table('org_team_managers',
db.Column('org_team_id', db.Integer, db.ForeignKey('org_teams.id', ondelete='CASCADE'), primary_key=True),
db.Column('manager_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True)
)
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.
Supports multiple coaches and managers per team through many-to-many
junction tables (org_team_coaches, org_team_managers).
Attributes:
id: Unique identifier.
name: Team name (e.g., "Varsity", "Junior Varsity").
created_by: Foreign key to the user who created the team.
created_at: Timestamp of team creation.
coaches: Many-to-many relationship to User (coaches).
managers: Many-to-many relationship to User (managers).
"""
__tablename__ = 'org_teams'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Legacy single coach/manager columns kept for backward compatibility during migration
# These will be removed in a future migration after existing data is migrated
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
creator = db.relationship('User', foreign_keys=[created_by])
# New many-to-many relationships for multiple coaches/managers
coaches = db.relationship('User', secondary=org_team_coaches, lazy='dynamic',
backref=db.backref('coached_org_teams', lazy='dynamic'))
managers = db.relationship('User', secondary=org_team_managers, lazy='dynamic',
backref=db.backref('managed_org_teams', lazy='dynamic'))
# Legacy single relationships — now properties that use the many-to-many lists
coach = db.relationship('User', foreign_keys=[coach_id],
backref=db.backref('coached_org_team_legacy', uselist=False), viewonly=True)
manager = db.relationship('User', foreign_keys=[manager_id],
backref=db.backref('managed_org_team_legacy', uselist=False), viewonly=True)
def get_coaches(self):
"""Get the list of coaches for display/handling.
Returns coaches from the many-to-many relationship, falling back to
the legacy single coach for backward compatibility.
Returns:
list: List of User objects who are coaches for this team.
"""
coach_list = self.coaches.all()
if not coach_list and self.coach:
return [self.coach]
return coach_list
def get_managers(self):
"""Get the list of managers for display/handling.
Returns managers from the many-to-many relationship, falling back to
the legacy single manager for backward compatibility.
Returns:
list: List of User objects who are managers for this team.
"""
manager_list = self.managers.all()
if not manager_list and self.manager:
return [self.manager]
return manager_list
@property
def players(self):
"""Get all players assigned to this team.
Returns:
list: List of User objects assigned to this team.
"""
return [tp.player for tp in self.team_players]
def get_players_with_status(self):
"""Get all players with their status on this team.
Returns:
list: List of dicts with 'player' and 'status' keys.
"""
return [{'player': tp.player, 'status': tp.status, 'position': tp.position} for tp in self.team_players]
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.
manager_id: Foreign key to the assigned manager.
coach_id: Foreign key to the assigned coach.
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)
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
creator = db.relationship('User', foreign_keys=[created_by], backref='created_tryouts')
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_tryouts')
coach = db.relationship('User', foreign_keys=[coach_id], backref='coached_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
attendance_confirmed = db.Column(db.Boolean, default=False) # Whether the player confirmed via Discord or manual toggle
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 == 'admin':
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.get_org_teams():
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
class CoachAvailability(db.Model):
"""Coach availability in 30-minute time blocks for One on One sessions.
Allows coaches to specify when they're available for individual coaching sessions.
Attributes:
id: Unique identifier.
coach_id: Foreign key to the coach.
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__ = 'coach_availabilities'
id = db.Column(db.Integer, primary_key=True)
coach_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)
coach = db.relationship('User', backref='coach_availabilities')
class TeamNote(db.Model):
"""Team improvement notes from coach.
Contains coaching notes and suggestions for team improvement.
Attributes:
id: Unique identifier.
org_team_id: Foreign key to the organization team.
coach_id: Foreign key to the coach who wrote the notes.
content: The note content.
created_at: Timestamp of creation.
updated_at: Timestamp of last update.
"""
__tablename__ = 'team_notes'
id = db.Column(db.Integer, primary_key=True)
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False)
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
team = db.relationship('OrgTeam', backref='team_notes')
coach = db.relationship('User', foreign_keys=[coach_id])
class PersonalNote(db.Model):
"""Personal notes from coach to individual player.
Contains individual feedback and coaching tips for players.
Can be linked to specific contexts: match, team, or tryout.
Attributes:
id: Unique identifier.
player_id: Foreign key to the player.
coach_id: Foreign key to the coach who wrote the notes.
content: The note content.
created_at: Timestamp of creation.
updated_at: Timestamp of last update.
match_id: Optional foreign key to the match context.
team_id: Optional foreign key to the team context.
tryout_id: Optional foreign key to the tryout context.
"""
__tablename__ = 'personal_notes'
id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Optional context linking
match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=True)
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=True)
player = db.relationship('User', foreign_keys=[player_id], backref='personal_notes')
coach = db.relationship('User', foreign_keys=[coach_id])
match = db.relationship('Match', foreign_keys=[match_id])
team = db.relationship('Team', foreign_keys=[team_id])
tryout = db.relationship('Tryout', foreign_keys=[tryout_id])
class OneOnOneRequest(db.Model):
"""Request from player to coach for a One on One session.
Tracks requests for individual coaching sessions with time slot selection.
Attributes:
id: Unique identifier.
player_id: Foreign key to the player requesting.
coach_id: Foreign key to the coach.
org_team_id: Foreign key to the player's team.
date: Requested date for the session.
start_time: Requested start time.
end_time: Requested end time.
points: What the player wants to discuss.
status: Request status (pending, approved, rejected, scheduled).
created_at: Timestamp of creation.
responded_at: Timestamp when coach responded.
discord_message_id: Discord message ID for reaction handling.
coach_rejection_message: Optional justification from coach when rejecting.
"""
__tablename__ = 'one_on_one_requests'
id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
date = db.Column(db.Date, nullable=False)
start_time = db.Column(db.Time, nullable=False)
end_time = db.Column(db.Time, nullable=False)
points = db.Column(db.Text, nullable=True)
status = db.Column(db.String(20), default='pending') # pending, approved, rejected, scheduled
created_at = db.Column(db.DateTime, default=datetime.utcnow)
responded_at = db.Column(db.DateTime, nullable=True)
discord_message_id = db.Column(db.BigInteger, nullable=True) # Discord message ID for reaction handling
coach_rejection_message = db.Column(db.Text, nullable=True) # Optional justification when rejecting
player = db.relationship('User', foreign_keys=[player_id], backref='one_on_one_requests')
coach = db.relationship('User', foreign_keys=[coach_id])
team = db.relationship('OrgTeam', foreign_keys=[org_team_id])
class TeamMatch(db.Model):
"""Regular season match for an organization team (not tied to a tryout).
These matches are team-specific, not tryout-specific. The team's roster is
automatically pre-filled as participants. The creator (coach, manager, president)
only needs to select date-time and an optional opponent.
Attributes:
id: Unique identifier.
org_team_id: Foreign key to the organization team.
title: Match title/name.
description: Optional description.
opponent: Optional opponent name.
date: Match date.
start_time: Match start time.
end_time: Match end time.
location: Match location.
status: Match status (scheduled, completed, cancelled).
created_by: Foreign key to the creator.
created_at: Timestamp of creation.
"""
__tablename__ = 'team_matches'
id = db.Column(db.Integer, primary_key=True)
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text, nullable=True)
opponent = db.Column(db.String(200), 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
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
org_team = db.relationship('OrgTeam', backref='team_matches')
creator = db.relationship('User', backref='created_team_matches')
participants = db.relationship('TeamMatchParticipant', backref='team_match', lazy='dynamic', cascade='all, delete-orphan')
def get_confirmed_count(self):
"""Get count of confirmed participants.
Returns:
tuple: (confirmed_count, total_count)
"""
all_p = self.participants.all()
confirmed = sum(1 for p in all_p if p.is_confirmed)
return confirmed, len(all_p)
class TeamMatchParticipant(db.Model):
"""Participant in a team match (regular season).
Automatically created for all team players when a TeamMatch is created.
Tracks attendance confirmation per player.
Attributes:
id: Unique identifier.
team_match_id: Foreign key to the team match.
player_id: Foreign key to the player.
is_confirmed: Whether attendance is confirmed (manual toggle or Discord reaction).
added_at: Timestamp when added.
"""
__tablename__ = 'team_match_participants'
id = db.Column(db.Integer, primary_key=True)
team_match_id = db.Column(db.Integer, db.ForeignKey('team_matches.id'), nullable=False)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
is_confirmed = db.Column(db.Boolean, default=False)
added_at = db.Column(db.DateTime, default=datetime.utcnow)
player = db.relationship('User')
__table_args__ = (
db.UniqueConstraint('team_match_id', 'player_id', name='unique_team_match_player'),
)