"""Database models for the Team Tryouts application. This module defines all SQLAlchemy models using: - Single-table polymorphic inheritance for User → Admin, Manager, Coach, Player, Scout - Abstract base classes for repeating hierarchies: BaseMatch, BaseParticipant, BaseAvailability All logic stays roughly the same; role-check chains are replaced with proper polymorphic dispatch. """ from app.extensions import db, login_manager from flask_login import UserMixin from datetime import datetime from urllib.parse import quote # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- USER_TYPES = ['admin', 'manager', 'coach', 'player', 'scout'] ESPORT_GAMES = [ 'Valorant', 'League of Legends', 'Counter-Strike 2', 'Apex Legends', 'Overwatch 2', 'Rainbow Six Siege', 'Rocket League', 'Super Smash Bros.', ] 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.': [], } GAME_PLATFORMS = { 'Valorant': [], 'League of Legends': [], 'Counter-Strike 2': [], 'Apex Legends': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'], 'Overwatch 2': [], 'Rainbow Six Siege': ['Ubisoft', 'PlayStation', 'Xbox'], 'Rocket League': ['Epic', 'PlayStation', 'Xbox', 'Nintendo Switch'], 'Super Smash Bros.': ['Nintendo Switch'], } PLATFORM_CODES = { 'Ubisoft': 'ubi', 'PlayStation': 'psn', 'Xbox': 'xbl', 'Nintendo Switch': 'switch', 'PC': 'pc', 'Steam': 'steam', 'Epic': 'epic', } 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}', } # --------------------------------------------------------------------------- # Flask-Login user loader # --------------------------------------------------------------------------- @login_manager.user_loader def load_user(user_id): """Load a user by ID for Flask-Login session management. Returns the correct polymorphic subclass (Admin, Coach, Player, etc.) automatically because SQLAlchemy resolves the identity column. """ return User.query.get(int(user_id)) # =========================================================================== # USER HIERARCHY (single-table polymorphic inheritance) # =========================================================================== # # User (base, __tablename__ = 'users', polymorphic_on = 'role') # ├── Admin ─ polymorphic_identity = 'admin' # ├── Manager ─ polymorphic_identity = 'manager' # ├── Coach ─ polymorphic_identity = 'coach' # ├── Player ─ polymorphic_identity = 'player' # └── Scout ─ polymorphic_identity = 'scout' # # Single-table inheritance keeps the DB simple while giving full isinstance() # support and per-subclass methods. Every existing foreign-key pointing at # users.id continues to work without migration. # =========================================================================== class User(UserMixin, db.Model): """Base user model — shared fields for every role. Do not instantiate this class directly; use Admin, Manager, Coach, Player, or Scout so that `polymorphic_identity` is set correctly. """ __tablename__ = 'users' # --- columns ----------------------------------------------------------- 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') # polymorphic discriminator 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) failed_login_attempts = db.Column(db.Integer, default=0) locked_until = db.Column(db.DateTime, nullable=True) # E-Sports fields games = db.Column(db.Text, nullable=True) # comma-separated (only meaningful for Player) discord_username = db.Column(db.String(128), nullable=True) discord_user_id = db.Column(db.String(64), nullable=True) league_os_profile = db.Column(db.String(256), nullable=True) # --- polymorphic configuration ----------------------------------------- __mapper_args__ = { 'polymorphic_identity': 'user', 'polymorphic_on': role, } # --- relationships (defined once on the base) -------------------------- 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') # --- shared helper methods --------------------------------------------- def get_games_list(self): """Return the user's games as a list.""" if self.games: return [g.strip() for g in self.games.split(',') if g.strip()] return [] 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} def get_org_teams(self): """Return all OrgTeams this player belongs to.""" return [tp.org_team for tp in self.team_placements] # --- stubs (overridden in subclasses) ---------------------------------- def can_evaluate(self): return False def can_manage_users(self): return False def can_manage_teams(self): return False def can_manage_tryouts(self): return False def can_schedule_matches(self): return False def can_manage_this_tryout(self, tryout): return False def can_manage_this_org_team(self, org_team): return False def get_visible_tryouts(self): return [] # --------------------------------------------------------------------------- # Concrete user subclasses # --------------------------------------------------------------------------- class Admin(User): """President / super-admin — full access to everything.""" __mapper_args__ = {'polymorphic_identity': 'admin'} def can_evaluate(self): return True def can_manage_users(self): return True def can_manage_teams(self): return True def can_manage_tryouts(self): return True def can_schedule_matches(self): return True def can_manage_this_tryout(self, tryout): return True def can_manage_this_org_team(self, org_team): return True def get_visible_tryouts(self): return Tryout.query.order_by(Tryout.date).all() class Manager(User): """Manager — manages own tryouts, all org teams, all contracts.""" __mapper_args__ = {'polymorphic_identity': 'manager'} def can_evaluate(self): return True def can_manage_teams(self): return True def can_manage_tryouts(self): return True def can_schedule_matches(self): return True def can_manage_this_tryout(self, tryout): return tryout.created_by == self.id or tryout.manager_id == self.id def can_manage_this_org_team(self, org_team): return True def get_visible_tryouts(self): return Tryout.query.filter_by(created_by=self.id).order_by(Tryout.date).all() class Coach(User): """Coach — evaluates, schedules matches, manages their own org team.""" __mapper_args__ = {'polymorphic_identity': 'coach'} def can_evaluate(self): return True def can_schedule_matches(self): return True def can_manage_tryouts(self): return True def can_manage_this_tryout(self, tryout): 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 if tryout.coach_id == self.id: return True return False def can_manage_this_org_team(self, org_team): if org_team.coaches.filter_by(id=self.id).first(): return True if org_team.coach_id == self.id: return True return False def get_visible_tryouts(self): team_ids = [t.id for t in self.coached_org_teams.all()] if not team_ids: return Tryout.query.filter(Tryout.id == -1).all() # empty return Tryout.query.filter( Tryout.target_org_team_id.in_(team_ids) ).order_by(Tryout.date).all() class Player(User): """Player — registers for tryouts, manages their own profile.""" __mapper_args__ = {'polymorphic_identity': 'player'} def can_evaluate(self): return False def can_schedule_matches(self): return False def can_manage_this_tryout(self, tryout): return False def can_manage_this_org_team(self, org_team): return False def get_visible_tryouts(self): # tryouts they registered for player_tryout_ids = [r.tryout_id for r in self.tryout_registrations.all()] tryouts = Tryout.query.filter( Tryout.id.in_(player_tryout_ids) ).order_by(Tryout.date).all() if player_tryout_ids else [] # plus tryouts where they participate in a match player_matches = Match.query.join(MatchParticipant).filter( MatchParticipant.player_id == self.id, ).all() extra_ids = set(m.tryout_id for m in player_matches) extra = Tryout.query.filter( Tryout.id.in_(extra_ids), ).order_by(Tryout.date).all() if extra_ids else [] all_ids = {t.id for t in tryouts} return tryouts + [t for t in extra if t.id not in all_ids] class Scout(User): """Scout — view-only access to tryouts and evaluations.""" __mapper_args__ = {'polymorphic_identity': 'scout'} def can_evaluate(self): return True def get_visible_tryouts(self): return Tryout.query.order_by(Tryout.date).all() # =========================================================================== # GAMERTAGS # =========================================================================== 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) user = db.relationship('User', backref='gamertags') __table_args__ = ( db.UniqueConstraint('user_id', 'game', name='unique_user_game'), ) def get_trn_url(self): if self.game not in TRN_URLS: return None url = TRN_URLS[self.game] encoded_gamertag = quote(self.gamertag, safe='') 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 # =========================================================================== # ABSTRACT BASE: AVAILABILITY (PlayerDisponibility + CoachAvailability) # =========================================================================== class BaseAvailability(db.Model): """Shared schema for player disponibilities and coach availabilities.""" __abstract__ = True 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) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class PlayerDisponibility(BaseAvailability): """Player availability in 30-minute blocks.""" __tablename__ = 'player_disponibilities' id = db.Column(db.Integer, primary_key=True) player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) player = db.relationship('User', backref='disponibilities') class CoachAvailability(BaseAvailability): """Coach availability in 30-minute blocks for One on One sessions.""" __tablename__ = 'coach_availabilities' id = db.Column(db.Integer, primary_key=True) coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) coach = db.relationship('User', backref='coach_availabilities') # =========================================================================== # ASSOCIATION TABLES # =========================================================================== 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), ) # =========================================================================== # ORGANISATION TEAMS # =========================================================================== class OrgTeam(db.Model): """Persistent organisation team (e.g. Varsity, JV).""" __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-assignment columns (kept for back-compat during migration) 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]) 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')) 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) # --- helpers ----------------------------------------------------------- def get_coaches(self): coach_list = self.coaches.all() if not coach_list and self.coach: return [self.coach] return coach_list def get_managers(self): manager_list = self.managers.all() if not manager_list and self.manager: return [self.manager] return manager_list @property def players(self): return [tp.player for tp in self.team_players] def get_players_with_status(self): return [{'player': tp.player, 'status': tp.status, 'position': tp.position} for tp in self.team_players] class TeamPlayer(db.Model): """Many-to-many: player ↔ org-team.""" __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') 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'), ) # =========================================================================== # TRYOUTS # =========================================================================== class Tryout(db.Model): """Tryout event for player evaluations and team formation.""" __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') 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.""" __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') notes = db.Column(db.Text, nullable=True) class Evaluation(db.Model): """Player evaluation record.""" __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'), ) # =========================================================================== # TRYOUT-SPECIFIC TEAMS # =========================================================================== class Team(db.Model): """Tryout-specific team (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): """Link between a player and a tryout-specific team.""" __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") # =========================================================================== # ABSTRACT BASE: MATCH (Match + TeamMatch) # =========================================================================== class BaseMatch(db.Model): """Shared schema for tryout-scoped matches and regular-season team matches.""" __abstract__ = True 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 created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) class Match(BaseMatch): """Match / scrimmage within a tryout.""" __tablename__ = 'matches' id = db.Column(db.Integer, primary_key=True) tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) match_type = db.Column(db.String(20), nullable=False) # team_vs_team | player_scrim | player_vs_player 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 [p.player_id for p in self.participants.all()] class TeamMatch(BaseMatch): """Regular-season match for an organisation team (not tied to a tryout).""" __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) opponent = db.Column(db.String(200), nullable=True) 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): all_p = self.participants.all() confirmed = sum(1 for p in all_p if p.is_confirmed) return confirmed, len(all_p) # =========================================================================== # ABSTRACT BASE: PARTICIPANT (MatchParticipant + TeamMatchParticipant) # =========================================================================== class BaseParticipant(db.Model): """Shared schema for match participants.""" __abstract__ = True player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) added_at = db.Column(db.DateTime, default=datetime.utcnow) class MatchParticipant(BaseParticipant): """Participant in a tryout-scoped match.""" __tablename__ = 'match_participants' id = db.Column(db.Integer, primary_key=True) match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=False) team_side = db.Column(db.Integer, nullable=True) # 1 or 2 (player_vs_player) position = db.Column(db.String(50), nullable=True) attendance_confirmed = db.Column(db.Boolean, default=False) player = db.relationship('User') class TeamMatchParticipant(BaseParticipant): """Participant in a regular-season team match.""" __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) is_confirmed = db.Column(db.Boolean, default=False) player = db.relationship('User') # =========================================================================== # CONTRACTS # =========================================================================== class Contract(db.Model): """Contract documents for players to sign.""" __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) 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_filename = db.Column(db.String(255), nullable=True) signed_file_path = db.Column(db.String(500), nullable=True) status = db.Column(db.String(20), default='pending') notes = db.Column(db.Text, nullable=True) uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) signed_at = db.Column(db.DateTime, nullable=True) 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): if user.id == self.player_id: return True if isinstance(user, Admin): return True if isinstance(user, Manager): player = User.query.get(self.player_id) if player and player.get_org_teams(): return True if isinstance(user, 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): return user.id == self.player_id # =========================================================================== # NOTES # =========================================================================== class TeamNote(db.Model): """Team improvement notes from coach.""" __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.""" __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) 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]) # =========================================================================== # ONE-ON-ONE REQUESTS # =========================================================================== class OneOnOneRequest(db.Model): """Request from player to coach for a One on One session.""" __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') 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) coach_rejection_message = db.Column(db.Text, nullable=True) 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])