from extensions import db, login_manager from flask_login import UserMixin from datetime import datetime from urllib.parse import quote ROLES = ['president', 'manager', 'coach', 'player', 'scout'] # Popular E-Sports games list ESPORT_GAMES = [ 'Valorant', 'League of Legends', 'Counter-Strike 2', 'Apex Legends', 'Overwatch 2', 'Rainbow Six Siege', 'Fortnite', 'Rocket League', 'Call of Duty', 'Dota 2', 'Super Smash Bros.', 'Street Fighter 6', ] class User(UserMixin, db.Model): __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(100), nullable=True) # Discord handle league_os_profile = db.Column(db.String(255), 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 games as a list.""" if self.games: return [g.strip() for g in self.games.split(',') if g.strip()] return [] def has_role(self, *roles): return self.role in roles def can_evaluate(self): return self.role in ['president', 'manager', 'coach'] def can_manage_users(self): return self.role == 'president' def can_manage_tryouts(self): return self.role in ['president', 'manager', 'coach', 'scout'] def can_manage_teams(self): return self.role in ['president', 'manager'] def can_schedule_matches(self): return self.role in ['president', 'manager', 'coach'] def can_manage_this_tryout(self, tryout): """Check if user can manage a specific tryout (president, or manager/coach in charge of it).""" 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 (president, or manager/coach in charge of it).""" 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 dict keyed by game.""" return {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in self.gamertags} @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) # 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 'Fortnite': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch', 'Mobile'], 'Rocket League': ['Epic', 'PlayStation', 'Xbox', 'Nintendo Switch'], # Epic uses EpicID, others use username 'Call of Duty': ['PC', 'PlayStation', 'Xbox'], 'Dota 2': [], 'Super Smash Bros.': ['Nintendo Switch'], 'Street Fighter 6': ['PC', 'PlayStation', 'Xbox'], } # Platform code mapping for TRN URLs (platform -> TRN code) PLATFORM_CODES = { 'Ubisoft': 'ubi', 'PlayStation': 'psn', 'Xbox': 'xbl', 'Nintendo Switch': 'switch', 'PC': 'pc', 'Mobile': 'mobile', '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}', 'Fortnite': 'https://tracker.gg/fortnite/profile/{platform}/{username}', 'Rocket League': 'https://rocketleague.tracker.network/rocket-league/profile/{platform_code}/{username}', 'Call of Duty': 'https://tracker.gg/call-of-duty/profile/{platform}/{username}', 'Dota 2': 'https://tracker.gg/dota2/profile/steam/{username}', 'Super Smash Bros.': 'https://tracker.gg/smash/profile/{username}', 'Street Fighter 6': 'https://tracker.gg/streetfighter/profile/{username}', } class UserGamertag(db.Model): """Store gamertag per game for each user.""" __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 URL for this gamertag.""" 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.""" __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): __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) 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): __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): __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) speed_score = db.Column(db.Integer, nullable=True) agility_score = db.Column(db.Integer, nullable=True) technique_score = db.Column(db.Integer, nullable=True) teamwork_score = db.Column(db.Integer, nullable=True) attitude_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 teams (e.g., Alpha, Bravo within a single tryout).""" __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): __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): """Matches/scrimmages scheduled within tryouts.""" __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 players participating in this match.""" return [p.player_id for p in self.participants.all()] class PlayerDisponibility(db.Model): """Player availability in 30-minute time blocks for scheduling matches.""" __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.""" __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')