Files
team-tryouts/app/models.py
T

854 lines
32 KiB
Python

"""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])