remodulation du projet en POO et changement de l'organisation des fichiers
This commit is contained in:
+2
-2
@@ -342,8 +342,8 @@ def create_app():
|
|||||||
# Database Initialization
|
# Database Initialization
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
import app.models as models # noqa: F401 — registers all models with SQLAlchemy
|
import app.models.models as models # noqa: F401 — registers all models with SQLAlchemy
|
||||||
from app.models import User
|
from app.models.models import User
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
# Seed database if empty
|
# Seed database if empty
|
||||||
|
|||||||
+6
-6
@@ -188,7 +188,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Look up the DB user to get their Discord user ID
|
# Look up the DB user to get their Discord user ID
|
||||||
from app.models import User as DBUser
|
from app.models.models import User as DBUser
|
||||||
db_user = DBUser.query.get(user_id)
|
db_user = DBUser.query.get(user_id)
|
||||||
if not db_user:
|
if not db_user:
|
||||||
logger.warning(f"DB user {user_id} not found for schedule notification")
|
logger.warning(f"DB user {user_id} not found for schedule notification")
|
||||||
@@ -234,7 +234,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
async def handle_one_on_one_approve(self, coach, message_id, request_id, original_message):
|
async def handle_one_on_one_approve(self, coach, message_id, request_id, original_message):
|
||||||
"""Handle coach approving a One on One request."""
|
"""Handle coach approving a One on One request."""
|
||||||
try:
|
try:
|
||||||
from app.models import OneOnOneRequest, db
|
from app.models.models import OneOnOneRequest, db
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
request = OneOnOneRequest.query.options(
|
request = OneOnOneRequest.query.options(
|
||||||
@@ -278,7 +278,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
async def handle_one_on_one_reject(self, coach, message_id, request_id, original_message):
|
async def handle_one_on_one_reject(self, coach, message_id, request_id, original_message):
|
||||||
"""Handle coach rejecting a One on One request."""
|
"""Handle coach rejecting a One on One request."""
|
||||||
try:
|
try:
|
||||||
from app.models import OneOnOneRequest, db
|
from app.models.models import OneOnOneRequest, db
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
request = OneOnOneRequest.query.options(
|
request = OneOnOneRequest.query.options(
|
||||||
@@ -336,7 +336,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
async def handle_attendance_confirm(self, player, message_id, reference_id, original_message):
|
async def handle_attendance_confirm(self, player, message_id, reference_id, original_message):
|
||||||
"""Handle player confirming attendance for a match/tryout."""
|
"""Handle player confirming attendance for a match/tryout."""
|
||||||
try:
|
try:
|
||||||
from app.models import MatchParticipant, TryoutRegistration, Match, Tryout, db
|
from app.models.models import MatchParticipant, TryoutRegistration, Match, Tryout, db
|
||||||
|
|
||||||
request_info = self.pending_requests[message_id]
|
request_info = self.pending_requests[message_id]
|
||||||
event_type = request_info.get('event_type')
|
event_type = request_info.get('event_type')
|
||||||
@@ -361,7 +361,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
async def handle_attendance_decline(self, player, message_id, reference_id, original_message):
|
async def handle_attendance_decline(self, player, message_id, reference_id, original_message):
|
||||||
"""Handle player declining attendance for a match/tryout."""
|
"""Handle player declining attendance for a match/tryout."""
|
||||||
try:
|
try:
|
||||||
from app.models import MatchParticipant, TryoutRegistration, Match, Tryout, db
|
from app.models.models import MatchParticipant, TryoutRegistration, Match, Tryout, db
|
||||||
|
|
||||||
request_info = self.pending_requests[message_id]
|
request_info = self.pending_requests[message_id]
|
||||||
event_type = request_info.get('event_type')
|
event_type = request_info.get('event_type')
|
||||||
@@ -471,7 +471,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
async def send_daily_reminders(self):
|
async def send_daily_reminders(self):
|
||||||
"""Send daily reminders at 18:00 EDT for events in 24-48 hours."""
|
"""Send daily reminders at 18:00 EDT for events in 24-48 hours."""
|
||||||
try:
|
try:
|
||||||
from app.models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest, db
|
from app.models.models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest, db
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
now = datetime.now(self.timezone)
|
now = datetime.now(self.timezone)
|
||||||
|
|||||||
-854
@@ -1,854 +0,0 @@
|
|||||||
"""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])
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"""All models — split into individual files for maintainability.
|
||||||
|
|
||||||
|
Import this module to register all models with SQLAlchemy and expose every
|
||||||
|
class, constant, and helper for use throughout the application.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
from app.models import User, Admin, Evaluation, ESPORT_GAMES, ...
|
||||||
|
|
||||||
|
Backward-compatible — no consumer changes needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 0: constants (no app deps)
|
||||||
|
# =========================================================================
|
||||||
|
from app.models._constants import (
|
||||||
|
USER_TYPES,
|
||||||
|
ESPORT_GAMES,
|
||||||
|
GAME_POSITIONS,
|
||||||
|
GAME_PLATFORMS,
|
||||||
|
PLATFORM_CODES,
|
||||||
|
TRN_URLS,
|
||||||
|
)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 1: loaders & associations
|
||||||
|
# =========================================================================
|
||||||
|
from app.models._loaders import load_user # noqa: F401 — registers Flask-Login callback
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 2: abstract base classes
|
||||||
|
# =========================================================================
|
||||||
|
from app.models.availability.base import BaseAvailability
|
||||||
|
from app.models.match_model.base import BaseMatch
|
||||||
|
from app.models.participant.base import BaseParticipant
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 3: user hierarchy (polymorphic)
|
||||||
|
# =========================================================================
|
||||||
|
from app.models.user_model.user import User
|
||||||
|
from app.models.user_model.admin import Admin
|
||||||
|
from app.models.user_model.manager import Manager
|
||||||
|
from app.models.user_model.coach import Coach
|
||||||
|
from app.models.user_model.player import Player
|
||||||
|
from app.models.user_model.scout import Scout
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 4: org_team + junction
|
||||||
|
# =========================================================================
|
||||||
|
from app.models.org_team.org_team import OrgTeam
|
||||||
|
from app.models.org_team.team_player import TeamPlayer
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 5: concrete availability models
|
||||||
|
# =========================================================================
|
||||||
|
from app.models.availability.player_disponibility import PlayerDisponibility
|
||||||
|
from app.models.availability.coach_availability import CoachAvailability
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 6: tryout + registration
|
||||||
|
# =========================================================================
|
||||||
|
from app.models.tryout.tryout import Tryout
|
||||||
|
from app.models.tryout.tryout_registration import TryoutRegistration
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 7: evaluation
|
||||||
|
# =========================================================================
|
||||||
|
from app.models.evaluation import Evaluation
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 8: tryout-specific teams
|
||||||
|
# =========================================================================
|
||||||
|
from app.models.team.team import Team
|
||||||
|
from app.models.team.team_member import TeamMember
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 9: matches (tryout-scoped + regular-season)
|
||||||
|
# =========================================================================
|
||||||
|
from app.models.match_model.match import Match
|
||||||
|
from app.models.match_model.team_match import TeamMatch
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 10: participants
|
||||||
|
# =========================================================================
|
||||||
|
from app.models.participant.match_participant import MatchParticipant
|
||||||
|
from app.models.participant.team_match_participant import TeamMatchParticipant
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Layer 11: remaining standalone models
|
||||||
|
# =========================================================================
|
||||||
|
from app.models.user_gamertag import UserGamertag
|
||||||
|
from app.models.contract import Contract
|
||||||
|
from app.models.team_note import TeamNote
|
||||||
|
from app.models.personal_note import PersonalNote
|
||||||
|
from app.models.one_on_one_request import OneOnOneRequest
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"""Many-to-many association tables for OrgTeam ↔ User relationships."""
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Global constants shared by all model files.
|
||||||
|
|
||||||
|
Contains game lists, position mappings, platform codes, and TRN URL templates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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}',
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""Flask-Login user loader — registered with login_manager in models.py."""
|
||||||
|
|
||||||
|
from app.extensions import login_manager
|
||||||
|
|
||||||
|
|
||||||
|
@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.
|
||||||
|
"""
|
||||||
|
from app.models.user_model.user import User
|
||||||
|
return User.query.get(int(user_id))
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
"""Availability models — BaseAvailability and its concrete subclasses."""
|
||||||
|
|
||||||
|
from app.models.availability.base import BaseAvailability
|
||||||
|
from app.models.availability.player_disponibility import PlayerDisponibility
|
||||||
|
from app.models.availability.coach_availability import CoachAvailability
|
||||||
|
|
||||||
|
__all__ = ['BaseAvailability', 'PlayerDisponibility', 'CoachAvailability']
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""Abstract base class for availability models (PlayerDisponibility + CoachAvailability)."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAvailability(db.Model):
|
||||||
|
"""Shared schema for player disponibilities and coach availabilities."""
|
||||||
|
__abstract__ = True
|
||||||
|
|
||||||
|
day_of_week = db.Column(db.Integer, nullable=False)
|
||||||
|
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)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"""Coach availability in 30-minute time blocks for One on One sessions."""
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.availability.base import BaseAvailability
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"""Player availability in 30-minute time blocks."""
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.availability.base import BaseAvailability
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Contract documents for players to sign."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
from app.models.user_model.admin import Admin
|
||||||
|
from app.models.user_model.manager import Manager
|
||||||
|
from app.models.user_model.coach import Coach
|
||||||
|
from app.models.user_model.user import User
|
||||||
|
from app.models.org_team.org_team import OrgTeam
|
||||||
|
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
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Player evaluation record."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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'),
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""Match models — BaseMatch and its concrete subclasses."""
|
||||||
|
from app.models.match_model.base import BaseMatch
|
||||||
|
from app.models.match_model.match import Match
|
||||||
|
from app.models.match_model.team_match import TeamMatch
|
||||||
|
|
||||||
|
__all__ = ['BaseMatch', 'Match', 'TeamMatch']
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"""Abstract base class for match models (Match + TeamMatch)."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
|
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""Match / scrimmage within a tryout."""
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.match_model.base import BaseMatch
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
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()]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""Regular-season match for an organisation team (not tied to a tryout)."""
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.match_model.base import BaseMatch
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""Request from player to coach for a One on One session."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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])
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Organisation team models."""
|
||||||
|
from app.models.org_team.org_team import OrgTeam
|
||||||
|
from app.models.org_team.team_player import TeamPlayer
|
||||||
|
|
||||||
|
__all__ = ['OrgTeam', 'TeamPlayer']
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Persistent organisation team (e.g. Varsity, JV)."""
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models._associations import org_team_coaches, org_team_managers
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"""Many-to-many junction: player to org-team."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class TeamPlayer(db.Model):
|
||||||
|
"""Many-to-many: player to 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'),
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""Participant models — BaseParticipant and its concrete subclasses."""
|
||||||
|
from app.models.participant.base import BaseParticipant
|
||||||
|
from app.models.participant.match_participant import MatchParticipant
|
||||||
|
from app.models.participant.team_match_participant import TeamMatchParticipant
|
||||||
|
|
||||||
|
__all__ = ['BaseParticipant', 'MatchParticipant', 'TeamMatchParticipant']
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"""Abstract base class for match participant models."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"""Participant in a tryout-scoped match."""
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.participant.base import BaseParticipant
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
position = db.Column(db.String(50), nullable=True)
|
||||||
|
attendance_confirmed = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
|
player = db.relationship('User')
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
"""Participant in a regular-season team match."""
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.participant.base import BaseParticipant
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""Personal notes from coach to individual player."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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])
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Tryout-specific temporary team models."""
|
||||||
|
from app.models.team.team import Team
|
||||||
|
from app.models.team.team_member import TeamMember
|
||||||
|
|
||||||
|
__all__ = ['Team', 'TeamMember']
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""Tryout-specific team (e.g. Alpha, Bravo within a single tryout)."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"""Link between a player and a tryout-specific team."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"""Team improvement notes from coach."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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])
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Tryout models."""
|
||||||
|
from app.models.tryout.tryout import Tryout
|
||||||
|
from app.models.tryout.tryout_registration import TryoutRegistration
|
||||||
|
|
||||||
|
__all__ = ['Tryout', 'TryoutRegistration']
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Tryout event for player evaluations and team formation."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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])
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""Registration linking a player to a tryout."""
|
||||||
|
from app.extensions import db
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Store gamertag per game for each user."""
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models._constants import TRN_URLS, PLATFORM_CODES
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
"""User hierarchy — single-table polymorphic inheritance (User → Admin, Manager, Coach, Player, Scout)."""
|
||||||
|
from app.models.user_model.user import User
|
||||||
|
from app.models.user_model.admin import Admin
|
||||||
|
from app.models.user_model.manager import Manager
|
||||||
|
from app.models.user_model.coach import Coach
|
||||||
|
from app.models.user_model.player import Player
|
||||||
|
from app.models.user_model.scout import Scout
|
||||||
|
|
||||||
|
__all__ = ['User', 'Admin', 'Manager', 'Coach', 'Player', 'Scout']
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Admin / President — full access to everything."""
|
||||||
|
from app.models.user_model.user import User
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
from app.models.tryout.tryout import Tryout
|
||||||
|
return Tryout.query.order_by(Tryout.date).all()
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Coach — evaluates, schedules matches, manages their own org team."""
|
||||||
|
from app.models.user_model.user import User
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
from app.models.org_team.org_team import OrgTeam
|
||||||
|
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):
|
||||||
|
from app.models.tryout.tryout import Tryout
|
||||||
|
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()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Manager — manages own tryouts, all org teams, all contracts."""
|
||||||
|
from app.models.user_model.user import User
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
from app.models.tryout.tryout import Tryout
|
||||||
|
return Tryout.query.filter_by(created_by=self.id).order_by(Tryout.date).all()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Player — registers for tryouts, manages their own profile."""
|
||||||
|
from app.models.user_model.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Player(User):
|
||||||
|
"""Player — registers for tryouts, manages their own profile."""
|
||||||
|
__mapper_args__ = {'polymorphic_identity': 'player'}
|
||||||
|
|
||||||
|
def get_visible_tryouts(self):
|
||||||
|
from app.models.tryout.tryout import Tryout
|
||||||
|
from app.models.match_model.match import Match
|
||||||
|
from app.models.participant.match_participant import MatchParticipant
|
||||||
|
|
||||||
|
# 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]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""Scout — view-only access to tryouts and evaluations."""
|
||||||
|
from app.models.user_model.user import User
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
from app.models.tryout.tryout import Tryout
|
||||||
|
return Tryout.query.order_by(Tryout.date).all()
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""Base User model — shared fields and polymorphic configuration."""
|
||||||
|
from app.extensions import db
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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 []
|
||||||
+1
-1
@@ -10,7 +10,7 @@ from datetime import datetime, timedelta
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, session
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, session
|
||||||
from flask_login import login_user, logout_user, login_required, current_user
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
from app.extensions import db, hash_password, check_password, limiter
|
from app.extensions import db, hash_password, check_password, limiter
|
||||||
from app.models import User, Player, ESPORT_GAMES
|
from app.models.models import User, Player, ESPORT_GAMES
|
||||||
from app.validators import RegisterSchema, LoginSchema
|
from app.validators import RegisterSchema, LoginSchema
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons.
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import (
|
from app.models.models import (
|
||||||
Admin, Coach, Manager, Player,
|
Admin, Coach, Manager, Player,
|
||||||
User, Tryout, Evaluation, TryoutRegistration,
|
User, Tryout, Evaluation, TryoutRegistration,
|
||||||
OrgTeam, GAME_POSITIONS,
|
OrgTeam, GAME_POSITIONS,
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons.
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash
|
from flask import Blueprint, render_template, redirect, url_for, flash
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import (
|
from app.models.models import (
|
||||||
Admin, Manager, Coach, Player, Scout,
|
Admin, Manager, Coach, Player, Scout,
|
||||||
User, Tryout, Evaluation, TryoutRegistration, Team, TeamMember,
|
User, Tryout, Evaluation, TryoutRegistration, Team, TeamMember,
|
||||||
Match, MatchParticipant, OrgTeam,
|
Match, MatchParticipant, OrgTeam,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons.
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import (
|
from app.models.models import (
|
||||||
Admin, Manager, Coach, Player, Scout,
|
Admin, Manager, Coach, Player, Scout,
|
||||||
User, Tryout, Match, MatchParticipant, Team, TeamMember,
|
User, Tryout, Match, MatchParticipant, Team, TeamMember,
|
||||||
OrgTeam, TryoutRegistration, PlayerDisponibility,
|
OrgTeam, TryoutRegistration, PlayerDisponibility,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons.
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import (
|
from app.models.models import (
|
||||||
Admin, Manager, Coach, Player,
|
Admin, Manager, Coach, Player,
|
||||||
OrgTeam, User, TeamMatch, TeamMatchParticipant, TeamPlayer,
|
OrgTeam, User, TeamMatch, TeamMatchParticipant, TeamPlayer,
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons.
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import (
|
from app.models.models import (
|
||||||
Admin, Manager, Coach, Player,
|
Admin, Manager, Coach, Player,
|
||||||
OrgTeam, User, Team, TeamMember,
|
OrgTeam, User, Team, TeamMember,
|
||||||
PersonalNote, TeamNote, Tryout, TeamPlayer,
|
PersonalNote, TeamNote, Tryout, TeamPlayer,
|
||||||
@@ -60,7 +60,7 @@ def my_teams():
|
|||||||
flash('This page is for players.', 'info')
|
flash('This page is for players.', 'info')
|
||||||
return redirect(url_for('teams.list_teams'))
|
return redirect(url_for('teams.list_teams'))
|
||||||
|
|
||||||
from app.models import TeamMatch, TeamMatchParticipant
|
from app.models.models import TeamMatch, TeamMatchParticipant
|
||||||
|
|
||||||
player_teams = current_user.get_org_teams()
|
player_teams = current_user.get_org_teams()
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons.
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import (
|
from app.models.models import (
|
||||||
Admin, Manager, Coach, Player, Scout,
|
Admin, Manager, Coach, Player, Scout,
|
||||||
User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember,
|
User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember,
|
||||||
OrgTeam, Match, MatchParticipant,
|
OrgTeam, Match, MatchParticipant,
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ import uuid
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from app.extensions import db, hash_password, csrf
|
from app.extensions import db, hash_password, csrf
|
||||||
from app.models import (
|
from app.models.models import (
|
||||||
Admin, Manager, Coach, Player, Scout,
|
Admin, Manager, Coach, Player, Scout,
|
||||||
User, USER_TYPES, ESPORT_GAMES,
|
User, USER_TYPES, ESPORT_GAMES,
|
||||||
PlayerDisponibility, UserGamertag, GAME_PLATFORMS,
|
PlayerDisponibility, UserGamertag, GAME_PLATFORMS,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Admin, Manager, Coach, Player, Scout.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from app.extensions import db, hash_password
|
from app.extensions import db, hash_password
|
||||||
from app.models import (
|
from app.models.models import (
|
||||||
Admin, Manager, Coach, Player, Scout,
|
Admin, Manager, Coach, Player, Scout,
|
||||||
Tryout, TryoutRegistration, Evaluation, Team, TeamMember,
|
Tryout, TryoutRegistration, Evaluation, Team, TeamMember,
|
||||||
OrgTeam, TeamPlayer,
|
OrgTeam, TeamPlayer,
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ Usage:
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
from marshmallow import Schema, fields, validate, ValidationError, pre_load, validates_schema, EXCLUDE
|
from marshmallow import Schema, fields, validate, ValidationError, pre_load, validates_schema, EXCLUDE
|
||||||
from app.models import USER_TYPES
|
from app.models.models import USER_TYPES
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user