Merge branch 'dev' of https://git.immortal.host/clubesportsudes/team-tryouts into dev
This commit is contained in:
@@ -20,6 +20,7 @@ from app.models._constants import (
|
||||
GAME_PLATFORMS,
|
||||
PLATFORM_CODES,
|
||||
TRN_URLS,
|
||||
EVALUATION_CRITERIA,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
@@ -95,4 +96,4 @@ from app.models.personal_note import PersonalNote
|
||||
from app.models.one_on_one_request import OneOnOneRequest
|
||||
from app.models.admin_settings import AppSettings
|
||||
from app.models.backup_record import BackupRecord
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.models.audit_log import AuditLog
|
||||
+40
-16
@@ -2,24 +2,48 @@
|
||||
|
||||
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_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),
|
||||
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
|
||||
),
|
||||
)
|
||||
|
||||
tryout_coaches = db.Table('tryout_coaches',
|
||||
db.Column('tryout_id', db.Integer, db.ForeignKey('tryouts.id', ondelete='CASCADE'),
|
||||
primary_key=True),
|
||||
db.Column('coach_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'),
|
||||
primary_key=True),
|
||||
tryout_coaches = db.Table(
|
||||
'tryout_coaches',
|
||||
db.Column(
|
||||
'tryout_id', db.Integer, db.ForeignKey('tryouts.id', ondelete='CASCADE'), primary_key=True
|
||||
),
|
||||
db.Column(
|
||||
'coach_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True
|
||||
),
|
||||
)
|
||||
|
||||
tryout_managers = db.Table(
|
||||
'tryout_managers',
|
||||
db.Column(
|
||||
'tryout_id', db.Integer, db.ForeignKey('tryouts.id', ondelete='CASCADE'), primary_key=True
|
||||
),
|
||||
db.Column(
|
||||
'manager_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True
|
||||
),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,21 @@ Contains game lists, position mappings, platform codes, and TRN URL templates.
|
||||
|
||||
USER_TYPES = ['admin', 'manager', 'coach', 'player', 'scout']
|
||||
|
||||
# Ordered list of (field_name, human_label) pairs for the player evaluation
|
||||
# score criteria. Kept in a single place so the evaluation forms, batch
|
||||
# evaluation page, and any future reporting all stay in sync.
|
||||
EVALUATION_CRITERIA = [
|
||||
('mecanics_score', 'Mecanics'),
|
||||
('cohesion_score', 'Cohesion'),
|
||||
('communication_score', 'Communication'),
|
||||
('gamesense_score', 'Gamesense'),
|
||||
('versatility_score', 'Versatility'),
|
||||
('discipline_score', 'Discipline'),
|
||||
('analysis_score', 'Analysis'),
|
||||
('sport_ethics_score', 'Sport Ethics'),
|
||||
('mental_score', 'Mental'),
|
||||
]
|
||||
|
||||
ESPORT_GAMES = [
|
||||
'Valorant',
|
||||
'League of Legends',
|
||||
@@ -52,11 +67,7 @@ PLATFORM_CODES = {
|
||||
'Epic': 'epic',
|
||||
}
|
||||
|
||||
PLATFORM_DEFAULTS = {
|
||||
'Apex Legends': 'pc',
|
||||
'Rainbow Six Siege': 'ubi',
|
||||
'Rocket League': 'epic'
|
||||
}
|
||||
PLATFORM_DEFAULTS = {'Apex Legends': 'pc', 'Rainbow Six Siege': 'ubi', 'Rocket League': 'epic'}
|
||||
|
||||
TRN_URLS = {
|
||||
'Valorant': 'https://tracker.gg/valorant/profile/riot/{username}',
|
||||
@@ -67,4 +78,4 @@ TRN_URLS = {
|
||||
'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}',
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -9,6 +9,21 @@ def load_user(user_id):
|
||||
|
||||
Returns the correct polymorphic subclass (Admin, Coach, Player, etc.)
|
||||
automatically because SQLAlchemy resolves the identity column.
|
||||
|
||||
Returns None for deactivated accounts so that disabling a user also
|
||||
invalidates the sessions they already hold. Flask-Login only consults
|
||||
is_active when login_user() is called, never when restoring a session
|
||||
from the cookie, so the check has to happen here.
|
||||
"""
|
||||
from app.extensions import db
|
||||
from app.models.user_model.user import User
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
try:
|
||||
pk = int(user_id)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
user = db.session.get(User, pk)
|
||||
if user is None or not user.is_active_account:
|
||||
return None
|
||||
return user
|
||||
|
||||
@@ -1,7 +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
|
||||
from app.models.availability.player_disponibility import PlayerDisponibility
|
||||
|
||||
__all__ = ['BaseAvailability', 'PlayerDisponibility', 'CoachAvailability']
|
||||
__all__ = ['BaseAvailability', 'PlayerDisponibility', 'CoachAvailability']
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
"""Abstract base class for availability models (PlayerDisponibility + CoachAvailability)."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"""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')
|
||||
coach = db.relationship('User', backref='coach_availabilities')
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"""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')
|
||||
player = db.relationship('User', backref='disponibilities')
|
||||
|
||||
+25
-9
@@ -1,10 +1,12 @@
|
||||
"""Contract documents for players to sign."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
@@ -20,7 +22,7 @@ class Contract(db.Model):
|
||||
|
||||
status = db.Column(db.String(20), default='pending')
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
uploaded_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
signed_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
player = db.relationship('User', foreign_keys=[player_id], backref='contracts')
|
||||
@@ -28,24 +30,38 @@ class Contract(db.Model):
|
||||
uploader = db.relationship('User', foreign_keys=[uploaded_by_id])
|
||||
|
||||
def can_view(self, user):
|
||||
"""Whether this user may read the contract and download its files.
|
||||
|
||||
Two defects used to sit in the coach branch:
|
||||
|
||||
- `not self.team_id` acted as a wildcard, so any coach listed in the
|
||||
legacy OrgTeam.coach_id column could read every contract with no
|
||||
team attached — and upload_contract leaves team_id null whenever
|
||||
the player belongs to no team.
|
||||
- the lookup went through OrgTeam.coach_id only, so a coach attached
|
||||
through the many-to-many relationship saw nothing at all.
|
||||
|
||||
Access now follows the same rule as everywhere else: the coach and
|
||||
the player must actually work together.
|
||||
"""
|
||||
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.manager import Manager
|
||||
from app.models.user_model.user import User
|
||||
from app.models.org_team.org_team import OrgTeam
|
||||
from app.permissions import coach_can_access_player
|
||||
|
||||
if isinstance(user, Admin):
|
||||
return True
|
||||
if isinstance(user, Manager):
|
||||
player = User.query.get(self.player_id)
|
||||
player = db.session.get(User, 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 coach_can_access_player(user, self.player_id)
|
||||
return False
|
||||
|
||||
def can_upload_signed(self, user):
|
||||
return user.id == self.player_id
|
||||
return user.id == self.player_id
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""Player evaluation record."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
@@ -22,9 +24,55 @@ class Evaluation(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('tryout_id', 'player_id', 'evaluator_id', name='unique_evaluation'),
|
||||
)
|
||||
)
|
||||
|
||||
#: The nine criteria, in the order the form shows them. The overall score
|
||||
#: is their mean; a criterion left blank is left out of the mean rather
|
||||
#: than counted as a zero, which is why this list exists rather than the
|
||||
#: route summing nine named variables (ARCH-005, QUA-003).
|
||||
CRITERIA = (
|
||||
'mecanics_score',
|
||||
'cohesion_score',
|
||||
'communication_score',
|
||||
'gamesense_score',
|
||||
'versatility_score',
|
||||
'discipline_score',
|
||||
'analysis_score',
|
||||
'sport_ethics_score',
|
||||
'mental_score',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def overall_from(cls, scores):
|
||||
"""Mean of the criteria that were actually filled in.
|
||||
|
||||
Args:
|
||||
scores: Mapping of criterion name to score or None.
|
||||
|
||||
Returns:
|
||||
float | None: None when nothing was scored — which is not the
|
||||
same as zero, and must not become one. A player nobody could
|
||||
assess has no overall score; a player who scored zero on
|
||||
everything cannot exist, the scale starts at one.
|
||||
"""
|
||||
given = [scores.get(name) for name in cls.CRITERIA]
|
||||
given = [score for score in given if score is not None]
|
||||
if not given:
|
||||
return None
|
||||
return sum(given) / len(given)
|
||||
|
||||
def apply_scores(self, scores):
|
||||
"""Write these criteria onto the record and recompute the overall.
|
||||
|
||||
Every criterion is assigned, including the ones left blank: an edit
|
||||
that clears a score has to clear it, and the mean has to be the mean
|
||||
of what is on the record afterwards.
|
||||
"""
|
||||
for name in self.CRITERIA:
|
||||
setattr(self, name, scores.get(name))
|
||||
self.overall_score = self.overall_from(scores)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""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']
|
||||
__all__ = ['BaseMatch', 'Match', 'TeamMatch']
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""Abstract base class for match models (Match + TeamMatch)."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
@@ -15,4 +17,4 @@ class BaseMatch(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""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)
|
||||
@@ -16,7 +18,13 @@ class Match(BaseMatch):
|
||||
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')
|
||||
# delete-orphan: without it, SQLAlchemy tries to detach participants by
|
||||
# setting match_id to NULL, which the NOT NULL column refuses — so
|
||||
# deleting any match that had participants raised IntegrityError.
|
||||
# TeamMatch.participants already declared this; Match did not.
|
||||
participants = db.relationship(
|
||||
'MatchParticipant', backref='match', lazy='dynamic', cascade='all, delete-orphan'
|
||||
)
|
||||
|
||||
def get_participating_players(self):
|
||||
return [p.player_id for p in self.participants.all()]
|
||||
return [p.player_id for p in self.participants.all()]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""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)
|
||||
@@ -13,10 +15,10 @@ class TeamMatch(BaseMatch):
|
||||
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')
|
||||
'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)
|
||||
return confirmed, len(all_p)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""Request from player to coach for a One on One session."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
@@ -15,11 +17,11 @@ class OneOnOneRequest(db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
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])
|
||||
team = db.relationship('OrgTeam', foreign_keys=[org_team_id])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Organisation team models."""
|
||||
|
||||
from app.models.org_team.org_team import OrgTeam
|
||||
from app.models.org_team.team_player import TeamPlayer
|
||||
|
||||
__all__ = ['OrgTeam', 'TeamPlayer']
|
||||
__all__ = ['OrgTeam', 'TeamPlayer']
|
||||
|
||||
@@ -1,36 +1,48 @@
|
||||
"""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
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
|
||||
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'))
|
||||
'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'))
|
||||
'User',
|
||||
secondary=org_team_managers,
|
||||
lazy='dynamic',
|
||||
backref=db.backref('managed_org_teams', lazy='dynamic'),
|
||||
)
|
||||
|
||||
coach = db.relationship(
|
||||
'User', foreign_keys=[coach_id],
|
||||
'User',
|
||||
foreign_keys=[coach_id],
|
||||
backref=db.backref('coached_org_team_legacy', uselist=False),
|
||||
viewonly=True)
|
||||
viewonly=True,
|
||||
)
|
||||
manager = db.relationship(
|
||||
'User', foreign_keys=[manager_id],
|
||||
'User',
|
||||
foreign_keys=[manager_id],
|
||||
backref=db.backref('managed_org_team_legacy', uselist=False),
|
||||
viewonly=True)
|
||||
viewonly=True,
|
||||
)
|
||||
|
||||
def get_coaches(self):
|
||||
coach_list = self.coaches.all()
|
||||
@@ -49,5 +61,7 @@ class OrgTeam(db.Model):
|
||||
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]
|
||||
return [
|
||||
{'player': tp.player, 'status': tp.status, 'position': tp.position}
|
||||
for tp in self.team_players
|
||||
]
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
"""Many-to-many junction: player to org-team."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
added_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
|
||||
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'),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""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']
|
||||
__all__ = ['BaseParticipant', 'MatchParticipant', 'TeamMatchParticipant']
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Abstract base class for match participant models."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
added_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""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)
|
||||
@@ -12,4 +14,4 @@ class MatchParticipant(BaseParticipant):
|
||||
position = db.Column(db.String(50), nullable=True)
|
||||
attendance_confirmed = db.Column(db.Boolean, default=False)
|
||||
|
||||
player = db.relationship('User')
|
||||
player = db.relationship('User')
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"""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')
|
||||
player = db.relationship('User')
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
"""Personal notes from coach to individual player."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=True)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
|
||||
@@ -21,4 +23,4 @@ class PersonalNote(db.Model):
|
||||
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])
|
||||
tryout = db.relationship('Tryout', foreign_keys=[tryout_id])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tryout-specific temporary team models."""
|
||||
|
||||
from app.models.team.team import Team
|
||||
from app.models.team.team_member import TeamMember
|
||||
|
||||
__all__ = ['Team', 'TeamMember']
|
||||
__all__ = ['Team', 'TeamMember']
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
"""Tryout-specific team (e.g. Alpha, Bravo within a single tryout)."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
|
||||
creator = db.relationship('User', backref='created_teams')
|
||||
members = db.relationship('TeamMember', backref='team', lazy='dynamic')
|
||||
members = db.relationship('TeamMember', backref='team', lazy='dynamic')
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"""Link between a player and a tryout-specific team."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
added_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
|
||||
player = db.relationship('User', overlaps="player_ref,team_assignments")
|
||||
player = db.relationship('User', overlaps="player_ref,team_assignments")
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
"""Team improvement notes from coach."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
team = db.relationship('OrgTeam', backref='team_notes')
|
||||
coach = db.relationship('User', foreign_keys=[coach_id])
|
||||
coach = db.relationship('User', foreign_keys=[coach_id])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tryout models."""
|
||||
|
||||
from app.models.tryout.tryout import Tryout
|
||||
from app.models.tryout.tryout_registration import TryoutRegistration
|
||||
|
||||
__all__ = ['Tryout', 'TryoutRegistration']
|
||||
__all__ = ['Tryout', 'TryoutRegistration']
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Tryout event for player evaluations and team formation."""
|
||||
|
||||
from app.extensions import db
|
||||
from app.models._associations import tryout_coaches
|
||||
from datetime import datetime
|
||||
from app.models._associations import tryout_coaches, tryout_managers
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
@@ -18,25 +20,45 @@ class Tryout(db.Model):
|
||||
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) # deprecated, kept for migration
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # deprecated, kept for migration
|
||||
coach_id = db.Column(
|
||||
db.Integer, db.ForeignKey('users.id'), nullable=True
|
||||
) # deprecated, kept for migration
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
|
||||
creator = db.relationship('User', foreign_keys=[created_by], backref='created_tryouts')
|
||||
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_tryouts')
|
||||
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_tryouts') # deprecated
|
||||
coach = db.relationship('User', foreign_keys=[coach_id], backref='_deprecated_coached_tryouts')
|
||||
coaches = db.relationship('User', secondary=tryout_coaches, backref='coached_tryouts')
|
||||
managers = db.relationship('User', secondary=tryout_managers, backref='managed_tryouts_m2m')
|
||||
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])
|
||||
target_org_team = db.relationship(
|
||||
'OrgTeam', backref='tryouts', foreign_keys=[target_org_team_id]
|
||||
)
|
||||
|
||||
@property
|
||||
def is_ended(self):
|
||||
"""Tryout is considered ended after its end_date passes.
|
||||
Falls back to date if end_date is not set."""
|
||||
from datetime import date as date_type
|
||||
|
||||
today = date_type.today()
|
||||
if self.end_date is not None:
|
||||
return self.end_date < today
|
||||
return self.date < today
|
||||
|
||||
def get_managers(self):
|
||||
"""Managers attached to this tryout, both legacy and many-to-many."""
|
||||
manager_list = list(self.managers)
|
||||
if not manager_list and self.manager:
|
||||
return [self.manager]
|
||||
return manager_list
|
||||
|
||||
def get_coaches(self):
|
||||
"""Coaches attached to this tryout, both legacy and many-to-many."""
|
||||
coach_list = list(self.coaches)
|
||||
if not coach_list and self.coach:
|
||||
return [self.coach]
|
||||
return coach_list
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
"""Registration linking a player to a tryout."""
|
||||
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
registered_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
status = db.Column(db.String(20), default='registered')
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Store gamertag per game for each user."""
|
||||
from app.extensions import db
|
||||
from app.models._constants import TRN_URLS, PLATFORM_CODES, PLATFORM_DEFAULTS
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
from app.extensions import db
|
||||
from app.models._constants import PLATFORM_CODES, PLATFORM_DEFAULTS, TRN_URLS
|
||||
|
||||
|
||||
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)
|
||||
@@ -15,9 +18,7 @@ class UserGamertag(db.Model):
|
||||
|
||||
user = db.relationship('User', backref='gamertags')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('user_id', 'game', name='unique_user_game'),
|
||||
)
|
||||
__table_args__ = (db.UniqueConstraint('user_id', 'game', name='unique_user_game'),)
|
||||
|
||||
def get_trn_url(self):
|
||||
if self.game not in TRN_URLS:
|
||||
@@ -34,11 +35,11 @@ class UserGamertag(db.Model):
|
||||
platform.lower().replace(' ', '-') if platform else '',
|
||||
)
|
||||
return url.format(platform_code=platform_code, username=encoded_gamertag)
|
||||
elif '{platform}' in url and '{username}' in url:
|
||||
if '{platform}' in url and '{username}' in url:
|
||||
return url.format(
|
||||
platform=platform.lower().replace(' ', '-') if platform else '',
|
||||
username=encoded_gamertag,
|
||||
)
|
||||
elif '{username}' in url:
|
||||
if '{username}' in url:
|
||||
return url.format(username=encoded_gamertag)
|
||||
return url
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""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.manager import Manager
|
||||
from app.models.user_model.player import Player
|
||||
from app.models.user_model.scout import Scout
|
||||
from app.models.user_model.user import User
|
||||
|
||||
__all__ = ['User', 'Admin', 'Manager', 'Coach', 'Player', 'Scout']
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""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):
|
||||
@@ -29,4 +31,5 @@ class Admin(User):
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
from app.models.tryout.tryout import Tryout
|
||||
|
||||
return Tryout.query.order_by(Tryout.date).all()
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
"""Coach — evaluates, schedules matches, manages their own org team."""
|
||||
"""Coach — evaluates, schedules matches, manages their own org teams."""
|
||||
|
||||
from app.models.user_model.user import User
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class Coach(User):
|
||||
"""Coach — evaluates, schedules matches, manages their own org team."""
|
||||
"""Coach — evaluates, schedules matches, manages their own org teams.
|
||||
|
||||
Every question about *which* teams or tryouts belong to this coach is
|
||||
delegated to ``app.permissions``. Two of the three methods below used to
|
||||
answer it themselves, each considering a different subset of the two
|
||||
ways a coach can be attached to a team: ``can_manage_this_tryout``
|
||||
ignored the legacy ``coach_id`` column when checking the target team,
|
||||
and ``get_visible_tryouts`` ignored it entirely. A coach attached only
|
||||
by that column therefore saw an empty calendar (ARCH-002).
|
||||
"""
|
||||
|
||||
__mapper_args__ = {'polymorphic_identity': 'coach'}
|
||||
|
||||
def can_evaluate(self):
|
||||
@@ -17,38 +27,16 @@ class Coach(User):
|
||||
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
|
||||
# Check many-to-many coaches relationship
|
||||
if any(c.id == self.id for c in tryout.coaches):
|
||||
return True
|
||||
# Backward compat: check deprecated coach_id
|
||||
if tryout.coach_id == self.id:
|
||||
return True
|
||||
return False
|
||||
from app.permissions import coach_manages_tryout
|
||||
|
||||
return coach_manages_tryout(self, tryout)
|
||||
|
||||
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
|
||||
return org_team.coach_id == self.id
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
from app.models.tryout.tryout import Tryout
|
||||
from app.models._associations import tryout_coaches
|
||||
team_ids = [t.id for t in self.coached_org_teams.all()]
|
||||
conditions = []
|
||||
if team_ids:
|
||||
conditions.append(Tryout.target_org_team_id.in_(team_ids))
|
||||
# Check many-to-many coaches
|
||||
conditions.append(Tryout.coaches.any(id=self.id))
|
||||
# Backward compat: check deprecated coach_id
|
||||
conditions.append(Tryout.coach_id == self.id)
|
||||
return Tryout.query.filter(db.or_(*conditions)).order_by(Tryout.date).all()
|
||||
from app.permissions import coach_tryouts
|
||||
|
||||
return coach_tryouts(self)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""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):
|
||||
@@ -19,14 +21,28 @@ class Manager(User):
|
||||
return True
|
||||
|
||||
def can_manage_this_tryout(self, tryout):
|
||||
return tryout.created_by == self.id or tryout.manager_id == self.id
|
||||
return (
|
||||
tryout.created_by == self.id
|
||||
or tryout.manager_id == self.id
|
||||
or any(m.id == self.id for m in tryout.managers)
|
||||
)
|
||||
|
||||
def can_manage_this_org_team(self, org_team):
|
||||
return True
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
from app.models.tryout.tryout import Tryout
|
||||
from sqlalchemy import or_
|
||||
return Tryout.query.filter(
|
||||
or_(Tryout.created_by == self.id, Tryout.manager_id == self.id)
|
||||
).order_by(Tryout.date).all()
|
||||
|
||||
from app.models.tryout.tryout import Tryout
|
||||
|
||||
return (
|
||||
Tryout.query.filter(
|
||||
or_(
|
||||
Tryout.created_by == self.id,
|
||||
Tryout.manager_id == self.id,
|
||||
Tryout.managers.any(id=self.id),
|
||||
)
|
||||
)
|
||||
.order_by(Tryout.date)
|
||||
.all()
|
||||
)
|
||||
|
||||
@@ -1,30 +1,44 @@
|
||||
"""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
|
||||
from app.models.tryout.tryout import Tryout
|
||||
|
||||
# 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 []
|
||||
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 []
|
||||
player_matches = (
|
||||
Match.query.join(MatchParticipant)
|
||||
.filter(
|
||||
MatchParticipant.player_id == self.id,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
extra_ids = {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]
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""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):
|
||||
@@ -11,4 +13,5 @@ class Scout(User):
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
from app.models.tryout.tryout import Tryout
|
||||
|
||||
return Tryout.query.order_by(Tryout.date).all()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Base User model — shared fields and polymorphic configuration."""
|
||||
from app.extensions import db
|
||||
|
||||
from flask_login import UserMixin
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
@@ -10,6 +12,7 @@ class User(UserMixin, db.Model):
|
||||
Do not instantiate this class directly; use Admin, Manager, Coach, Player,
|
||||
or Scout so that `polymorphic_identity` is set correctly.
|
||||
"""
|
||||
|
||||
__tablename__ = 'users'
|
||||
|
||||
# --- columns -----------------------------------------------------------
|
||||
@@ -21,13 +24,13 @@ class User(UserMixin, db.Model):
|
||||
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)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
|
||||
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)
|
||||
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)
|
||||
@@ -40,16 +43,26 @@ class User(UserMixin, db.Model):
|
||||
|
||||
# --- relationships (defined once on the base) --------------------------
|
||||
evaluations_given = db.relationship(
|
||||
'Evaluation', foreign_keys='Evaluation.evaluator_id',
|
||||
backref='evaluator', lazy='dynamic')
|
||||
'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')
|
||||
'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')
|
||||
'TeamMember', foreign_keys='TeamMember.player_id', backref='player_ref', lazy='dynamic'
|
||||
)
|
||||
|
||||
# --- Flask-Login integration -------------------------------------------
|
||||
@property
|
||||
def is_active(self):
|
||||
"""Whether Flask-Login should accept this account.
|
||||
|
||||
UserMixin returns True unconditionally, which meant a deactivated
|
||||
account kept any session it already held. Binding this to
|
||||
is_active_account makes deactivation take effect on the next request.
|
||||
"""
|
||||
return bool(self.is_active_account)
|
||||
|
||||
# --- shared helper methods ---------------------------------------------
|
||||
def get_games_list(self):
|
||||
@@ -60,8 +73,9 @@ class User(UserMixin, db.Model):
|
||||
|
||||
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}
|
||||
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."""
|
||||
|
||||
Reference in New Issue
Block a user