Force connection HTTPS, proxy-inversé, WSGI de production, reset cookie de conncection à chaque reconnection, limite sur les mdp, fichiers et One on One par minute, verification d'injection de SQL dans les champs d'entrées. renommage des fichiers lors du téléchargement, fichier de backup quotidien pour la bd et j'ai oublié quelque chose :(
865 lines
36 KiB
Python
865 lines
36 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', '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 ['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
|
|
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 == 'president':
|
|
return True
|
|
if self.role == 'manager' and (tryout.created_by == self.id or tryout.manager_id == 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
|
|
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.
|
|
|
|
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}
|
|
|
|
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'),
|
|
)
|
|
|
|
|
|
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.
|
|
manager_id: Foreign key to the assigned manager.
|
|
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)
|
|
manager_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)
|
|
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_org_team', uselist=False)
|
|
creator = db.relationship('User', foreign_keys=[created_by])
|
|
|
|
@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 == '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.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]) |