remodulation du projet et des classes

This commit is contained in:
cedrick2711
2026-07-28 23:33:31 -04:00
parent 90ac3eb804
commit 3feae80767
94 changed files with 2644 additions and 4366 deletions
-1
View File
@@ -1 +0,0 @@
1. Ajouter l'option d'enlever des joueurs dans les tryouts.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
# app package
+15 -23
View File
@@ -7,7 +7,7 @@ the Flask application instance with comprehensive security hardening.
import os
from flask import Flask, request, redirect, jsonify, render_template, url_for
from flask_cors import CORS
from extensions import db, login_manager, csrf, hash_password, check_password, limiter
from app.extensions import db, login_manager, csrf, hash_password, check_password, limiter
from sqlalchemy import text
from werkzeug.exceptions import HTTPException
import markupsafe
@@ -96,17 +96,17 @@ def create_app():
limiter.init_app(app)
# Configure structured logging
from logging_config import configure_logging
from app.logging_config import configure_logging
configure_logging(app)
from routes.auth import auth_bp
from routes.tryouts import tryouts_bp
from routes.evaluations import evaluations_bp
from routes.users import users_bp
from routes.main import main_bp
from routes.teams import teams_bp
from routes.matches import matches_bp
from routes.team_matches import team_matches_bp
from app.routes.auth import auth_bp
from app.routes.tryouts import tryouts_bp
from app.routes.evaluations import evaluations_bp
from app.routes.users import users_bp
from app.routes.main import main_bp
from app.routes.teams import teams_bp
from app.routes.matches import matches_bp
from app.routes.team_matches import team_matches_bp
app.register_blueprint(auth_bp)
app.register_blueprint(tryouts_bp)
@@ -342,26 +342,18 @@ def create_app():
# Database Initialization
# =========================================================================
with app.app_context():
import models
from models import User
try:
# Check if the database schema is up to date by testing a query
db.session.execute(text('SELECT games, team_side FROM match_participants LIMIT 1'))
db.create_all()
except Exception:
# If there's a schema mismatch, drop and recreate all tables
db.session.rollback()
db.drop_all()
db.create_all()
import app.models as models # noqa: F401 — registers all models with SQLAlchemy
from app.models import User
db.create_all()
# Seed database if empty
if User.query.count() == 0:
from seed import seed_database
from app.supporting_scrits.seed import seed_database
seed_database()
# Start the Discord bot for notifications
try:
from discord_bot import start_bot
from app.discord_bot import start_bot
start_bot()
except Exception as e:
app.logger.warning('Could not start Discord bot: %s', e)
+6 -6
View File
@@ -188,7 +188,7 @@ class TeamTryoutsBot(commands.Bot):
"""
try:
# Look up the DB user to get their Discord user ID
from models import User as DBUser
from app.models import User as DBUser
db_user = DBUser.query.get(user_id)
if not db_user:
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):
"""Handle coach approving a One on One request."""
try:
from models import OneOnOneRequest, db
from app.models import OneOnOneRequest, db
from sqlalchemy.orm import joinedload
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):
"""Handle coach rejecting a One on One request."""
try:
from models import OneOnOneRequest, db
from app.models import OneOnOneRequest, db
from sqlalchemy.orm import joinedload
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):
"""Handle player confirming attendance for a match/tryout."""
try:
from models import MatchParticipant, TryoutRegistration, Match, Tryout, db
from app.models import MatchParticipant, TryoutRegistration, Match, Tryout, db
request_info = self.pending_requests[message_id]
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):
"""Handle player declining attendance for a match/tryout."""
try:
from models import MatchParticipant, TryoutRegistration, Match, Tryout, db
from app.models import MatchParticipant, TryoutRegistration, Match, Tryout, db
request_info = self.pending_requests[message_id]
event_type = request_info.get('event_type')
@@ -471,7 +471,7 @@ class TeamTryoutsBot(commands.Bot):
async def send_daily_reminders(self):
"""Send daily reminders at 18:00 EDT for events in 24-48 hours."""
try:
from models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest, db
from app.models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest, db
from sqlalchemy.orm import joinedload
now = datetime.now(self.timezone)
View File
+854
View File
@@ -0,0 +1,854 @@
"""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])
View File
+5 -5
View File
@@ -9,9 +9,9 @@ import uuid
from datetime import datetime, timedelta
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 extensions import db, hash_password, check_password, limiter
from models import User, ESPORT_GAMES
from validators import RegisterSchema, LoginSchema
from app.extensions import db, hash_password, check_password, limiter
from app.models import User, Player, ESPORT_GAMES
from app.validators import RegisterSchema, LoginSchema
from marshmallow import ValidationError
from urllib.parse import urlparse
@@ -246,7 +246,7 @@ def register():
)
hashed_password = hash_password(password)
user = User(
user = Player(
username=username,
password_hash=hashed_password,
role='player',
@@ -255,7 +255,7 @@ def register():
phone=phone,
games=','.join(selected_games) if selected_games else None,
discord_username=discord_username,
league_os_profile=league_os_profile
league_os_profile=league_os_profile,
)
db.session.add(user)
db.session.commit()
@@ -1,12 +1,16 @@
"""Evaluation routes for assessing player performance during tryouts.
This module handles player evaluation creation, management, and viewing.
Uses polymorphic isinstance checks instead of role-string comparisons.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from extensions import db
from models import User, Tryout, Evaluation, TryoutRegistration, GAME_POSITIONS, OrgTeam
from app.extensions import db
from app.models import (
Admin, Coach, Manager, Player,
User, Tryout, Evaluation, TryoutRegistration,
OrgTeam, GAME_POSITIONS,
)
from sqlalchemy import func
from sqlalchemy.orm import aliased
@@ -14,14 +18,7 @@ evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
def validate_score(score_value):
"""Validate that a score is between 1 and 10.
Args:
score_value: The score value to validate (can be None, string, or int).
Returns:
int or None: The validated score or None if invalid/empty.
"""
"""Validate that a score is between 1 and 10."""
if score_value is None:
return None
try:
@@ -36,32 +33,18 @@ def validate_score(score_value):
@evaluations_bp.route('')
@login_required
def list_evaluations():
"""List all evaluations accessible to the current user.
President: All evaluations with player score summaries.
Evaluators (coach/manager): Their given evaluations.
Supports sorting by any column header via 'sort' and 'order' query parameters.
Returns:
Response: Rendered evaluations list template.
"""
"""List all evaluations accessible to the current user."""
user = current_user
# Players are not allowed to view evaluations
if user.role == 'player':
if isinstance(user, Player):
flash('You do not have permission to view evaluations.', 'danger')
return redirect(url_for('main.dashboard'))
# Get sort parameters
sort_column = request.args.get('sort', 'created_at')
sort_order = request.args.get('order', 'desc')
# Validate sort_order
if sort_order not in ('asc', 'desc'):
sort_order = 'desc'
# Map sort columns to SQLAlchemy expressions using aliased User models for relationship sorting
player_alias = aliased(User, name='eval_player')
evaluator_alias = aliased(User, name='eval_evaluator')
@@ -89,7 +72,7 @@ def list_evaluations():
else:
sort_expr = sort_expr.desc()
if user.role == 'admin':
if isinstance(user, Admin):
evaluations = Evaluation.query \
.outerjoin(Tryout, Evaluation.tryout_id == Tryout.id) \
.outerjoin(player_alias, Evaluation.player_id == player_alias.id) \
@@ -98,14 +81,16 @@ def list_evaluations():
avg_scores = db.session.query(
Evaluation.player_id,
func.count(Evaluation.id).label('eval_count'),
func.avg(Evaluation.overall_score).label('avg_score')
func.avg(Evaluation.overall_score).label('avg_score'),
).group_by(Evaluation.player_id).all()
player_scores = {}
for row in avg_scores:
p = User.query.get(row.player_id)
if p:
player_scores[p.id] = {'player': p, 'count': row.eval_count, 'avg': round(row.avg_score, 1) if row.avg_score else 0}
player_scores[p.id] = {
'player': p, 'count': row.eval_count,
'avg': round(row.avg_score, 1) if row.avg_score else 0,
}
elif user.can_evaluate():
evaluations = Evaluation.query \
.outerjoin(Tryout, Evaluation.tryout_id == Tryout.id) \
@@ -123,53 +108,38 @@ def list_evaluations():
.order_by(sort_expr).all()
player_scores = {}
return render_template('pages/evaluations.html', evaluations=evaluations, player_scores=player_scores, sort_column=sort_column, sort_order=sort_order)
return render_template('pages/evaluations.html',
evaluations=evaluations, player_scores=player_scores,
sort_column=sort_column, sort_order=sort_order)
@evaluations_bp.route('/<int:tryout_id>/<int:player_id>', methods=['GET', 'POST'])
@login_required
def evaluate_player(tryout_id, player_id):
"""Evaluate a specific player in a tryout.
GET: Render the evaluation form with any existing evaluation.
POST: Create or update the evaluation for the player.
Args:
tryout_id: The ID of the tryout.
player_id: The ID of the player to evaluate.
Returns:
Response: Evaluation form or redirect to tryout view.
"""
"""Evaluate a specific player in a tryout."""
if not current_user.can_evaluate():
flash('You do not have permission to evaluate players.', 'danger')
return redirect(url_for('main.dashboard'))
tryout = Tryout.query.get_or_404(tryout_id)
# Check if user has permission to evaluate players in this tryout
if not current_user.can_manage_this_tryout(tryout):
flash('You do not have permission to evaluate players in this tryout.', 'danger')
return redirect(url_for('tryouts.list_tryouts'))
# Check if player is registered for this tryout
is_registered = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=player_id
tryout_id=tryout_id, player_id=player_id,
).first() is not None
if not is_registered:
flash('Player is not registered for this tryout.', 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
player = User.query.get_or_404(player_id)
if player.role != 'player':
if not isinstance(player, Player):
flash('Can only evaluate players.', 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
existing_eval = Evaluation.query.filter_by(
tryout_id=tryout_id,
player_id=player_id,
evaluator_id=current_user.id
tryout_id=tryout_id, player_id=player_id, evaluator_id=current_user.id,
).first()
if request.method == 'POST':
@@ -185,7 +155,9 @@ def evaluate_player(tryout_id, player_id):
comments = request.form.get('comments')
position = request.form.get('position_recommendation')
scores = [s for s in [mecanics, cohesion, communication, gamesense, versatility, discipline, analysis, sport_ethics, mental] if s is not None]
scores = [s for s in [mecanics, cohesion, communication, gamesense,
versatility, discipline, analysis, sport_ethics, mental]
if s is not None]
overall = sum(scores) / len(scores) if scores else None
if existing_eval:
@@ -204,21 +176,14 @@ def evaluate_player(tryout_id, player_id):
flash('Evaluation updated!', 'success')
else:
evaluation = Evaluation(
tryout_id=tryout_id,
player_id=player_id,
tryout_id=tryout_id, player_id=player_id,
evaluator_id=current_user.id,
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,
overall_score=overall,
comments=comments,
position_recommendation=position
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, overall_score=overall,
comments=comments, position_recommendation=position,
)
db.session.add(evaluation)
flash('Evaluation submitted successfully!', 'success')
@@ -227,53 +192,42 @@ def evaluate_player(tryout_id, player_id):
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
evaluators = None
if current_user.role == 'admin':
all_evaluations = Evaluation.query.filter_by(tryout_id=tryout_id, player_id=player_id).all()
evaluators = [{'evaluator': User.query.get(e.evaluator_id), 'eval': e} for e in all_evaluations]
if isinstance(current_user, Admin):
all_evaluations = Evaluation.query.filter_by(
tryout_id=tryout_id, player_id=player_id,
).all()
evaluators = [{'evaluator': User.query.get(e.evaluator_id), 'eval': e}
for e in all_evaluations]
return render_template('pages/evaluate_player.html',
tryout=tryout,
player=player,
existing_eval=existing_eval,
evaluators=evaluators,
game_positions=GAME_POSITIONS)
tryout=tryout, player=player,
existing_eval=existing_eval,
evaluators=evaluators,
game_positions=GAME_POSITIONS)
@evaluations_bp.route('/<int:tryout_id>/players')
@login_required
def players_to_evaluate(tryout_id):
"""List players that need evaluation in a specific tryout.
Shows all registered players and marks which ones have already been evaluated
by the current user.
Args:
tryout_id: The ID of the tryout.
Returns:
Response: Rendered players-to-evaluate template.
"""
"""List players that need evaluation in a specific tryout."""
if not current_user.can_evaluate():
flash('Permission denied.', 'danger')
return redirect(url_for('main.dashboard'))
tryout = Tryout.query.get_or_404(tryout_id)
# Check if user has permission to evaluate players in this tryout
if not current_user.can_manage_this_tryout(tryout):
flash('You do not have permission to evaluate players in this tryout.', 'danger')
return redirect(url_for('tryouts.list_tryouts'))
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
players = []
for reg in registrations:
p = User.query.get(reg.player_id)
if p and p.role == 'player':
if p and isinstance(p, Player):
existing = Evaluation.query.filter_by(
tryout_id=tryout_id,
player_id=p.id,
evaluator_id=current_user.id
tryout_id=tryout_id, player_id=p.id, evaluator_id=current_user.id,
).first()
players.append({'player': p, 'evaluated': existing is not None, 'registration': reg})
players.append({'player': p, 'evaluated': existing is not None,
'registration': reg})
return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players)
return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players)
+46 -65
View File
@@ -1,27 +1,25 @@
"""Main dashboard routes for the Team Tryouts application.
This module provides the main dashboard view with role-specific statistics.
Uses polymorphic isinstance checks instead of role-string comparisons.
"""
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_required, current_user
from extensions import db
from models import User, Tryout, Evaluation, TryoutRegistration, Team, TeamMember, Match, MatchParticipant, OrgTeam
from app.extensions import db
from app.models import (
Admin, Manager, Coach, Player, Scout,
User, Tryout, Evaluation, TryoutRegistration, Team, TeamMember,
Match, MatchParticipant, OrgTeam,
)
from sqlalchemy import func
from datetime import datetime, date
from datetime import date
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def index():
"""Redirect root URL to login page.
This is the entry point for the application when no specific route is provided.
Returns:
Response: Redirect to login page.
"""
"""Redirect root URL to login page."""
return redirect(url_for('auth.login'))
@@ -29,21 +27,13 @@ def index():
@login_required
def dashboard():
"""Render the main dashboard with role-specific statistics.
Displays different statistics based on the user's role:
- President: Overview of all users, tryouts, and evaluations
- Manager: Their created tryouts and evaluations
- Coach: Their evaluations and pending evaluations
- Player: Their registrations, evaluations, and upcoming matches
- Scout: Top-rated players across all tryouts
Returns:
Response: Rendered dashboard template with user stats.
Each User subclass provides its own stats view.
"""
user = current_user
stats = {}
if user.role == 'admin':
if isinstance(user, Admin):
stats['total_users'] = User.query.count()
stats['total_players'] = User.query.filter_by(role='player').count()
stats['total_tryouts'] = Tryout.query.count()
@@ -54,101 +44,92 @@ def dashboard():
stats['recent_tryouts'] = Tryout.query.order_by(Tryout.created_at.desc()).limit(10).all()
today = date.today()
stats['upcoming_matches'] = Match.query.filter(
Match.status == 'scheduled',
Match.date >= today
Match.status == 'scheduled', Match.date >= today,
).order_by(Match.date, Match.start_time).limit(5).all()
elif user.role == 'manager':
elif isinstance(user, Manager):
stats['total_tryouts'] = Tryout.query.filter_by(created_by=user.id).count()
stats['active_tryouts'] = Tryout.query.filter_by(created_by=user.id, status='in_progress').count()
stats['active_tryouts'] = Tryout.query.filter_by(
created_by=user.id, status='in_progress').count()
stats['total_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).count()
stats['my_tryouts'] = Tryout.query.filter_by(created_by=user.id).order_by(Tryout.date.desc()).limit(5).all()
stats['my_tryouts'] = Tryout.query.filter_by(
created_by=user.id).order_by(Tryout.date.desc()).limit(5).all()
today = date.today()
manager_tryout_ids = [t.id for t in Tryout.query.filter_by(created_by=user.id).all()]
stats['upcoming_matches'] = Match.query.filter(
Match.tryout_id.in_(manager_tryout_ids),
Match.status == 'scheduled',
Match.date >= today
Match.status == 'scheduled', Match.date >= today,
).order_by(Match.date, Match.start_time).limit(5).all() if manager_tryout_ids else []
elif user.role == 'coach':
elif isinstance(user, Coach):
stats['my_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).count()
stats['pending_evaluations'] = 0
registrations = TryoutRegistration.query.filter(TryoutRegistration.status.in_(['registered', 'attended'])).all()
registrations = TryoutRegistration.query.filter(
TryoutRegistration.status.in_(['registered', 'attended'])).all()
registered_player_ids = [r.player_id for r in registrations]
evaluated_player_ids = [e.player_id for e in Evaluation.query.filter_by(evaluator_id=user.id).all()]
stats['pending_evaluations'] = len(set(registered_player_ids) - set(evaluated_player_ids))
stats['my_recent_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).order_by(Evaluation.created_at.desc()).limit(10).all()
stats['my_recent_evaluations'] = Evaluation.query.filter_by(
evaluator_id=user.id).order_by(Evaluation.created_at.desc()).limit(10).all()
today = date.today()
org_team = OrgTeam.query.filter_by(coach_id=user.id).first()
coach_tryout_ids = [t.id for t in Tryout.query.filter_by(target_org_team_id=org_team.id).all()] if org_team else []
coach_tryout_ids = [t.id for t in Tryout.query.filter_by(
target_org_team_id=org_team.id).all()] if org_team else []
stats['upcoming_matches'] = Match.query.filter(
Match.tryout_id.in_(coach_tryout_ids),
Match.status == 'scheduled',
Match.date >= today
Match.status == 'scheduled', Match.date >= today,
).order_by(Match.date, Match.start_time).limit(5).all() if coach_tryout_ids else []
elif user.role == 'player':
elif isinstance(user, Player):
stats['my_tryouts'] = TryoutRegistration.query.filter_by(player_id=user.id).count()
stats['my_registrations'] = TryoutRegistration.query.filter_by(player_id=user.id).order_by(TryoutRegistration.registered_at.desc()).limit(5).all()
# Get upcoming matches for the player
stats['my_registrations'] = TryoutRegistration.query.filter_by(
player_id=user.id).order_by(TryoutRegistration.registered_at.desc()).limit(5).all()
today = date.today()
next_matches = []
# Get all tryouts the player is registered for (not just the 5 most recent)
all_registrations = TryoutRegistration.query.filter_by(player_id=user.id).all()
registered_tryout_ids = [r.tryout_id for r in all_registrations]
# Get all matches where player is a participant (any match type)
player_participant_matches = MatchParticipant.query.filter_by(player_id=user.id).all()
player_match_ids = [p.match_id for p in player_participant_matches]
# Get all team memberships for this player
player_team_memberships = TeamMember.query.filter_by(player_id=user.id).all()
player_team_ids = [tm.team_id for tm in player_team_memberships]
# Find all scheduled matches in registered tryouts
upcoming_matches = Match.query.filter(
Match.tryout_id.in_(registered_tryout_ids),
Match.status == 'scheduled',
Match.date >= today
Match.status == 'scheduled', Match.date >= today,
).order_by(Match.date, Match.start_time).all()
for match in upcoming_matches:
is_participant = False
team = None
if match.match_type == 'team_vs_team':
# Check if player is on either team
if match.team1_id in player_team_ids:
is_participant = True
team = next((tm for tm in player_team_memberships if tm.team_id == match.team1_id), None)
team = next((tm for tm in player_team_memberships
if tm.team_id == match.team1_id), None)
elif match.team2_id in player_team_ids:
is_participant = True
team = next((tm for tm in player_team_memberships if tm.team_id == match.team2_id), None)
team = next((tm for tm in player_team_memberships
if tm.team_id == match.team2_id), None)
else:
# For player_vs_player and player_scrim, check MatchParticipant
if match.id in player_match_ids:
is_participant = True
if is_participant:
tryout = match.tryout
next_matches.append({
'tryout': tryout,
'match': match,
'team': team.team if team else None
'tryout': match.tryout, 'match': match,
'team': team.team if team else None,
})
stats['next_matches'] = next_matches
elif user.role == 'scout':
elif isinstance(user, Scout):
stats['total_players'] = User.query.filter_by(role='player').count()
stats['total_evaluations'] = Evaluation.query.count()
stats['avg_scores'] = db.session.query(
Evaluation.player_id,
func.avg(Evaluation.overall_score).label('avg_score')
).group_by(Evaluation.player_id).order_by(func.avg(Evaluation.overall_score).desc()).limit(5).all()
func.avg(Evaluation.overall_score).label('avg_score'),
).group_by(Evaluation.player_id).order_by(
func.avg(Evaluation.overall_score).desc()).limit(5).all()
stats['top_players'] = []
for row in stats['avg_scores']:
p = User.query.get(row.player_id)
+159 -415
View File
@@ -1,73 +1,65 @@
"""Match scheduling routes for managing scrimmages and matches within tryouts.
This module handles calendar views, match creation, and player availability.
Uses polymorphic isinstance checks instead of role-string comparisons.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user
from extensions import db
from models import User, Tryout, Match, MatchParticipant, Team, TeamMember, OrgTeam, TryoutRegistration, PlayerDisponibility
from app.extensions import db
from app.models import (
Admin, Manager, Coach, Player, Scout,
User, Tryout, Match, MatchParticipant, Team, TeamMember,
OrgTeam, TryoutRegistration, PlayerDisponibility,
)
from datetime import datetime, time, timedelta
from discord_bot import send_schedule_notification
from app.discord_bot import send_schedule_notification
matches_bp = Blueprint('matches', __name__, url_prefix='/matches')
def can_schedule_match():
"""Check if user can schedule matches (coaches and above).
Returns:
bool: True if user is president, manager, coach, or scout.
"""Check if user can schedule matches (Admin, Manager, Coach, Scout)."""
return isinstance(current_user, (Admin, Manager, Coach, Scout))
def get_visible_tryouts_for_user():
"""Get tryouts that the current user can see based on their role.
Delegates to the polymorphic User subclass.
"""
return current_user.role in ['admin', 'manager', 'coach', 'scout']
return current_user.get_visible_tryouts()
@matches_bp.route('/calendar')
@login_required
def calendar():
"""Render the calendar view showing all tryouts and matches.
Returns:
Response: Rendered calendar template.
"""
"""Render the calendar view."""
return render_template('pages/calendar.html')
@matches_bp.route('/api/events')
@login_required
def api_events():
"""API endpoint returning calendar events for FullCalendar.
Returns tryout events and match events with participant information.
Returns:
Response: JSON array of calendar events.
"""
"""API endpoint returning calendar events for FullCalendar."""
events = []
# Get tryouts based on user permissions (this already filters by user's role)
tryouts = get_visible_tryouts_for_user()
for tryout in tryouts:
events.append({
'id': f'tryout_{tryout.id}',
'title': tryout.title,
'date': tryout.date.strftime('%Y-%m-%d'),
'type': 'tryout',
'color': '#3b82f6', # Blue for tryouts
'type': 'tryout', 'color': '#3b82f6',
'extendedProps': {
'location': tryout.location or 'TBD',
'status': tryout.status,
'description': tryout.description or '',
'tryout_id': tryout.id
}
'tryout_id': tryout.id,
},
})
# Add matches for this tryout
for match in tryout.matches:
match_color = '#10b981' if match.match_type == 'team_vs_team' else '#f59e0b'
# Build match description with participants
match_desc = match.description or ''
participants_str = ''
if match.match_type == 'team_vs_team':
@@ -79,108 +71,76 @@ def api_events():
participants_str = f"{' vs '.join(teams)}"
match_desc = participants_str + (f"<br>{match.description}" if match.description else '')
else:
# Player scrim - show all participants
player_names = []
for p in match.participants.all():
player_name = p.player.username if p.player else 'Unknown Player'
player_names.append(player_name)
player_names.append(p.player.username if p.player else 'Unknown Player')
participants_str = ', '.join(player_names) if player_names else 'No players'
match_desc = participants_str + (f"<br>{match.description}" if match.description else '')
# Include time for calendar display
start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None
end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None
# Find current user's participant record for presence toggle
user_participant = MatchParticipant.query.filter_by(
match_id=match.id,
player_id=current_user.id
match_id=match.id, player_id=current_user.id,
).first()
events.append({
'id': f'match_{match.id}',
'title': match.title,
'date': match.date.strftime('%Y-%m-%d'),
'type': 'match',
'color': match_color,
'type': 'match', 'color': match_color,
'extendedProps': {
'location': match.location or tryout.location or 'TBD',
'status': match.status,
'description': match_desc,
'status': match.status, 'description': match_desc,
'match_type': match.match_type,
'tryout_id': tryout.id,
'match_id': match.id,
'start_time': start_time_str,
'end_time': end_time_str,
'tryout_id': tryout.id, 'match_id': match.id,
'start_time': start_time_str, 'end_time': end_time_str,
'participants': participants_str,
'user_participant_id': user_participant.id if user_participant else None,
'user_attendance_confirmed': user_participant.attendance_confirmed if user_participant else False
}
'user_attendance_confirmed': user_participant.attendance_confirmed if user_participant else False,
},
})
return jsonify(events)
@matches_bp.route('/api/events/<int:tryout_id>')
@login_required
def api_events_for_tryout(tryout_id):
"""API endpoint returning calendar events for a specific tryout.
Args:
tryout_id: The ID of the tryout to get events for.
Returns:
Response: JSON array of calendar events for the tryout.
"""
"""API endpoint returning calendar events for a specific tryout."""
tryout = Tryout.query.get_or_404(tryout_id)
# Check if user can view this tryout
can_view = current_user.can_manage_this_tryout(tryout)
# For players, check if they're registered or participating in a match
is_registered = False
player_in_match = False
if current_user.role == 'player':
if isinstance(current_user, Player):
is_registered = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=current_user.id
tryout_id=tryout_id, player_id=current_user.id,
).first() is not None
# Check if player is participating in any matches for this tryout
player_matches = Match.query.join(MatchParticipant).filter(
MatchParticipant.player_id == current_user.id,
Match.tryout_id == tryout_id
Match.tryout_id == tryout_id,
).all()
player_in_match = len(player_matches) > 0
# Non-participating players cannot see the calendar
if not can_view and not is_registered and not player_in_match:
return jsonify([])
events = []
# Add tryout date as an event (read-only, for context)
events.append({
events = [{
'id': f'tryout_{tryout.id}',
'title': f'Tryout: {tryout.title}',
'date': tryout.date.strftime('%Y-%m-%d'),
'type': 'tryout',
'color': '#3b82f6', # Blue for tryouts
'type': 'tryout', 'color': '#3b82f6',
'extendedProps': {
'location': tryout.location or 'TBD',
'status': tryout.status,
'description': tryout.description or '',
'tryout_id': tryout.id
}
})
'tryout_id': tryout.id,
},
}]
for match in tryout.matches:
# Determine color based on match type
if match.match_type == 'team_vs_team' or match.match_type == 'player_vs_player':
match_color = '#10b981' # Green for team matches
else:
match_color = '#f59e0b' # Orange for scrims
# Build participant string with proper grouping
match_color = '#10b981' if match.match_type in ('team_vs_team', 'player_vs_player') else '#f59e0b'
participants_str = ''
if match.match_type == 'team_vs_team':
teams = []
@@ -190,124 +150,51 @@ def api_events_for_tryout(tryout_id):
teams.append(match.team2.name)
participants_str = f"{' vs '.join(teams)}"
elif match.match_type == 'player_vs_player':
# Get players grouped by team side
team1_players = []
for p in match.participants.filter_by(team_side=1).all():
if p.player:
team1_players.append(p.player.username)
team2_players = []
for p in match.participants.filter_by(team_side=2).all():
if p.player:
team2_players.append(p.player.username)
team1_players = [p.player.username for p in match.participants.filter_by(team_side=1).all() if p.player]
team2_players = [p.player.username for p in match.participants.filter_by(team_side=2).all() if p.player]
if team1_players and team2_players:
participants_str = f"{', '.join(team1_players)} vs {', '.join(team2_players)}"
else:
participants_str = 'TBD vs TBD'
else:
player_names = []
for p in match.participants.all():
player_name = p.player.username if p.player else 'Unknown Player'
player_names.append(player_name)
player_names = [p.player.username for p in match.participants.all() if p.player]
participants_str = ', '.join(player_names) if player_names else 'No players'
# Include time for calendar display
start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None
end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None
events.append({
'id': f'match_{match.id}',
'title': match.title,
'date': match.date.strftime('%Y-%m-%d'),
'type': 'match',
'color': match_color,
'type': 'match', 'color': match_color,
'extendedProps': {
'location': match.location or tryout.location or 'TBD',
'status': match.status,
'match_type': match.match_type,
'tryout_id': tryout.id,
'match_id': match.id,
'status': match.status, 'match_type': match.match_type,
'tryout_id': tryout.id, 'match_id': match.id,
'participants': participants_str,
'start_time': start_time_str,
'end_time': end_time_str
}
'start_time': start_time_str, 'end_time': end_time_str,
},
})
return jsonify(events)
def get_visible_tryouts_for_user():
"""Get tryouts that the current user can see based on their role.
Permission hierarchy:
- President: All tryouts
- Manager: Only their created tryouts
- Coach: Tryouts targeting their org team
- Player: Tryouts they're registered for or matches they're in
- Scout: All tryouts
Returns:
list: Query result of Tryout objects.
"""
if current_user.role == 'admin':
return Tryout.query.order_by(Tryout.date).all()
elif current_user.role == 'manager':
return Tryout.query.filter_by(created_by=current_user.id).order_by(Tryout.date).all()
elif current_user.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if org_team:
return Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date).all()
return []
elif current_user.role == 'player':
# Get tryouts player is registered for
player_tryout_ids = [r.tryout_id for r in current_user.tryout_registrations.all()]
tryouts = Tryout.query.filter(Tryout.id.in_(player_tryout_ids)).order_by(Tryout.date).all() if player_tryout_ids else []
# Also include matches where player is participating
player_matches = Match.query.join(MatchParticipant).filter(
MatchParticipant.player_id == current_user.id
).all()
player_match_tryout_ids = list(set(m.tryout_id for m in player_matches))
additional_tryouts = Tryout.query.filter(
Tryout.id.in_(player_match_tryout_ids)
).order_by(Tryout.date).all() if player_match_tryout_ids else []
# Combine and deduplicate
all_tryouts = tryouts + [t for t in additional_tryouts if t.id not in player_tryout_ids]
return all_tryouts
else: # scout
return Tryout.query.order_by(Tryout.date).all()
@matches_bp.route('/create/<int:tryout_id>', methods=['GET', 'POST'])
@login_required
def create_match(tryout_id):
"""Create a new match/scrimmage within a tryout.
GET: Render the match creation form.
POST: Create a match with submitted details.
Args:
tryout_id: The ID of the tryout to create the match for.
Returns:
Response: Create form or redirect to tryout view.
"""
"""Create a new match / scrimmage within a tryout."""
tryout = Tryout.query.get_or_404(tryout_id)
# Check if user can manage this tryout (president, manager, or coach)
if not current_user.can_manage_this_tryout(tryout):
flash('You do not have permission to schedule matches for this tryout.', 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
teams = Team.query.filter_by(tryout_id=tryout_id).all()
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
all_players = [User.query.get(r.player_id) for r in registrations if User.query.get(r.player_id)]
all_players = sorted([p for p in all_players if p], key=lambda x: x.username)
# Allow pre-filling the date from query param (e.g., from calendar click)
prefill_date = request.args.get('date', '')
if request.method == 'POST':
title = request.form.get('title')
description = request.form.get('description')
@@ -316,59 +203,49 @@ def create_match(tryout_id):
end_time_str = request.form.get('end_time')
location = request.form.get('location')
match_type = request.form.get('match_type')
# Start time is now mandatory
if not start_time_str:
flash('Start time is required. Please select a time slot.', 'danger')
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players, prefill_date=prefill_date)
return render_template('pages/match_form.html', tryout=tryout, teams=teams,
all_players=all_players, prefill_date=prefill_date)
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else tryout.date
except (ValueError, TypeError):
flash('Invalid date format.', 'danger')
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players, prefill_date=prefill_date)
return render_template('pages/match_form.html', tryout=tryout, teams=teams,
all_players=all_players, prefill_date=prefill_date)
start_time = None
end_time = None
try:
start_time = datetime.strptime(start_time_str, '%H:%M').time()
# Auto-calculate end time if not provided (start + 30 minutes)
if end_time_str:
end_time = datetime.strptime(end_time_str, '%H:%M').time()
else:
# Auto-calculate end time as start + 30 minutes
start_dt = datetime.combine(date_obj, start_time)
end_dt = start_dt + timedelta(minutes=30)
end_time = end_dt.time()
except ValueError:
flash('Invalid time format.', 'danger')
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players)
match = Match(
tryout_id=tryout_id,
title=title,
description=description,
date=date_obj,
start_time=start_time,
end_time=end_time,
location=location,
match_type=match_type,
created_by=current_user.id
tryout_id=tryout_id, title=title, description=description,
date=date_obj, start_time=start_time, end_time=end_time,
location=location, match_type=match_type, created_by=current_user.id,
)
db.session.add(match)
db.session.flush() # Get match.id before commit
# Collect player IDs for Discord notifications
db.session.flush()
notified_player_ids = []
# Handle team vs team matches
notified_participant_ids = []
if match_type == 'team_vs_team':
team1_id = request.form.get('team1_id')
team2_id = request.form.get('team2_id')
match.team1_id = int(team1_id) if team1_id else None
match.team2_id = int(team2_id) if team2_id else None
# Create MatchParticipant records for all team members AND get notified player IDs
notified_participant_ids = []
if match.team1_id:
for m in TeamMember.query.filter_by(team_id=match.team1_id).all():
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=1)
@@ -383,14 +260,11 @@ def create_match(tryout_id):
db.session.flush()
notified_participant_ids.append(participant.id)
notified_player_ids.append(m.player_id)
# Handle player vs player matches
elif match_type == 'player_vs_player':
team1_player_ids = request.form.get('team1_player_ids', '')
team2_player_ids = request.form.get('team2_player_ids', '')
team1_ids = [int(p) for p in team1_player_ids.split(',') if p] if team1_player_ids else []
team2_ids = [int(p) for p in team2_player_ids.split(',') if p] if team2_player_ids else []
notified_participant_ids = []
for pid in team1_ids:
participant = MatchParticipant(match_id=match.id, player_id=pid, team_side=1)
db.session.add(participant)
@@ -402,85 +276,54 @@ def create_match(tryout_id):
db.session.flush()
notified_participant_ids.append(participant.id)
notified_player_ids = team1_ids + team2_ids
# Handle player scrim matches
elif match_type == 'player_scrim':
player_ids = request.form.getlist('player_ids')
notified_participant_ids = []
for pid in player_ids:
participant = MatchParticipant(match_id=match.id, player_id=int(pid))
db.session.add(participant)
db.session.flush()
notified_participant_ids.append(participant.id)
notified_player_ids = [int(p) for p in player_ids]
db.session.commit()
# Send Discord notifications to players (one per participant for proper attendance tracking)
# Discord notifications
event_date_str = date_obj.strftime('%Y-%m-%d')
event_time_str = f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}" if start_time and end_time else 'TBD'
# Send Discord notifications with proper participant reference IDs
if match_type == 'team_vs_team':
for i, player_id in enumerate(notified_player_ids):
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
send_schedule_notification(
user_id=player_id,
event_type='match',
event_title=match.title,
event_date=event_date_str,
event_time=event_time_str,
reference_id=reference_id
)
else:
for i, player_id in enumerate(notified_player_ids):
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
send_schedule_notification(
user_id=player_id,
event_type='match',
event_title=match.title,
event_date=event_date_str,
event_time=event_time_str,
reference_id=reference_id
)
for i, player_id in enumerate(notified_player_ids):
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
send_schedule_notification(
user_id=player_id, event_type='match', event_title=match.title,
event_date=event_date_str, event_time=event_time_str,
reference_id=reference_id,
)
flash('Match scheduled successfully!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players)
return render_template('pages/match_form.html', tryout=tryout, teams=teams,
all_players=all_players, prefill_date=prefill_date)
@matches_bp.route('/<int:match_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_match(match_id):
"""Edit an existing match.
GET: Render the match edit form.
POST: Update match with submitted changes.
Args:
match_id: The ID of the match to edit.
Returns:
Response: Edit form or redirect to tryout view.
"""
"""Edit an existing match."""
match = Match.query.get_or_404(match_id)
tryout = match.tryout
if not current_user.can_manage_this_tryout(tryout):
flash('You do not have permission to edit this match.', 'danger')
return redirect(url_for('matches.calendar'))
teams = Team.query.filter_by(tryout_id=tryout.id).all()
# Only show players registered for this tryout
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout.id).all()
all_players = [User.query.get(r.player_id) for r in registrations if r.player_id]
all_players = sorted([p for p in all_players if p], key=lambda x: x.username)
current_player_ids = [p.player_id for p in match.participants.all()]
# Get players grouped by team side for player_vs_player matches
team1_player_ids = [p.player_id for p in match.participants.filter_by(team_side=1).all()]
team2_player_ids = [p.player_id for p in match.participants.filter_by(team_side=2).all()]
if request.method == 'POST':
match.title = request.form.get('title')
match.description = request.form.get('description')
@@ -489,47 +332,45 @@ def edit_match(match_id):
end_time_str = request.form.get('end_time')
location = request.form.get('location')
status = request.form.get('status')
try:
match.date = datetime.strptime(date_str, '%Y-%m-%d').date()
except (ValueError, TypeError):
flash('Invalid date format.', 'danger')
return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids)
# Start time is now mandatory
return render_template('pages/match_form.html', match=match, tryout=tryout,
teams=teams, all_players=all_players,
current_player_ids=current_player_ids)
if not start_time_str:
flash('Start time is required. Please select a time slot.', 'danger')
return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids)
flash('Start time is required.', 'danger')
return render_template('pages/match_form.html', match=match, tryout=tryout,
teams=teams, all_players=all_players,
current_player_ids=current_player_ids)
try:
match.start_time = datetime.strptime(start_time_str, '%H:%M').time()
# Auto-calculate end time if not provided (start + 30 minutes)
if end_time_str:
match.end_time = datetime.strptime(end_time_str, '%H:%M').time()
else:
# Auto-calculate end time as start + 30 minutes
start_dt = datetime.combine(match.date, match.start_time)
end_dt = start_dt + timedelta(minutes=30)
match.end_time = end_dt.time()
except ValueError:
match.start_time = None
match.location = location
if status in ['scheduled', 'completed', 'cancelled']:
match.status = status
# Collect player IDs for Discord notifications
notified_player_ids = []
# Handle team vs team matches
notified_participant_ids = []
if match.match_type == 'team_vs_team':
team1_id = request.form.get('team1_id')
team2_id = request.form.get('team2_id')
new_team1_id = int(team1_id) if team1_id else None
new_team2_id = int(team2_id) if team2_id else None
# If teams changed, recreate MatchParticipant records
if new_team1_id != match.team1_id or new_team2_id != match.team2_id:
MatchParticipant.query.filter_by(match_id=match.id).delete()
match.team1_id = new_team1_id
@@ -549,129 +390,87 @@ def edit_match(match_id):
notified_participant_ids.append(participant.id)
notified_player_ids.append(m.player_id)
else:
# Teams didn't change, still get notified player IDs
if match.team1_id:
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team1_id).all()])
if match.team2_id:
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team2_id).all()])
# Handle player vs player matches - update participants
elif match.match_type == 'player_vs_player':
MatchParticipant.query.filter_by(match_id=match.id).delete()
team1_str = request.form.get('team1_player_ids', '')
team2_str = request.form.get('team2_player_ids', '')
team1_player_ids = [p for p in team1_str.split(',') if p.strip()] if team1_str else []
team2_player_ids = [p for p in team2_str.split(',') if p.strip()] if team2_str else []
notified_participant_ids = []
for pid in team1_player_ids:
t1_ids = [p for p in team1_str.split(',') if p.strip()] if team1_str else []
t2_ids = [p for p in team2_str.split(',') if p.strip()] if team2_str else []
for pid in t1_ids:
participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=1)
db.session.add(participant)
db.session.flush()
notified_participant_ids.append(participant.id)
for pid in team2_player_ids:
for pid in t2_ids:
participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=2)
db.session.add(participant)
db.session.flush()
notified_participant_ids.append(participant.id)
notified_player_ids = [int(p) for p in team1_player_ids] + [int(p) for p in team2_player_ids]
# Handle player scrim matches - update participants
notified_player_ids = [int(p) for p in t1_ids] + [int(p) for p in t2_ids]
elif match.match_type == 'player_scrim':
MatchParticipant.query.filter_by(match_id=match.id).delete()
player_ids = request.form.getlist('player_ids')
notified_participant_ids = []
for pid in player_ids:
participant = MatchParticipant(match_id=match.id, player_id=int(pid))
db.session.add(participant)
db.session.flush()
notified_participant_ids.append(participant.id)
notified_player_ids = [int(p) for p in player_ids]
db.session.commit()
# Send Discord notifications to players
if match.match_type in ['player_vs_player', 'player_scrim']:
end_time_val = match.end_time if match.end_time else match.start_time if match.start_time else None
if match.start_time and end_time_val:
event_time_str = f"{match.start_time.strftime('%I:%M %p')} - {end_time_val.strftime('%I:%M %p')}"
else:
event_time_str = 'TBD'
event_date_str = match.date.strftime('%Y-%m-%d')
for i, player_id in enumerate(notified_player_ids):
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
send_schedule_notification(
user_id=player_id,
event_type='match',
event_title=match.title,
event_date=event_date_str,
event_time=event_time_str,
reference_id=reference_id
)
elif match.match_type == 'team_vs_team':
event_date_str = match.date.strftime('%Y-%m-%d')
end_time_val = match.end_time if match.end_time else match.start_time if match.start_time else None
event_time_str = f"{match.start_time.strftime('%I:%M %p')} - {end_time_val.strftime('%I:%M %p')}" if match.start_time and end_time_val else 'TBD'
if notified_participant_ids:
for i, player_id in enumerate(notified_player_ids):
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
send_schedule_notification(
user_id=player_id,
event_type='match',
event_title=match.title,
event_date=event_date_str,
event_time=event_time_str,
reference_id=reference_id
)
else:
for player_id in notified_player_ids:
send_schedule_notification(
user_id=player_id,
event_type='match',
event_title=match.title,
event_date=event_date_str,
event_time=event_time_str,
reference_id=match.id
)
# Discord notifications
end_time_val = match.end_time or (match.start_time if match.start_time else None)
if match.start_time and end_time_val:
event_time_str = f"{match.start_time.strftime('%I:%M %p')} - {end_time_val.strftime('%I:%M %p')}"
else:
event_time_str = 'TBD'
event_date_str = match.date.strftime('%Y-%m-%d')
for i, player_id in enumerate(notified_player_ids):
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
send_schedule_notification(
user_id=player_id, event_type='match', event_title=match.title,
event_date=event_date_str, event_time=event_time_str,
reference_id=reference_id,
)
flash('Match updated successfully!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
# Build participant attendance map for the template
participants_map = {}
for p in match.participants.all():
participants_map[p.player_id] = {
'participant_id': p.id,
'attendance_confirmed': p.attendance_confirmed,
'team_side': p.team_side
'team_side': p.team_side,
}
return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams,
all_players=all_players, current_player_ids=current_player_ids,
team1_player_ids=team1_player_ids, team2_player_ids=team2_player_ids,
return render_template('pages/match_form.html', match=match, tryout=tryout,
teams=teams, all_players=all_players,
current_player_ids=current_player_ids,
team1_player_ids=team1_player_ids,
team2_player_ids=team2_player_ids,
participants_map=participants_map)
@matches_bp.route('/api/manageable-tryouts')
@login_required
def api_manageable_tryouts():
"""API endpoint returning tryouts the current user can manage.
Used by the calendar's "Create Event" modal to populate the tryout dropdown.
Returns:
Response: JSON array of {id, title, date}.
"""
"""API endpoint returning tryouts the current user can manage."""
if not can_schedule_match():
return jsonify([])
tryouts = get_visible_tryouts_for_user()
manageable = []
for t in tryouts:
if current_user.can_manage_this_tryout(t):
manageable.append({
'id': t.id,
'title': t.title,
'date': t.date.strftime('%Y-%m-%d')
'id': t.id, 'title': t.title,
'date': t.date.strftime('%Y-%m-%d'),
})
return jsonify(manageable)
@@ -679,21 +478,12 @@ def api_manageable_tryouts():
@matches_bp.route('/<int:match_id>/delete', methods=['POST'])
@login_required
def delete_match(match_id):
"""Delete a match.
Args:
match_id: The ID of the match to delete.
Returns:
Response: Redirect to tryout view with status message.
"""
"""Delete a match."""
match = Match.query.get_or_404(match_id)
tryout = match.tryout
if not current_user.can_manage_this_tryout(tryout):
flash('You do not have permission to delete this match.', 'danger')
return redirect(url_for('matches.calendar'))
db.session.delete(match)
db.session.commit()
flash('Match deleted successfully.', 'success')
@@ -701,72 +491,38 @@ def delete_match(match_id):
def get_players_available_at_time(date_str, time_str):
"""Get list of player IDs available at a specific date and time.
Checks player disponibility records to find who is available
during the specified time block.
Args:
date_str: Date in YYYY-MM-DD format.
time_str: Time in HH:MM format.
Returns:
list: List of available player IDs.
"""
"""Get list of player IDs available at a specific date and time."""
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
time_obj = datetime.strptime(time_str, '%H:%M').time()
except (ValueError, TypeError):
return []
# Calculate day of week (Python: 0=Monday, 6=Sunday)
# JavaScript: 0=Sunday, 6=Saturday, so we convert
date_parts = date_str.split('-')
date_for_day = datetime(int(date_parts[0]), int(date_parts[1]), int(date_parts[2]))
js_day = date_for_day.weekday()
# Convert Python weekday (Mon=0) to our format (Mon=0)
day_of_week = js_day
# Get all active players
date_for_day = datetime.strptime(date_str, '%Y-%m-%d')
day_of_week = date_for_day.weekday()
players = User.query.filter_by(role='player', is_active_account=True).all()
available_players = []
for player in players:
# Check if player has disponibility at this time
disponibilities = PlayerDisponibility.query.filter_by(
player_id=player.id,
day_of_week=day_of_week
player_id=player.id, day_of_week=day_of_week,
).all()
for disp in disponibilities:
# Check if time falls within disponibility block
disp_start = disp.start_time.hour * 60 + disp.start_time.minute
disp_end = disp.end_time.hour * 60 + disp.end_time.minute
match_time = time_obj.hour * 60 + time_obj.minute
if disp_start <= match_time < disp_end:
available_players.append(player.id)
break
return available_players
@matches_bp.route('/api/available_players/<date>/<time>')
@login_required
def api_available_players(date, time):
"""API endpoint to get players available at a specific date/time slot.
Args:
date: Date in YYYY-MM-DD format.
time: Time in HH:MM format.
Returns:
Response: JSON with list of available player IDs.
"""
"""API endpoint to get players available at a specific date/time slot."""
if not current_user.can_manage_teams() and not current_user.can_schedule_matches():
return jsonify({'error': 'Unauthorized'}), 403
player_ids = get_players_available_at_time(date, time)
return jsonify({'available_player_ids': player_ids})
@@ -774,34 +530,22 @@ def api_available_players(date, time):
@matches_bp.route('/<int:match_id>/toggle-presence/<int:participant_id>', methods=['POST'])
@login_required
def toggle_presence(match_id, participant_id):
"""Toggle the attendance_confirmed status for a match participant.
Accessible to tryout managers AND the participant themselves.
Args:
match_id: The ID of the match.
participant_id: The ID of the MatchParticipant record.
Returns:
Response: JSON with new status.
"""
"""Toggle attendance_confirmed for a match participant."""
match = Match.query.get_or_404(match_id)
tryout = match.tryout
participant = MatchParticipant.query.get_or_404(participant_id)
if participant.match_id != match_id:
return jsonify({'error': 'Participant does not belong to this match'}), 400
# Allow the participant themselves OR a tryout manager
is_self = participant.player_id == current_user.id
if not is_self and not current_user.can_manage_this_tryout(tryout):
return jsonify({'error': 'Unauthorized'}), 403
participant.attendance_confirmed = not participant.attendance_confirmed
db.session.commit()
return jsonify({
'participant_id': participant.id,
'attendance_confirmed': participant.attendance_confirmed,
'player_name': participant.player.username if participant.player else 'Unknown'
})
'player_name': participant.player.username if participant.player else 'Unknown',
})
@@ -1,30 +1,28 @@
"""Team match management routes for regular season matches.
This module handles CRUD operations for team-specific matches that are
not tied to tryouts. Players are pre-filled from the team roster.
Uses polymorphic isinstance checks instead of role-string comparisons.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user
from extensions import db
from models import OrgTeam, User, TeamMatch, TeamMatchParticipant, TeamPlayer
from app.extensions import db
from app.models import (
Admin, Manager, Coach, Player,
OrgTeam, User, TeamMatch, TeamMatchParticipant, TeamPlayer,
)
from datetime import datetime, timedelta
from discord_bot import send_schedule_notification
from app.discord_bot import send_schedule_notification
team_matches_bp = Blueprint('team_matches', __name__, url_prefix='/team-matches')
def can_manage_team_match(team):
"""Check if current user can manage matches for this team.
Returns:
bool: True if user is president, manager, or a coach of this team.
"""
if current_user.role in ['admin']:
"""Check if current user can manage matches for this team."""
if isinstance(current_user, Admin):
return True
if current_user.role == 'manager':
if isinstance(current_user, Manager):
return True
if current_user.role == 'coach':
if isinstance(current_user, Coach):
if team.coaches.filter_by(id=current_user.id).first():
return True
if team.coach_id == current_user.id:
@@ -35,105 +33,75 @@ def can_manage_team_match(team):
@team_matches_bp.route('')
@login_required
def list_matches():
"""List all team matches visible to the current user.
Supports optional ?team_id= query param to pre-filter by team.
Returns:
Response: Rendered team matches list template.
"""
# Optional pre-filter by team_id from query param
"""List all team matches visible to the current user."""
filter_team_id = request.args.get('team_id', type=int)
if current_user.role == 'admin':
if isinstance(current_user, Admin):
teams = OrgTeam.query.order_by(OrgTeam.name).all()
matches_query = TeamMatch.query
elif current_user.role == 'manager':
elif isinstance(current_user, Manager):
teams = OrgTeam.query.order_by(OrgTeam.name).all()
matches_query = TeamMatch.query
elif current_user.role == 'coach':
elif isinstance(current_user, Coach):
teams = OrgTeam.query.filter(
db.or_(
OrgTeam.coaches.any(id=current_user.id),
OrgTeam.coach_id == current_user.id
OrgTeam.coach_id == current_user.id,
)
).order_by(OrgTeam.name).all()
team_ids = [t.id for t in teams]
matches_query = TeamMatch.query.filter(
TeamMatch.org_team_id.in_(team_ids)
TeamMatch.org_team_id.in_(team_ids),
) if team_ids else TeamMatch.query.filter(TeamMatch.id == -1)
elif current_user.role == 'player':
elif isinstance(current_user, Player):
player_team_ids = [tp.org_team_id for tp in current_user.team_placements]
teams = OrgTeam.query.filter(OrgTeam.id.in_(player_team_ids)).all() if player_team_ids else []
matches_query = TeamMatch.query.filter(
TeamMatch.org_team_id.in_(player_team_ids)
TeamMatch.org_team_id.in_(player_team_ids),
) if player_team_ids else TeamMatch.query.filter(TeamMatch.id == -1)
else:
teams = []
matches_query = TeamMatch.query.filter(TeamMatch.id == -1)
# Apply team_id filter if provided
if filter_team_id:
matches_query = matches_query.filter(TeamMatch.org_team_id == filter_team_id)
matches = matches_query.order_by(TeamMatch.date.desc()).all()
# Build participants map for each match
match_data = []
for tm in matches:
confirmed, total = tm.get_confirmed_count()
participants = []
for p in tm.participants.all():
participants.append({
'id': p.id,
'player': p.player,
'is_confirmed': p.is_confirmed
'id': p.id, 'player': p.player,
'is_confirmed': p.is_confirmed,
})
match_data.append({
'match': tm,
'participants': participants,
'confirmed_count': confirmed,
'total_count': total
'match': tm, 'participants': participants,
'confirmed_count': confirmed, 'total_count': total,
})
return render_template('pages/team_matches.html',
teams=teams,
match_data=match_data,
now=datetime.utcnow())
teams=teams, match_data=match_data,
now=datetime.utcnow())
@team_matches_bp.route('/<int:team_id>/create', methods=['GET', 'POST'])
@login_required
def create_match(team_id):
"""Create a new team match (regular season).
GET: Render the match creation form with pre-filled team roster.
POST: Create the match with all team players as participants.
Args:
team_id: The ID of the org team to create a match for.
Returns:
Response: Create form or redirect to team matches list.
"""
"""Create a new regular-season team match."""
team = OrgTeam.query.get_or_404(team_id)
if not can_manage_team_match(team):
flash('You do not have permission to schedule matches for this team.', 'danger')
return redirect(url_for('team_matches.list_matches'))
team_players = [tp for tp in TeamPlayer.query.filter_by(org_team_id=team_id).all()]
# Allow pre-filling the date from query param (e.g., from calendar click)
prefill_date = request.args.get('date', '')
# Check if this is a practice (no opponent)
is_practice = request.args.get('type') == 'practice'
default_title = 'Practice' if is_practice else f'Team Match — {team.name}'
# For practices, render the tryout match form (with availability calendar)
if is_practice and request.method == 'GET':
# Build a lightweight proxy for the tryout object the template expects
class TryoutProxy:
def __init__(self, team_obj):
self.id = 0
@@ -141,19 +109,15 @@ def create_match(team_id):
self.date = ''
self.game = ''
self.target_org_team = team_obj
proxy_tryout = TryoutProxy(team)
all_players = [tp.player for tp in team_players if tp.player]
return render_template('pages/match_form.html',
tryout=proxy_tryout,
teams=[],
all_players=all_players,
prefill_date=prefill_date,
is_practice=True,
team_id=team_id,
team=team)
tryout=proxy_tryout, teams=[], all_players=all_players,
prefill_date=prefill_date, is_practice=True,
team_id=team_id, team=team)
if request.method == 'POST':
title = request.form.get('title', default_title)
opponent = request.form.get('opponent', '').strip() if not is_practice else None
@@ -162,17 +126,20 @@ def create_match(team_id):
start_time_str = request.form.get('start_time')
end_time_str = request.form.get('end_time')
location = request.form.get('location', '')
if not date_str:
flash('Date is required.', 'danger')
return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date)
return render_template('pages/team_match_form.html', team=team,
team_players=team_players, prefill_date=prefill_date)
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
except (ValueError, TypeError):
flash('Invalid date format.', 'danger')
return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date, is_practice=is_practice)
return render_template('pages/team_match_form.html', team=team,
team_players=team_players, prefill_date=prefill_date,
is_practice=is_practice)
start_time = None
end_time = None
if start_time_str:
@@ -186,82 +153,66 @@ def create_match(team_id):
end_time = end_dt.time()
except ValueError:
flash('Invalid time format.', 'danger')
return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date, is_practice=is_practice)
return render_template('pages/team_match_form.html', team=team,
team_players=team_players, prefill_date=prefill_date,
is_practice=is_practice)
team_match = TeamMatch(
org_team_id=team_id,
title=title,
org_team_id=team_id, title=title,
description=description or None,
opponent=opponent or None,
date=date_obj,
start_time=start_time,
end_time=end_time,
location=location or None,
created_by=current_user.id
date=date_obj, start_time=start_time, end_time=end_time,
location=location or None, created_by=current_user.id,
)
db.session.add(team_match)
db.session.flush() # Get team_match.id
# Auto-add all team players as participants
db.session.flush()
notified_participant_ids = []
for tp in team_players:
participant = TeamMatchParticipant(
team_match_id=team_match.id,
player_id=tp.player_id
team_match_id=team_match.id, player_id=tp.player_id,
)
db.session.add(participant)
db.session.flush()
notified_participant_ids.append(participant.id)
db.session.commit()
# Send Discord notifications to players
# Discord notifications
event_date_str = date_obj.strftime('%Y-%m-%d')
event_time_str = f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}" if start_time and end_time else 'TBD'
for i, tp in enumerate(team_players):
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else team_match.id
send_schedule_notification(
user_id=tp.player_id,
event_type='match',
user_id=tp.player_id, event_type='match',
event_title=team_match.title,
event_date=event_date_str,
event_time=event_time_str,
reference_id=reference_id
event_date=event_date_str, event_time=event_time_str,
reference_id=reference_id,
)
flash(f'Team match "{title}" scheduled successfully!', 'success')
return redirect(url_for('team_matches.list_matches'))
return render_template('pages/team_match_form.html', team=team, team_players=team_players)
@team_matches_bp.route('/<int:match_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_match(match_id):
"""Edit an existing team match.
GET: Render the edit form.
POST: Update match details.
Args:
match_id: The ID of the team match to edit.
Returns:
Response: Edit form or redirect to team matches list.
"""
"""Edit an existing team match."""
team_match = TeamMatch.query.get_or_404(match_id)
team = team_match.org_team
if not can_manage_team_match(team):
flash('You do not have permission to edit this match.', 'danger')
return redirect(url_for('team_matches.list_matches'))
if request.method == 'POST':
team_match.title = request.form.get('title', team_match.title)
team_match.description = request.form.get('description', '') or None
team_match.opponent = request.form.get('opponent', '').strip() or None
date_str = request.form.get('date')
if date_str:
try:
@@ -269,55 +220,43 @@ def edit_match(match_id):
except (ValueError, TypeError):
flash('Invalid date format.', 'danger')
return redirect(url_for('team_matches.edit_match', match_id=match_id))
start_time_str = request.form.get('start_time')
if start_time_str:
try:
team_match.start_time = datetime.strptime(start_time_str, '%H:%M').time()
except ValueError:
pass
end_time_str = request.form.get('end_time')
if end_time_str:
try:
team_match.end_time = datetime.strptime(end_time_str, '%H:%M').time()
except ValueError:
pass
team_match.location = request.form.get('location', '') or None
status = request.form.get('status')
if status in ['scheduled', 'completed', 'cancelled']:
team_match.status = status
db.session.commit()
flash('Match updated successfully!', 'success')
return redirect(url_for('team_matches.list_matches'))
return render_template('pages/team_match_form.html',
match=team_match,
team=team,
team_players=[])
match=team_match, team=team, team_players=[])
@team_matches_bp.route('/<int:match_id>/delete', methods=['POST'])
@login_required
def delete_match(match_id):
"""Delete a team match.
Args:
match_id: The ID of the team match to delete.
Returns:
Response: Redirect to team matches list.
"""
"""Delete a team match."""
team_match = TeamMatch.query.get_or_404(match_id)
team = team_match.org_team
if not can_manage_team_match(team):
flash('You do not have permission to delete this match.', 'danger')
return redirect(url_for('team_matches.list_matches'))
db.session.delete(team_match)
db.session.commit()
flash('Match deleted successfully.', 'success')
@@ -327,62 +266,44 @@ def delete_match(match_id):
@team_matches_bp.route('/api/manageable-teams')
@login_required
def api_manageable_teams():
"""API endpoint returning teams the current user can schedule matches for.
Used by the calendar's "Create Event" modal.
Returns:
Response: JSON array of {id, name}.
"""
"""API endpoint returning teams the current user can schedule matches for."""
if not current_user.can_schedule_matches():
return jsonify([])
if current_user.role in ['admin', 'manager']:
if isinstance(current_user, (Admin, Manager)):
teams = OrgTeam.query.order_by(OrgTeam.name).all()
elif current_user.role == 'coach':
elif isinstance(current_user, Coach):
teams = OrgTeam.query.filter(
db.or_(
OrgTeam.coaches.any(id=current_user.id),
OrgTeam.coach_id == current_user.id
OrgTeam.coach_id == current_user.id,
)
).order_by(OrgTeam.name).all()
else:
return jsonify([])
return jsonify([{'id': t.id, 'name': t.name} for t in teams])
@team_matches_bp.route('/<int:match_id>/toggle-presence/<int:participant_id>', methods=['POST'])
@login_required
def toggle_presence(match_id, participant_id):
"""Toggle the is_confirmed status for a team match participant.
Accessible to team managers/coaches AND the player themselves.
Args:
match_id: The ID of the team match.
participant_id: The ID of the TeamMatchParticipant record.
Returns:
Response: JSON with new status.
"""
"""Toggle is_confirmed for a team match participant."""
team_match = TeamMatch.query.get_or_404(match_id)
team = team_match.org_team
participant = TeamMatchParticipant.query.get_or_404(participant_id)
if participant.team_match_id != match_id:
return jsonify({'error': 'Participant does not belong to this match'}), 400
# Allow manager, coach, president, or the player themselves
can_toggle = can_manage_team_match(team) or participant.player_id == current_user.id
if not can_toggle:
return jsonify({'error': 'Unauthorized'}), 403
participant.is_confirmed = not participant.is_confirmed
db.session.commit()
return jsonify({
'participant_id': participant.id,
'is_confirmed': participant.is_confirmed,
'player_name': participant.player.username if participant.player else 'Unknown'
'player_name': participant.player.username if participant.player else 'Unknown',
})
+69 -122
View File
@@ -1,12 +1,17 @@
"""Organization team management routes.
This module handles CRUD operations for organization teams and player assignments.
Uses polymorphic isinstance checks instead of role-string comparisons.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user
from extensions import db
from models import OrgTeam, User, Team, TeamMember, PersonalNote, TeamNote, Tryout, TeamPlayer
from app.extensions import db
from app.models import (
Admin, Manager, Coach, Player,
OrgTeam, User, Team, TeamMember,
PersonalNote, TeamNote, Tryout, TeamPlayer,
)
from datetime import datetime
teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
@@ -14,33 +19,26 @@ teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
@teams_bp.route('')
@login_required
def list_teams():
"""List all organization teams visible to the current user.
Coaches and managers see only their assigned teams.
President sees all teams.
Returns:
Response: Rendered teams list template.
"""
"""List all organization teams visible to the current user."""
can_manage = current_user.can_manage_teams()
if current_user.role == 'admin':
if isinstance(current_user, Admin):
teams = OrgTeam.query.order_by(OrgTeam.name).all()
elif current_user.role == 'coach':
elif isinstance(current_user, Coach):
teams = OrgTeam.query.filter(
db.or_(
OrgTeam.coaches.any(id=current_user.id),
OrgTeam.coach_id == current_user.id
OrgTeam.coach_id == current_user.id,
)
).order_by(OrgTeam.name).all()
elif current_user.role == 'manager':
elif isinstance(current_user, Manager):
teams = OrgTeam.query.filter(
db.or_(
OrgTeam.managers.any(id=current_user.id),
OrgTeam.manager_id == current_user.id
OrgTeam.manager_id == current_user.id,
)
).order_by(OrgTeam.name).all()
elif current_user.role in ['player', 'scout']:
elif isinstance(current_user, Player):
flash('Use My Team(s) to view your teams.', 'info')
return redirect(url_for('teams.my_teams'))
else:
@@ -50,77 +48,56 @@ def list_teams():
coaches = User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all()
managers = User.query.filter_by(role='manager', is_active_account=True).order_by(User.username).all()
all_players = User.query.filter_by(role='player').order_by(User.username).all()
return render_template('pages/teams.html', teams=teams, coaches=coaches, managers=managers, all_players=all_players, can_manage=can_manage)
return render_template('pages/teams.html', teams=teams, coaches=coaches,
managers=managers, all_players=all_players, can_manage=can_manage)
@teams_bp.route('/my-teams')
@login_required
def my_teams():
"""View the player's own teams with upcoming matches.
Players can see their team rosters, coaches, managers, and
upcoming team matches with presence confirmation toggles.
Returns:
Response: Rendered my_teams template.
"""
if current_user.role != 'player':
"""View the player's own teams with upcoming matches."""
if not isinstance(current_user, Player):
flash('This page is for players.', 'info')
return redirect(url_for('teams.list_teams'))
from models import TeamMatch, TeamMatchParticipant
from datetime import datetime
from app.models import TeamMatch, TeamMatchParticipant
player_teams = current_user.get_org_teams()
team_data = []
now = datetime.utcnow()
team_data = []
for org_team in player_teams:
matches = TeamMatch.query.filter(
TeamMatch.org_team_id == org_team.id,
TeamMatch.status == 'scheduled'
TeamMatch.status == 'scheduled',
).order_by(TeamMatch.date.asc(), TeamMatch.start_time.asc()).all()
matches_data = []
for tm in matches:
confirmed, total = tm.get_confirmed_count()
participant = TeamMatchParticipant.query.filter_by(
team_match_id=tm.id,
player_id=current_user.id
team_match_id=tm.id, player_id=current_user.id,
).first()
matches_data.append({
'match': tm,
'participant_id': participant.id if participant else None,
'is_confirmed': participant.is_confirmed if participant else False,
'confirmed_count': confirmed,
'total_count': total
'confirmed_count': confirmed, 'total_count': total,
})
team_data.append({
'team': org_team,
'matches': matches_data,
'team': org_team, 'matches': matches_data,
'coaches': org_team.get_coaches(),
'managers': org_team.get_managers()
'managers': org_team.get_managers(),
})
return render_template('pages/my_teams.html',
team_data=team_data,
now=now)
return render_template('pages/my_teams.html', team_data=team_data, now=now)
@teams_bp.route('/create', methods=['POST'])
@login_required
def create_team():
"""Create a new organization team.
Args:
name: Team name from form.
coach_id: Optional coach assignment from form.
manager_id: Optional manager assignment from form.
Returns:
Response: Redirect to teams list with status message.
"""
"""Create a new organization team."""
if not current_user.can_manage_teams():
flash('You do not have permission to create teams.', 'danger')
return redirect(url_for('teams.list_teams'))
@@ -142,11 +119,11 @@ def create_team():
name=name,
coach_id=int(coach_id) if coach_id else None,
manager_id=int(manager_id) if manager_id else None,
created_by=current_user.id
created_by=current_user.id,
)
db.session.add(team)
db.session.flush()
if coach_id:
coach_user = User.query.get(int(coach_id))
if coach_user:
@@ -155,7 +132,7 @@ def create_team():
manager_user = User.query.get(int(manager_id))
if manager_user:
team.managers.append(manager_user)
db.session.commit()
flash(f'Team "{name}" created successfully!', 'success')
return redirect(url_for('teams.list_teams'))
@@ -183,37 +160,31 @@ def edit_team(team_id):
flash(f'Team "{name}" already exists.', 'danger')
return redirect(url_for('teams.list_teams'))
# Check if we're syncing staff (multi-select) or single legacy update
if request.form.get('sync_staff') == '1':
coach_ids = request.form.getlist('coach_ids')
manager_ids = request.form.getlist('manager_ids')
# Sync coaches many-to-many
team.coaches = []
for cid in coach_ids:
if cid and cid.strip():
coach_user = User.query.get(int(cid))
if coach_user and coach_user.role == 'coach':
if coach_user and isinstance(coach_user, Coach):
team.coaches.append(coach_user)
# Update legacy coach_id with first coach
coach_list = team.coaches.all()
team.coach_id = coach_list[0].id if coach_list else None
# Sync managers many-to-many
team.managers = []
for mid in manager_ids:
if mid and mid.strip():
manager_user = User.query.get(int(mid))
if manager_user and manager_user.role == 'manager':
if manager_user and isinstance(manager_user, Manager):
team.managers.append(manager_user)
# Update legacy manager_id with first manager
manager_list = team.managers.all()
team.manager_id = manager_list[0].id if manager_list else None
else:
# Legacy single dropdown update
team.coach_id = int(coach_id) if coach_id else None
team.manager_id = int(manager_id) if manager_id else None
if coach_id:
coach_user = User.query.get(int(coach_id))
if coach_user and not team.coaches.filter_by(id=coach_user.id).first():
@@ -222,7 +193,7 @@ def edit_team(team_id):
manager_user = User.query.get(int(manager_id))
if manager_user and not team.managers.filter_by(id=manager_user.id).first():
team.managers.append(manager_user)
db.session.commit()
flash(f'Team "{name}" updated successfully!', 'success')
return redirect(url_for('teams.list_teams'))
@@ -239,12 +210,10 @@ def delete_team(team_id):
team = OrgTeam.query.get_or_404(team_id)
name = team.name
from models import Tryout
tryouts = Tryout.query.filter_by(target_org_team_id=team_id).all()
if tryouts:
for t in tryouts:
t.target_org_team_id = None
db.session.commit()
for t in tryouts:
t.target_org_team_id = None
db.session.commit()
TeamPlayer.query.filter_by(org_team_id=team_id).delete()
db.session.commit()
@@ -258,26 +227,26 @@ def delete_team(team_id):
@teams_bp.route('/<int:team_id>/add_coach', methods=['POST'])
@login_required
def add_coach(team_id):
"""Add a coach to an organization team (many-to-many)."""
"""Add a coach to an organization team."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
coach_id = request.form.get('coach_id')
if not coach_id:
flash('Please select a coach.', 'danger')
return redirect(url_for('teams.list_teams'))
coach = User.query.get_or_404(int(coach_id))
if coach.role != 'coach':
if not isinstance(coach, Coach):
flash('Only coaches can be assigned as coach.', 'danger')
return redirect(url_for('teams.list_teams'))
if team.coaches.filter_by(id=coach.id).first():
flash(f'{coach.username} is already a coach of {team.name}.', 'info')
return redirect(url_for('teams.list_teams'))
team.coaches.append(coach)
if not team.coach_id:
team.coach_id = coach.id
@@ -289,26 +258,26 @@ def add_coach(team_id):
@teams_bp.route('/<int:team_id>/add_manager', methods=['POST'])
@login_required
def add_manager(team_id):
"""Add a manager to an organization team (many-to-many)."""
"""Add a manager to an organization team."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
manager_id = request.form.get('manager_id')
if not manager_id:
flash('Please select a manager.', 'danger')
return redirect(url_for('teams.list_teams'))
manager = User.query.get_or_404(int(manager_id))
if manager.role != 'manager':
if not isinstance(manager, Manager):
flash('Only managers can be assigned as manager.', 'danger')
return redirect(url_for('teams.list_teams'))
if team.managers.filter_by(id=manager.id).first():
flash(f'{manager.username} is already a manager of {team.name}.', 'info')
return redirect(url_for('teams.list_teams'))
team.managers.append(manager)
if not team.manager_id:
team.manager_id = manager.id
@@ -325,7 +294,7 @@ def remove_coach(team_id):
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
coach_id = request.form.get('coach_id')
if coach_id:
coach = User.query.get(int(coach_id))
@@ -336,7 +305,7 @@ def remove_coach(team_id):
else:
team.coaches = []
team.coach_id = None
db.session.commit()
flash(f'Coach removed from {team.name}.', 'success')
return redirect(url_for('teams.list_teams'))
@@ -350,7 +319,7 @@ def remove_manager(team_id):
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
manager_id = request.form.get('manager_id')
if manager_id:
manager = User.query.get(int(manager_id))
@@ -361,7 +330,7 @@ def remove_manager(team_id):
else:
team.managers = []
team.manager_id = None
db.session.commit()
flash(f'Manager removed from {team.name}.', 'success')
return redirect(url_for('teams.list_teams'))
@@ -375,15 +344,15 @@ def add_player(team_id):
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
player_id = request.form.get('player_id')
status = request.form.get('status', 'starter')
if not player_id:
flash('Please select a player.', 'danger')
return redirect(url_for('teams.list_teams'))
player = User.query.get_or_404(int(player_id))
if player.role != 'player':
if not isinstance(player, Player):
flash('Can only assign players to teams.', 'danger')
return redirect(url_for('teams.list_teams'))
@@ -392,11 +361,7 @@ def add_player(team_id):
flash(f'{player.username} is already on {team.name}.', 'info')
return redirect(url_for('teams.list_teams'))
tp = TeamPlayer(
player_id=player.id,
org_team_id=team.id,
status=status
)
tp = TeamPlayer(player_id=player.id, org_team_id=team.id, status=status)
db.session.add(tp)
db.session.commit()
flash(f'{player.username} added to {team.name}!', 'success')
@@ -413,7 +378,6 @@ def remove_player(team_id, player_id):
return redirect(url_for('teams.list_teams'))
player = User.query.get_or_404(player_id)
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
if not tp:
flash(f'{player.username} is not on {team.name}.', 'danger')
@@ -439,52 +403,41 @@ def toggle_player_status(team_id, player_id):
tp.status = 'substitute' if tp.status == 'starter' else 'starter'
db.session.commit()
return jsonify({
'success': True,
'player_id': player_id,
'new_status': tp.status,
'player_name': tp.player.username
'success': True, 'player_id': player_id,
'new_status': tp.status, 'player_name': tp.player.username,
})
@teams_bp.route('/<int:team_id>/add-team-note', methods=['POST'])
@login_required
def add_team_note(team_id):
"""Add a team improvement note from the team page (for coaches)."""
"""Add a team improvement note (coaches only)."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('You do not have permission to add notes to this team.', 'danger')
return redirect(url_for('teams.list_teams'))
content = request.form.get('content', '').strip()
if content:
note = TeamNote(
org_team_id=team_id,
coach_id=current_user.id,
content=content
)
note = TeamNote(org_team_id=team_id, coach_id=current_user.id, content=content)
db.session.add(note)
db.session.commit()
flash('Team notes added successfully!', 'success')
return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/add-player-note/<int:player_id>', methods=['POST'])
@login_required
def add_player_note(team_id, player_id):
"""Add a personal note for a player from the team page (for coaches)."""
"""Add a personal note for a player (coaches only)."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('You do not have permission to add notes to this team.', 'danger')
return redirect(url_for('teams.list_teams'))
player = User.query.get_or_404(player_id)
if player.role != 'player':
if not isinstance(player, Player):
flash('Can only add notes for players.', 'danger')
return redirect(url_for('teams.list_teams'))
@@ -494,15 +447,9 @@ def add_player_note(team_id, player_id):
return redirect(url_for('teams.list_teams'))
content = request.form.get('content', '').strip()
if content:
note = PersonalNote(
player_id=player_id,
coach_id=current_user.id,
content=content
)
note = PersonalNote(player_id=player_id, coach_id=current_user.id, content=content)
db.session.add(note)
db.session.commit()
flash(f'Note added for {player.username}!', 'success')
return redirect(url_for('teams.list_teams'))
+88 -231
View File
@@ -1,24 +1,26 @@
"""Tryout management routes for creating, viewing, and managing tryout events.
This module handles CRUD operations for tryouts and player registrations.
Uses polymorphic isinstance checks instead of role-string comparisons.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from extensions import db
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, Match, MatchParticipant, ESPORT_GAMES, GAME_POSITIONS
from app.extensions import db
from app.models import (
Admin, Manager, Coach, Player, Scout,
User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember,
OrgTeam, Match, MatchParticipant,
ESPORT_GAMES, GAME_POSITIONS,
)
from datetime import datetime
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
def can_manage():
"""Check if current user can manage tryouts.
Returns:
bool: True if user is president or manager.
"""
return current_user.role in ['admin', 'manager']
"""Check if current user can manage tryouts (Admin or Manager)."""
return isinstance(current_user, (Admin, Manager))
@tryouts_bp.route('')
@@ -26,48 +28,16 @@ def can_manage():
def list_tryouts():
"""List all tryouts visible to the current user.
Shows tryouts filtered by user's role:
- President: All tryouts
- Manager: Only their created tryouts
- Coach: Tryouts targeting their org team
- Player: Only tryouts they are registered for or participating in
Returns:
Response: Rendered tryouts list template.
Delegates to the polymorphic User subclass's get_visible_tryouts() method.
"""
if current_user.role == 'admin':
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
elif current_user.role == 'manager':
tryouts = Tryout.query.filter_by(created_by=current_user.id).order_by(Tryout.date.desc()).all()
elif current_user.role == 'coach':
# Coaches see tryouts targeting their org team
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if org_team:
tryouts = Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date.desc()).all()
else:
tryouts = []
elif current_user.role == 'player':
# Players only see tryouts they are registered for or participating in matches
from routes.matches import get_visible_tryouts_for_user
tryouts = get_visible_tryouts_for_user()
else:
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
tryouts = current_user.get_visible_tryouts()
return render_template('pages/tryouts.html', tryouts=tryouts, now=datetime.utcnow())
@tryouts_bp.route('/create', methods=['GET', 'POST'])
@login_required
def create_tryout():
"""Create a new tryout event.
GET: Render the tryout creation form.
POST: Create a tryout with the submitted details.
Requires president or manager role.
Returns:
Response: Create form or redirect to the new tryout.
"""
"""Create a new tryout event. Requires Admin or Manager."""
if not can_manage():
flash('You do not have permission to create tryouts.', 'danger')
return redirect(url_for('tryouts.list_tryouts'))
@@ -91,48 +61,33 @@ def create_tryout():
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
except (ValueError, TypeError):
flash('Invalid date format.', 'danger')
return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams,
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
tryout = Tryout(
title=title,
description=description,
game=game,
date=date_obj,
title=title, description=description, game=game, date=date_obj,
location=location,
max_players=int(max_players) if max_players else None,
created_by=current_user.id,
status='upcoming',
created_by=current_user.id, status='upcoming',
target_org_team_id=int(target_org_team_id) if target_org_team_id else None,
manager_id=int(manager_id) if manager_id else None,
coach_id=int(coach_id) if coach_id else None
coach_id=int(coach_id) if coach_id else None,
)
db.session.add(tryout)
db.session.commit()
flash('Tryout created successfully!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams,
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
@tryouts_bp.route('/<int:tryout_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_tryout(tryout_id):
"""Edit an existing tryout event.
GET: Render the tryout edit form with current data.
POST: Update the tryout with submitted changes.
Permission based on can_manage_this_tryout check.
Args:
tryout_id: The ID of the tryout to edit.
Returns:
Response: Edit form or redirect to tryout view.
"""
"""Edit an existing tryout event. Permission based on can_manage_this_tryout."""
tryout = Tryout.query.get_or_404(tryout_id)
# Permission: president, manager (own tryouts), or coach (targets their team)
if not current_user.can_manage_this_tryout(tryout):
flash('You do not have permission to edit this tryout.', 'danger')
return redirect(url_for('tryouts.list_tryouts'))
@@ -156,7 +111,8 @@ def edit_tryout(tryout_id):
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
except (ValueError, TypeError):
flash('Invalid date format.', 'danger')
return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams,
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
tryout.title = title
tryout.description = description
@@ -171,45 +127,34 @@ def edit_tryout(tryout_id):
flash('Tryout updated successfully!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams,
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
@tryouts_bp.route('/<int:tryout_id>')
@login_required
def view_tryout(tryout_id):
"""View a specific tryout with all details.
Displays tryout information, registered players, evaluations, teams,
matches, and evaluation status information.
Args:
tryout_id: The ID of the tryout to view.
Returns:
Response: Rendered tryout detail template.
"""
"""View a specific tryout with all details. Permission via polymorphic dispatch."""
tryout = Tryout.query.get_or_404(tryout_id)
# Check if user has permission to view this tryout
can_view = False
if current_user.role == 'admin':
if isinstance(current_user, Admin):
can_view = True
elif current_user.role == 'manager' and tryout.created_by == current_user.id:
elif isinstance(current_user, Manager) and tryout.created_by == current_user.id:
can_view = True
elif current_user.role == 'coach':
elif isinstance(current_user, Coach):
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if org_team and tryout.target_org_team_id == org_team.id:
can_view = True
elif current_user.role == 'player':
elif isinstance(current_user, Player):
is_registered = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=current_user.id
).first() is not None
tryout_id=tryout_id, player_id=current_user.id).first() is not None
player_in_match = MatchParticipant.query.join(Match).filter(
MatchParticipant.player_id == current_user.id,
Match.tryout_id == tryout_id
Match.tryout_id == tryout_id,
).first() is not None
can_view = is_registered or player_in_match
elif current_user.role == 'scout':
elif isinstance(current_user, Scout):
can_view = True
if not can_view:
@@ -220,22 +165,16 @@ def view_tryout(tryout_id):
registered_players = [User.query.get(r.player_id) for r in registrations if r.player_id]
evaluations = Evaluation.query.filter_by(tryout_id=tryout_id).all()
evaluator_ids = set(e.evaluator_id for e in evaluations)
player_ids_with_eval = set(e.player_id for e in evaluations)
# Check if current user has evaluated each player
player_eval_status = {}
if current_user.can_evaluate():
for p in registered_players:
existing = Evaluation.query.filter_by(
tryout_id=tryout_id,
player_id=p.id,
evaluator_id=current_user.id
tryout_id=tryout_id, player_id=p.id, evaluator_id=current_user.id,
).first()
player_eval_status[p.id] = existing is not None
is_registered = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=current_user.id
tryout_id=tryout_id, player_id=current_user.id,
).first() is not None
teams = Team.query.filter_by(tryout_id=tryout_id).all()
@@ -244,109 +183,83 @@ def view_tryout(tryout_id):
members = TeamMember.query.filter_by(team_id=team.id).all()
team_data.append({
'team': team,
'members': [{'player': User.query.get(m.player_id), 'position': m.position} for m in members]
'members': [{'player': User.query.get(m.player_id), 'position': m.position}
for m in members],
})
# Determine if current user can edit this tryout
can_edit = current_user.can_manage_this_tryout(tryout)
# Determine if current user can view the calendar (managers/coaches can always see it)
# Players need to be registered or participating in a match
can_view_calendar = can_edit
if current_user.role == 'player':
# Check if player is participating in any matches for this tryout
if isinstance(current_user, Player):
player_in_match = MatchParticipant.query.join(Match).filter(
MatchParticipant.player_id == current_user.id,
Match.tryout_id == tryout_id
Match.tryout_id == tryout_id,
).first() is not None
can_view_calendar = is_registered or player_in_match
# Only expose all_players to users who can manage players in this tryout
all_players = None
if can_edit:
all_players = User.query.filter_by(role='player').order_by(User.username).all()
# Get matches for this tryout with participant info
matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date, Match.start_time).all()
match_data = []
for match in matches:
# Calculate presence stats
all_participants = list(match.participants.all())
confirmed_count = sum(1 for p in all_participants if p.attendance_confirmed)
total_count = len(all_participants)
# Build per-player presence data for toggle buttons
player_presence = []
for p in all_participants:
if p.player:
player_presence.append({
'participant_id': p.id,
'player_id': p.player_id,
'participant_id': p.id, 'player_id': p.player_id,
'player_name': p.player.username,
'attendance_confirmed': p.attendance_confirmed
'attendance_confirmed': p.attendance_confirmed,
})
if match.match_type == 'team_vs_team':
participants = {
'team1': match.team1.name if match.team1 else 'TBD',
'team2': match.team2.name if match.team2 else 'TBD',
'team1_players': [{'name': m.player.username, 'position': m.position} for m in match.team1.members.all()] if match.team1 else [],
'team2_players': [{'name': m.player.username, 'position': m.position} for m in match.team2.members.all()] if match.team2 else []
'team1_players': [{'name': m.player.username, 'position': m.position}
for m in match.team1.members.all()] if match.team1 else [],
'team2_players': [{'name': m.player.username, 'position': m.position}
for m in match.team2.members.all()] if match.team2 else [],
}
elif match.match_type == 'player_vs_player':
team1_players = [{'name': p.player.username, 'position': p.position} for p in match.participants.filter_by(team_side=1).all() if p.player]
team2_players = [{'name': p.player.username, 'position': p.position} for p in match.participants.filter_by(team_side=2).all() if p.player]
team1_players = [{'name': p.player.username, 'position': p.position}
for p in match.participants.filter_by(team_side=1).all() if p.player]
team2_players = [{'name': p.player.username, 'position': p.position}
for p in match.participants.filter_by(team_side=2).all() if p.player]
participants = {
'team1': 'Team 1',
'team2': 'Team 2',
'team1_players': team1_players,
'team2_players': team2_players
'team1': 'Team 1', 'team2': 'Team 2',
'team1_players': team1_players, 'team2_players': team2_players,
}
else:
participants = [p.player.username for p in match.participants.all()]
match_data.append({
'match': match,
'participants': participants,
'confirmed_count': confirmed_count,
'total_count': total_count,
'player_presence': player_presence
'match': match, 'participants': participants,
'confirmed_count': confirmed_count, 'total_count': total_count,
'player_presence': player_presence,
})
return render_template('pages/view_tryout.html',
tryout=tryout,
registered_players=registered_players,
evaluations=evaluations,
player_eval_status=player_eval_status,
is_registered=is_registered,
registrations=registrations,
team_data=team_data,
can_edit=can_edit,
can_view_calendar=can_view_calendar,
all_players=all_players,
matches=matches,
match_data=match_data,
game_positions=GAME_POSITIONS,
now=datetime.utcnow())
tryout=tryout, registered_players=registered_players,
evaluations=evaluations, player_eval_status=player_eval_status,
is_registered=is_registered, registrations=registrations,
team_data=team_data, can_edit=can_edit,
can_view_calendar=can_view_calendar, all_players=all_players,
matches=matches, match_data=match_data,
game_positions=GAME_POSITIONS, now=datetime.utcnow())
@tryouts_bp.route('/<int:tryout_id>/register', methods=['POST'])
@login_required
def register_for_tryout(tryout_id):
"""Register a player for a tryout.
Allows players to register for tryouts. Validates that the tryout
is accepting registrations and not at capacity.
Args:
tryout_id: The ID of the tryout to register for.
Returns:
Response: Redirect to tryout view with status message.
"""
"""Register a player for a tryout. Only Players can self-register."""
tryout = Tryout.query.get_or_404(tryout_id)
if current_user.role != 'player':
if not isinstance(current_user, Player):
flash('Only players can register for tryouts.', 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@@ -354,7 +267,8 @@ def register_for_tryout(tryout_id):
flash('This tryout is not accepting registrations.', 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
existing = TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=current_user.id).first()
existing = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=current_user.id).first()
if existing:
flash('You are already registered for this tryout.', 'info')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@@ -375,16 +289,7 @@ def register_for_tryout(tryout_id):
@tryouts_bp.route('/<int:tryout_id>/status', methods=['POST'])
@login_required
def update_status(tryout_id):
"""Update the status of a tryout.
Changes tryout status between upcoming, in_progress, and completed.
Args:
tryout_id: The ID of the tryout to update.
Returns:
Response: Redirect to tryout view.
"""
"""Update the status of a tryout."""
tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout):
flash('Permission denied.', 'danger')
@@ -400,21 +305,14 @@ def update_status(tryout_id):
@tryouts_bp.route('/<int:tryout_id>/registration/<int:player_id>/status', methods=['POST'])
@login_required
def update_registration_status(tryout_id, player_id):
"""Update the attendance status of a tryout registration.
Args:
tryout_id: The ID of the tryout.
player_id: The ID of the player whose status to update.
Returns:
Response: Redirect to tryout view.
"""
"""Update a registration's attendance status."""
tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout):
flash('Permission denied.', 'danger')
return redirect(url_for('tryouts.list_tryouts'))
registration = TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player_id).first_or_404()
registration = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=player_id).first_or_404()
new_status = request.form.get('status')
if new_status in ['registered', 'attended', 'no_show']:
registration.status = new_status
@@ -426,32 +324,23 @@ def update_registration_status(tryout_id, player_id):
@tryouts_bp.route('/<int:tryout_id>/register_player', methods=['POST'])
@login_required
def register_player(tryout_id):
"""Manually register a player for a tryout (by managers/coaches).
Allows authorized users to register players on their behalf.
Args:
tryout_id: The ID of the tryout.
Returns:
Response: Redirect to tryout view with status message.
"""
"""Manually register a player for a tryout (by managers/coaches)."""
tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout):
flash('Permission denied.', 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
player_id = request.form.get('player_id')
if not player_id:
flash('Please select a player.', 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
player = User.query.get_or_404(int(player_id))
if player.role != 'player':
if not isinstance(player, Player):
flash('Can only register players.', 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
existing = TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player.id).first()
existing = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=player.id).first()
if existing:
flash(f'{player.username} is already registered for this tryout.', 'info')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@@ -472,48 +361,33 @@ def register_player(tryout_id):
@tryouts_bp.route('/<int:tryout_id>/remove_player/<int:player_id>', methods=['POST'])
@login_required
def remove_player(tryout_id, player_id):
"""Remove a registered player from a tryout.
Also removes the player from any tryout teams and match participants
within this tryout.
Args:
tryout_id: The ID of the tryout.
player_id: The ID of the player to remove.
Returns:
Response: Redirect to tryout view with status message.
"""
"""Remove a registered player from a tryout (cascades to teams/matches)."""
tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout):
flash('Permission denied.', 'danger')
return redirect(url_for('tryouts.list_tryouts'))
player = User.query.get_or_404(player_id)
# Remove the tryout registration
registration = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=player_id
).first()
tryout_id=tryout_id, player_id=player_id).first()
if registration:
db.session.delete(registration)
# Remove from tryout teams within this tryout
team_ids = [t.id for t in Team.query.filter_by(tryout_id=tryout_id).all()]
if team_ids:
TeamMember.query.filter(
TeamMember.team_id.in_(team_ids),
TeamMember.player_id == player_id
TeamMember.player_id == player_id,
).delete(synchronize_session=False)
# Remove from match participants in this tryout
match_ids = [m.id for m in Match.query.filter_by(tryout_id=tryout_id).all()]
if match_ids:
MatchParticipant.query.filter(
MatchParticipant.match_id.in_(match_ids),
MatchParticipant.player_id == player_id
MatchParticipant.player_id == player_id,
).delete(synchronize_session=False)
db.session.commit()
flash(f'{player.username} removed from tryout.', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@@ -522,14 +396,7 @@ def remove_player(tryout_id, player_id):
@tryouts_bp.route('/<int:tryout_id>/team/create', methods=['POST'])
@login_required
def create_team(tryout_id):
"""Create a tryout-specific team.
Args:
tryout_id: The ID of the tryout to create the team for.
Returns:
Response: Redirect to tryout view with status message.
"""
"""Create a tryout-specific team."""
tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout):
flash('Permission denied.', 'danger')
@@ -547,15 +414,7 @@ def create_team(tryout_id):
@tryouts_bp.route('/<int:tryout_id>/team/<int:team_id>/add', methods=['POST'])
@login_required
def add_to_team(tryout_id, team_id):
"""Add a player to a tryout team.
Args:
tryout_id: The ID of the tryout.
team_id: The ID of the team to add the player to.
Returns:
Response: Redirect to tryout view with status message.
"""
"""Add a player to a tryout team."""
team = Team.query.get_or_404(team_id)
tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout):
@@ -564,7 +423,6 @@ def add_to_team(tryout_id, team_id):
player_id = request.form.get('player_id')
position = request.form.get('position', '')
existing = TeamMember.query.filter_by(team_id=team_id, player_id=player_id).first()
if existing:
flash('Player is already on this team.', 'info')
@@ -573,5 +431,4 @@ def add_to_team(tryout_id, team_id):
db.session.add(member)
db.session.commit()
flash('Player added to team!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
+770
View File
@@ -0,0 +1,770 @@
"""User management routes for profiles, disponibilities, and contracts.
Uses polymorphic isinstance checks instead of role-string comparisons.
"""
import os
import uuid
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
from flask_login import login_required, current_user
from app.extensions import db, hash_password, csrf
from app.models import (
Admin, Manager, Coach, Player, Scout,
User, USER_TYPES, ESPORT_GAMES,
PlayerDisponibility, UserGamertag, GAME_PLATFORMS,
Contract, OrgTeam, CoachAvailability,
TeamNote, PersonalNote, OneOnOneRequest,
Evaluation, Match, Team, TeamMember,
MatchParticipant, Tryout, TryoutRegistration, TeamPlayer,
)
from werkzeug.utils import secure_filename
from datetime import datetime, timedelta, date as date_type
from marshmallow import ValidationError
from app.validators import (
CreateUserSchema, EditUserSchema, EditProfileSchema,
UploadContractSchema, OneOnOneRequestSchema,
)
import requests
ALLOWED_CONTRACT_EXTENSIONS = {'pdf'}
ALLOWED_SIGNED_EXTENSIONS = {'pdf'}
users_bp = Blueprint('users', __name__, url_prefix='/users')
# ---------------------------------------------------------------------------
# Gamertag helper (shared)
# ---------------------------------------------------------------------------
def update_user_gamertags(user, selected_games):
"""Update gamertags for a user based on form input."""
existing_gamertags = {gt.game: gt for gt in user.gamertags}
for game in selected_games:
gamertag = request.form.get(f'gamertag_{game}', '').strip()
platform = request.form.get(f'platform_{game}', '').strip() if GAME_PLATFORMS.get(game) else None
existing = existing_gamertags.get(game)
if gamertag:
if existing:
existing.gamertag = gamertag
existing.platform = platform
else:
gt = UserGamertag(user_id=user.id, game=game, gamertag=gamertag, platform=platform)
db.session.add(gt)
elif existing:
db.session.delete(existing)
for game in existing_gamertags:
if game not in selected_games:
db.session.delete(existing_gamertags[game])
# ---------------------------------------------------------------------------
# USER_TYPE → Model mapping for create_user
# ---------------------------------------------------------------------------
_USER_CLASS_MAP = {
'admin': Admin,
'manager': Manager,
'coach': Coach,
'player': Player,
'scout': Scout,
}
# ===========================================================================
# ROUTES
# ===========================================================================
@users_bp.route('')
@login_required
def list_users():
"""List all users for management (Admin only)."""
if not isinstance(current_user, Admin):
flash('Only the president can manage users.', 'danger')
return redirect(url_for('main.dashboard'))
users = User.query.order_by(User.role, User.username).all()
return render_template('pages/users.html', users=users, roles=USER_TYPES)
@users_bp.route('/<int:user_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_user(user_id):
"""Edit an existing user (Admin only)."""
if not isinstance(current_user, Admin):
flash('Only the president can edit users.', 'danger')
return redirect(url_for('main.dashboard'))
user = User.query.get_or_404(user_id)
if request.method == 'POST':
full_name = request.form.get('full_name')
email = request.form.get('email')
phone = request.form.get('phone')
role = request.form.get('role')
is_active = request.form.get('is_active_account') == 'on'
if role not in USER_TYPES:
flash('Invalid role selected.', 'danger')
return render_template('pages/edit_user.html', user=user, roles=USER_TYPES,
esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS)
selected_games = request.form.getlist('games')
discord_username = request.form.get('discord_username', '').strip()
discord_user_id = request.form.get('discord_user_id', '').strip()
league_os_profile = request.form.get('league_os_profile', '').strip()
user.full_name = full_name
user.email = email
user.phone = phone
user.role = role
user.is_active_account = is_active
user.games = ','.join(selected_games) if selected_games else None
user.discord_username = discord_username or None
user.discord_user_id = discord_user_id or None
user.league_os_profile = league_os_profile or None
update_user_gamertags(user, selected_games)
password = request.form.get('password')
if password:
user.password_hash = hash_password(password)
db.session.commit()
flash(f'User {user.username} updated successfully!', 'success')
return redirect(url_for('users.list_users'))
user_gamertags = {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform}
for gt in user.gamertags}
return render_template('pages/edit_user.html', user=user, roles=USER_TYPES,
esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS,
user_gamertags=user_gamertags)
@users_bp.route('/<int:user_id>/delete', methods=['POST'])
@login_required
def delete_user(user_id):
"""Delete a user (Admin only)."""
if not isinstance(current_user, Admin):
flash('Only the president can delete users.', 'danger')
return redirect(url_for('main.dashboard'))
if current_user.id == user_id:
flash('You cannot delete your own account.', 'danger')
return redirect(url_for('users.list_users'))
user = User.query.get_or_404(user_id)
Evaluation.query.filter(
db.or_(Evaluation.evaluator_id == user_id, Evaluation.player_id == user_id),
).delete(synchronize_session=False)
PlayerDisponibility.query.filter_by(player_id=user_id).delete()
CoachAvailability.query.filter_by(coach_id=user_id).delete()
PersonalNote.query.filter(
db.or_(PersonalNote.player_id == user_id, PersonalNote.coach_id == user_id),
).delete(synchronize_session=False)
TeamNote.query.filter_by(coach_id=user_id).delete()
OneOnOneRequest.query.filter(
db.or_(OneOnOneRequest.player_id == user_id, OneOnOneRequest.coach_id == user_id),
).delete(synchronize_session=False)
UserGamertag.query.filter_by(user_id=user_id).delete()
Contract.query.filter_by(player_id=user_id).delete()
TryoutRegistration.query.filter_by(player_id=user_id).delete()
TeamPlayer.query.filter_by(player_id=user_id).delete()
TeamMember.query.filter_by(player_id=user_id).delete()
MatchParticipant.query.filter_by(player_id=user_id).delete()
OrgTeam.query.filter_by(coach_id=user_id).update({'coach_id': None})
OrgTeam.query.filter_by(manager_id=user_id).update({'manager_id': None})
Tryout.query.filter_by(created_by=user_id).update({'created_by': current_user.id})
Match.query.filter_by(created_by=user_id).update({'created_by': current_user.id})
Team.query.filter_by(created_by=user_id).update({'created_by': current_user.id})
OrgTeam.query.filter_by(created_by=user_id).update({'created_by': current_user.id})
Contract.query.filter_by(uploaded_by_id=user_id).update({'uploaded_by_id': current_user.id})
db.session.delete(user)
db.session.commit()
flash(f'User {user.username} has been removed.', 'success')
return redirect(url_for('users.list_users'))
@users_bp.route('/create', methods=['GET', 'POST'])
@login_required
def create_user():
"""Create a new user (Admin only). Uses the correct polymorphic subclass."""
if not isinstance(current_user, Admin):
flash('Only the president can create users.', 'danger')
return redirect(url_for('main.dashboard'))
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
full_name = request.form.get('full_name')
phone = request.form.get('phone')
role = request.form.get('role')
if role not in USER_TYPES:
flash('Invalid role selected.', 'danger')
return render_template('pages/create_user.html', roles=USER_TYPES)
if User.query.filter_by(username=username).first():
flash('Username already exists.', 'danger')
return render_template('pages/create_user.html', roles=USER_TYPES)
if User.query.filter_by(email=email).first():
flash('Email already registered.', 'danger')
return render_template('pages/create_user.html', roles=USER_TYPES)
hashed_password = hash_password(password)
user_cls = _USER_CLASS_MAP.get(role, Player)
user = user_cls(
username=username, password_hash=hashed_password,
role=role, full_name=full_name,
email=email, phone=phone,
)
db.session.add(user)
db.session.commit()
flash(f'User {full_name} created as {role}!', 'success')
return redirect(url_for('users.list_users'))
return render_template('pages/create_user.html', roles=USER_TYPES)
@users_bp.route('/<int:user_id>/view')
@login_required
def view_user(user_id):
"""View a public profile for any user."""
user = User.query.get_or_404(user_id)
return render_template('pages/view_user.html', profile_user=user)
@users_bp.route('/profile')
@login_required
def profile():
"""View the current user's profile."""
contracts = None
if isinstance(current_user, Player):
contracts = Contract.query.filter_by(
player_id=current_user.id,
).order_by(Contract.uploaded_at.desc()).all()
return render_template('pages/profile.html', user=current_user, contracts=contracts)
@users_bp.route('/profile/edit', methods=['GET', 'POST'])
@login_required
def edit_profile():
"""Edit the current user's profile."""
if request.method == 'POST':
username = request.form.get('username')
full_name = request.form.get('full_name')
email = request.form.get('email')
phone = request.form.get('phone')
selected_games = request.form.getlist('games')
discord_username = request.form.get('discord_username', '').strip()
discord_user_id = request.form.get('discord_user_id', '').strip()
league_os_profile = request.form.get('league_os_profile', '').strip()
if username != current_user.username and User.query.filter_by(username=username).first():
flash('Username already taken.', 'danger')
return render_template('pages/edit_profile.html', user=current_user,
esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS,
user_gamertags=current_user.get_gamertags())
if email != current_user.email and User.query.filter_by(email=email).first():
flash('Email already in use.', 'danger')
return render_template('pages/edit_profile.html', user=current_user,
esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS,
user_gamertags=current_user.get_gamertags())
current_user.username = username
current_user.full_name = full_name
current_user.email = email
current_user.phone = phone
current_user.games = ','.join(selected_games) if selected_games else None
current_user.discord_username = discord_username or None
current_user.discord_user_id = discord_user_id or None
current_user.league_os_profile = league_os_profile or None
update_user_gamertags(current_user, selected_games)
password = request.form.get('password')
if password:
current_user.password_hash = hash_password(password)
db.session.commit()
flash('Profile updated successfully!', 'success')
return redirect(url_for('users.profile'))
return render_template('pages/edit_profile.html', user=current_user,
esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS,
user_gamertags=current_user.get_gamertags())
# ---------------------------------------------------------------------------
# Disponibilities
# ---------------------------------------------------------------------------
DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
def add_30_minutes(t):
return (datetime.combine(datetime.today(), t) + timedelta(minutes=30)).time()
@users_bp.route('/disponibilities')
@login_required
def get_disponibilities():
"""API endpoint to get all player disponibilities for scheduling."""
if not current_user.can_manage_teams() and not current_user.can_schedule_matches():
return jsonify({'error': 'Unauthorized'}), 403
players = User.query.filter_by(role='player', is_active_account=True).order_by(User.username).all()
result = {}
for player in players:
disponibilities = list(player.disponibilities)
result[player.id] = {
'username': player.username,
'disponibilities': [
{
'id': d.id, 'day_of_week': d.day_of_week,
'day_name': DAY_NAMES[d.day_of_week],
'start_time': d.start_time.strftime('%H:%M'),
'end_time': d.end_time.strftime('%H:%M'),
}
for d in disponibilities
],
}
return jsonify(result)
@users_bp.route('/disponibilities/my')
@login_required
def get_my_disponibilities():
"""API endpoint for players to get their own disponibilities."""
disponibilities = PlayerDisponibility.query.filter_by(player_id=current_user.id).all()
result = {}
for d in disponibilities:
day = d.day_of_week
if day not in result:
result[day] = []
result[day].append({
'id': d.id, 'day_of_week': d.day_of_week,
'day_name': DAY_NAMES[d.day_of_week],
'start_time': d.start_time.strftime('%H:%M'),
'end_time': d.end_time.strftime('%H:%M'),
})
return jsonify(result)
@users_bp.route('/disponibilities/add', methods=['POST'])
@login_required
def add_disponibility():
"""Add a disponibility block for the current player."""
day_of_week = request.form.get('day_of_week', type=int)
start_time_str = request.form.get('start_time')
if day_of_week is None or day_of_week < 0 or day_of_week > 6:
return jsonify({'error': 'Invalid day of week'}), 400
try:
start_time = datetime.strptime(start_time_str, '%H:%M').time()
except (ValueError, TypeError):
return jsonify({'error': 'Invalid time format'}), 400
end_time = add_30_minutes(start_time)
disponibility = PlayerDisponibility(
player_id=current_user.id, day_of_week=day_of_week,
start_time=start_time, end_time=end_time,
)
db.session.add(disponibility)
db.session.commit()
return jsonify({
'id': disponibility.id, 'day_of_week': disponibility.day_of_week,
'day_name': DAY_NAMES[disponibility.day_of_week],
'start_time': disponibility.start_time.strftime('%H:%M'),
'end_time': disponibility.end_time.strftime('%H:%M'),
})
@users_bp.route('/disponibilities/add_bulk', methods=['POST'])
@login_required
def add_disponibilities_bulk():
"""Add multiple disponibility blocks at once."""
data = request.get_json()
slots = data.get('slots', [])
created = []
for slot in slots:
day_of_week = slot.get('day_of_week')
start_time_str = slot.get('start_time')
if day_of_week is None or day_of_week < 0 or day_of_week > 6:
continue
try:
start_time = datetime.strptime(start_time_str, '%H:%M').time()
except (ValueError, TypeError):
continue
end_time = add_30_minutes(start_time)
existing = PlayerDisponibility.query.filter_by(
player_id=current_user.id, day_of_week=day_of_week, start_time=start_time,
).first()
if not existing:
disponibility = PlayerDisponibility(
player_id=current_user.id, day_of_week=day_of_week,
start_time=start_time, end_time=end_time,
)
db.session.add(disponibility)
db.session.flush()
created.append({
'id': disponibility.id, 'day_of_week': disponibility.day_of_week,
'day_name': DAY_NAMES[disponibility.day_of_week],
'start_time': disponibility.start_time.strftime('%H:%M'),
})
db.session.commit()
return jsonify({'success': True, 'created': created})
@users_bp.route('/disponibilities/clear', methods=['POST'])
@login_required
def clear_disponibilities():
"""Clear all disponibilities for the current player."""
PlayerDisponibility.query.filter_by(player_id=current_user.id).delete()
db.session.commit()
return jsonify({'success': True})
@users_bp.route('/disponibilities/<int:disponibility_id>/delete', methods=['POST'])
@login_required
def delete_disponibility(disponibility_id):
"""Delete a disponibility block."""
disponibility = PlayerDisponibility.query.get_or_404(disponibility_id)
if disponibility.player_id != current_user.id:
return jsonify({'error': 'Unauthorized'}), 403
db.session.delete(disponibility)
db.session.commit()
return jsonify({'success': True})
# ---------------------------------------------------------------------------
# Contracts
# ---------------------------------------------------------------------------
def can_manage_player_contract(user, player_id):
"""Check if a user can upload contracts for a specific player."""
if isinstance(user, Admin):
return True
if isinstance(user, Manager):
return True
if isinstance(user, Coach):
org_team = OrgTeam.query.filter_by(coach_id=user.id).first()
if org_team:
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=org_team.id).first()
if tp:
return True
return False
@users_bp.route('/contracts')
@login_required
def list_contracts():
"""View contracts for the current user or players they manage."""
contracts = None
players = None
if isinstance(current_user, Player):
contracts = Contract.query.filter_by(
player_id=current_user.id,
).order_by(Contract.uploaded_at.desc()).all()
elif isinstance(current_user, (Admin, Manager, Coach)):
players = []
if isinstance(current_user, Coach):
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if org_team:
player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
players = User.query.filter(User.id.in_(player_ids)).all() if player_ids else []
else:
players = User.query.filter_by(role='player').all()
if players:
player_ids = [p.id for p in players]
contracts = Contract.query.filter(
Contract.player_id.in_(player_ids),
).order_by(Contract.uploaded_at.desc()).all()
return render_template('pages/contracts.html', contracts=contracts, players=players
if isinstance(current_user, (Admin, Manager, Coach)) else None)
@users_bp.route('/contracts/upload', methods=['GET', 'POST'])
@login_required
def upload_contract():
"""Upload a contract for a player."""
if not isinstance(current_user, (Admin, Manager, Coach)):
flash('Only presidents, managers, and coaches can upload contracts.', 'danger')
return redirect(url_for('users.list_contracts'))
if isinstance(current_user, Coach):
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
if org_team:
player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
players = User.query.filter(User.id.in_(player_ids)).all() if player_ids else []
else:
players = []
else:
players = User.query.filter_by(role='player').all()
if request.method == 'POST':
contract_schema = UploadContractSchema()
try:
validated = contract_schema.load(request.form)
except ValidationError as err:
for field, messages in err.messages.items():
for msg in messages:
flash(f'{field}: {msg}', 'danger')
return render_template('pages/upload_contract.html', players=players)
player_id = validated['player_id']
notes = validated.get('notes')
if not can_manage_player_contract(current_user, player_id):
flash('You do not have permission to upload a contract for this player.', 'danger')
return redirect(url_for('users.upload_contract'))
if 'contract_file' not in request.files:
flash('No file selected.', 'danger')
return redirect(url_for('users.upload_contract'))
file = request.files['contract_file']
if file.filename == '':
flash('No file selected.', 'danger')
return redirect(url_for('users.upload_contract'))
if not file.filename.lower().endswith('.pdf'):
flash('Only PDF files are allowed for contracts.', 'danger')
return redirect(url_for('users.upload_contract'))
upload_dir = os.path.join(os.getcwd(), 'documents', 'contrats signés')
os.makedirs(upload_dir, exist_ok=True)
player = User.query.get_or_404(player_id)
player_teams = player.get_org_teams()
team = player_teams[0] if player_teams else None
if team:
team_folder = os.path.join(upload_dir, secure_filename(team.name))
os.makedirs(team_folder, exist_ok=True)
final_dir = team_folder
else:
final_dir = upload_dir
original_filename = secure_filename(file.filename)
file_uuid = str(uuid.uuid4())
stored_filename = f"{file_uuid}.pdf"
file_path = os.path.join(final_dir, stored_filename)
file.save(file_path)
contract = Contract(
player_id=player_id, team_id=team.id if team else None,
uploaded_by_id=current_user.id,
original_filename=original_filename,
stored_filename=stored_filename,
file_path=file_path,
notes=notes if notes else None,
)
db.session.add(contract)
db.session.commit()
flash(f'Contract uploaded successfully for {player.username}!', 'success')
return redirect(url_for('users.list_contracts'))
return render_template('pages/upload_contract.html', players=players)
@users_bp.route('/contracts/<int:contract_id>/upload_signed', methods=['POST'])
@login_required
def upload_signed_contract(contract_id):
"""Upload a signed contract (player only)."""
contract = Contract.query.get_or_404(contract_id)
if not contract.can_upload_signed(current_user):
flash('Only the player can upload their signed contract.', 'danger')
return redirect(url_for('users.list_contracts'))
if 'signed_file' not in request.files:
flash('No file selected.', 'danger')
return redirect(url_for('users.list_contracts'))
file = request.files['signed_file']
if file.filename == '':
flash('No file selected.', 'danger')
return redirect(url_for('users.list_contracts'))
signed_filename = f"signed_{contract.stored_filename}"
file.save(contract.file_path.replace(contract.stored_filename, signed_filename))
contract.signed_filename = signed_filename
contract.signed_file_path = contract.file_path.replace(contract.stored_filename, signed_filename)
contract.status = 'signed'
contract.signed_at = datetime.utcnow()
db.session.commit()
flash('Signed contract uploaded successfully!', 'success')
return redirect(url_for('users.list_contracts'))
@users_bp.route('/contracts/<int:contract_id>/download')
@login_required
def download_contract(contract_id):
"""Download a contract file."""
contract = Contract.query.get_or_404(contract_id)
if not contract.can_view(current_user):
flash('You do not have permission to download this contract.', 'danger')
return redirect(url_for('users.list_contracts'))
return send_file(contract.file_path, as_attachment=True, download_name=contract.original_filename)
@users_bp.route('/contracts/<int:contract_id>/download_signed')
@login_required
def download_signed_contract(contract_id):
"""Download a signed contract file."""
contract = Contract.query.get_or_404(contract_id)
if not contract.can_view(current_user):
flash('You do not have permission to download this contract.', 'danger')
return redirect(url_for('users.list_contracts'))
if not contract.signed_file_path:
flash('No signed contract available.', 'danger')
return redirect(url_for('users.list_contracts'))
return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename)
# ---------------------------------------------------------------------------
# One on One
# ---------------------------------------------------------------------------
DISCORD_WEBHOOK_URL = os.environ.get('DISCORD_WEBHOOK_URL', '')
def send_discord_notification(player_name, points, date_str, start_time_str, end_time_str,
team_name, coach_name, coach_discord, coach_discord_id, request_id=None):
"""Send a Discord notification for a One on One request."""
import logging
logger = logging.getLogger(__name__)
if coach_discord_id:
try:
from app.discord_bot import send_one_on_one_dm
send_one_on_one_dm(
coach_name=coach_name, coach_discord_id=coach_discord_id,
player_name=player_name, team_name=team_name,
date_str=date_str, start_time=start_time_str,
end_time=end_time_str, points=points, request_id=request_id,
)
except Exception as e:
logger.warning(f"Failed to send Discord DM: {e}")
if DISCORD_WEBHOOK_URL:
try:
from app.discord_bot import send_one_on_one_dm
if DISCORD_WEBHOOK_URL.isdigit() and not coach_discord_id:
send_one_on_one_dm(
coach_name=coach_name, coach_discord_id=DISCORD_WEBHOOK_URL,
player_name=player_name, team_name=team_name,
date_str=date_str, start_time=start_time_str,
end_time=end_time_str, points=points,
)
elif not DISCORD_WEBHOOK_URL.isdigit():
embed = {
"embeds": [{
"title": "One on One Request", "color": 3447003,
"fields": [
{"name": "Player", "value": player_name, "inline": True},
{"name": "Team", "value": team_name or "Unknown Team", "inline": True},
{"name": "Date", "value": date_str, "inline": True},
{"name": "Time", "value": f"{start_time_str} - {end_time_str}", "inline": True},
{"name": "Discussion Points", "value": points or "No specific points provided", "inline": False},
],
"footer": {
"text": f"Coach: {coach_name}"
+ (f" (Discord: {coach_discord})" if coach_discord else ""),
},
}],
}
requests.post(DISCORD_WEBHOOK_URL, json=embed, timeout=5)
except Exception as e:
logger.warning(f"Failed to send Discord notification: {e}")
@users_bp.route('/one-on-one', methods=['GET', 'POST'])
@login_required
def one_on_one():
"""One on One request page for players."""
if not isinstance(current_user, Player):
flash('Only players can request One on One sessions.', 'danger')
return redirect(url_for('main.dashboard'))
org_teams = current_user.get_org_teams()
org_team = org_teams[0] if org_teams else None
coach = User.query.get(org_team.coach_id) if org_team and org_team.coach_id else None
if not coach:
flash('You do not have a coach assigned to your team.', 'info')
team_notes = []
if org_team:
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
personal_notes = PersonalNote.query.filter_by(player_id=current_user.id).order_by(PersonalNote.created_at.desc()).all()
coach_availability = []
if coach:
coach_availability = CoachAvailability.query.filter_by(coach_id=coach.id).all()
if request.method == 'POST':
date_str = request.form.get('date')
start_time_str = request.form.get('start_time')
end_time_str = request.form.get('end_time')
points = request.form.get('points', '').strip()
if not coach:
flash('Cannot request One on One - no coach assigned.', 'danger')
return redirect(url_for('users.one_on_one'))
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
start_time = datetime.strptime(start_time_str, '%H:%M').time()
end_time = datetime.strptime(end_time_str, '%H:%M').time()
except (ValueError, TypeError):
flash('Invalid date or time format.', 'danger')
return redirect(url_for('users.one_on_one'))
check_date = datetime.strptime(date_str, '%Y-%m-%d')
day_of_week = check_date.weekday()
is_available = any(
av.day_of_week == day_of_week and av.start_time <= start_time and av.end_time >= end_time
for av in coach_availability
)
if not is_available:
flash("The requested time is not within the coach's availability.", 'danger')
return redirect(url_for('users.one_on_one'))
request_obj = OneOnOneRequest(
player_id=current_user.id, coach_id=coach.id,
org_team_id=org_team.id if org_team else None,
date=date_obj, start_time=start_time, end_time=end_time,
points=points if points else None,
)
db.session.add(request_obj)
db.session.commit()
send_discord_notification(
player_name=current_user.full_name,
points=points, date_str=date_str,
start_time_str=start_time_str, end_time_str=end_time_str,
team_name=org_team.name if org_team else 'Unknown Team',
coach_name=coach.full_name,
coach_discord=coach.discord_username or '',
coach_discord_id=coach.discord_user_id or '',
request_id=request_obj.id,
)
flash('Your One on One request has been submitted!', 'success')
return redirect(url_for('users.one_on_one'))
return render_template('pages/one_on_one.html',
org_team=org_team, coach=coach,
team_notes=team_notes, personal_notes=personal_notes,
coach_availability=coach_availability)
+1
View File
@@ -0,0 +1 @@
# supporting scripts package
@@ -17,7 +17,7 @@ import subprocess
import ssl
import sys
from waitress.server import create_server
from app import create_app
from app.app import create_app
CERT_FILE = 'certs/localhost.pem'
KEY_FILE = 'certs/localhost-key.pem'
@@ -264,7 +264,7 @@ def check_flask_config():
all_ok = True
try:
from app import create_app
from app.app import create_app
app = create_app()
# Check session cookie settings
+460
View File
@@ -0,0 +1,460 @@
"""Database seeding script for Team Tryouts application.
Creates sample data using the polymorphic User subclasses:
Admin, Manager, Coach, Player, Scout.
"""
from app.extensions import db, hash_password
from app.models import (
Admin, Manager, Coach, Player, Scout,
Tryout, TryoutRegistration, Evaluation, Team, TeamMember,
OrgTeam, TeamPlayer,
PlayerDisponibility, CoachAvailability,
UserGamertag, TeamNote, PersonalNote,
Match, MatchParticipant,
BaseAvailability, BaseMatch, BaseParticipant,
)
from datetime import datetime, timedelta, time
import random
def seed_database():
"""Seed the database with sample data for development and testing."""
# Clear existing data (order matters for FK constraints)
for table in ['player_disponibilities', 'coach_availabilities',
'match_participants', 'team_match_participants',
'matches', 'team_matches',
'team_players', 'team_members', 'teams',
'evaluations', 'tryout_registrations', 'tryouts',
'org_team_coaches', 'org_team_managers',
'org_teams', 'user_gamertags',
'personal_notes', 'team_notes',
'one_on_one_requests',
'users']:
db.session.execute(db.text(f'DELETE FROM {table}'))
db.session.commit()
# -----------------------------------------------------------------------
# Staff users (Admin, Manager, Coach, Scout)
# -----------------------------------------------------------------------
admin = Admin(
username='admin', password_hash=hash_password('password'),
role='admin', full_name='Sarah Johnson',
email='[email protected]', phone='555-0101')
db.session.add(admin)
manager1 = Manager(
username='manager1', password_hash=hash_password('password'),
role='manager', full_name='Mike Williams',
email='[email protected]', phone='555-0102')
db.session.add(manager1)
manager2 = Manager(
username='manager2', password_hash=hash_password('password'),
role='manager', full_name='Emily Davis',
email='[email protected]', phone='555-0103')
db.session.add(manager2)
coach1 = Coach(
username='coach1', password_hash=hash_password('password'),
role='coach', full_name='Coach Thompson',
email='[email protected]', phone='555-0104',
discord_user_id='484107446298738689')
db.session.add(coach1)
coach2 = Coach(
username='coach2', password_hash=hash_password('password'),
role='coach', full_name='Coach Martinez',
email='[email protected]', phone='555-0105')
db.session.add(coach2)
coach3 = Coach(
username='coach3', password_hash=hash_password('password'),
role='coach', full_name='Coach Anderson',
email='[email protected]', phone='555-0106')
db.session.add(coach3)
scout = Scout(
username='scout1', password_hash=hash_password('password'),
role='scout', full_name='Alex Rivera',
email='[email protected]', phone='555-0107')
db.session.add(scout)
# -----------------------------------------------------------------------
# Players
# -----------------------------------------------------------------------
player_data = [
{'username': 'jplayer1', 'full_name': 'nordjan', 'email': '[email protected]',
'games': 'Valorant, Counter-Strike 2, Rainbow Six Siege, Rocket League, Overwatch 2',
'discord_username': 'nordjan', 'discord_user_id': '484107446298738689',
'league_os_profile': 'https://leagueos.gg/player/nordjan'},
{'username': 'jplayer2', 'full_name': 'Emma Garcia', 'email': '[email protected]',
'games': 'League of Legends,Valorant',
'discord_username': 'EmmaG#4452', 'discord_user_id': '',
'league_os_profile': 'https://leagueos.gg/player/emmagarcia'},
{'username': 'jplayer3', 'full_name': 'Liam Brown', 'email': '[email protected]',
'games': 'Apex Legends,Fortnite',
'discord_username': 'LiamB#8103', 'discord_user_id': '',
'league_os_profile': 'https://leagueos.gg/player/liambrown'},
{'username': 'jplayer4', 'full_name': 'Sophia Lee', 'email': '[email protected]',
'games': 'Overwatch 2,Valorant',
'discord_username': 'SophiaL#3327', 'discord_user_id': '',
'league_os_profile': 'https://leagueos.gg/player/sophialee'},
{'username': 'jplayer5', 'full_name': 'Noah Taylor', 'email': '[email protected]',
'games': 'Counter-Strike 2,Rainbow Six Siege',
'discord_username': 'NoahT#6614', 'discord_user_id': '',
'league_os_profile': 'https://leagueos.gg/player/noahtaylor'},
{'username': 'jplayer6', 'full_name': 'Olivia Martin', 'email': '[email protected]',
'games': 'Rocket League,Fortnite',
'discord_username': 'OliviaM#2298', 'discord_user_id': '',
'league_os_profile': 'https://leagueos.gg/player/oliviamartin'},
{'username': 'jplayer7', 'full_name': 'Ethan Clark', 'email': '[email protected]',
'games': 'Valorant,Apex Legends',
'discord_username': 'EthanC#7743', 'discord_user_id': '',
'league_os_profile': 'https://leagueos.gg/player/ethanclark'},
{'username': 'jplayer8', 'full_name': 'Ava White', 'email': '[email protected]',
'games': 'League of Legends,Counter-Strike 2',
'discord_username': 'AvaW#5561', 'discord_user_id': '',
'league_os_profile': 'https://leagueos.gg/player/avawhite'},
{'username': 'jplayer9', 'full_name': 'Mason Hall', 'email': '[email protected]',
'games': 'Call of Duty,Rocket League',
'discord_username': 'MasonH#1189', 'discord_user_id': '',
'league_os_profile': 'https://leagueos.gg/player/masonhall'},
{'username': 'jplayer10', 'full_name': 'Isabella Adams', 'email': '[email protected]',
'games': 'Overwatch 2,Dota 2',
'discord_username': 'IsabellaA#4437', 'discord_user_id': '',
'league_os_profile': 'https://leagueos.gg/player/isabellaadams'},
]
players = []
for i, data in enumerate(player_data, start=10):
p = Player(
username=data['username'], password_hash=hash_password('password'),
role='player', full_name=data['full_name'],
email=data['email'], phone=f'555-01{i:02d}',
games=data['games'],
discord_username=data['discord_username'],
discord_user_id=data['discord_user_id'],
league_os_profile=data['league_os_profile'])
db.session.add(p)
players.append(p)
db.session.commit()
print(f"[OK] Created {7 + 10} users (7 staff + 10 players)")
# Map for easy reference
coaches = [coach1, coach2, coach3]
# -----------------------------------------------------------------------
# Gamertags
# -----------------------------------------------------------------------
gt_map = [
(players[0], 'Valorant', 'nordjan#bad', None),
(players[0], 'Counter-Strike 2', 'nordjan', None),
(players[0], 'Rainbow Six Siege', 'nordjan', 'Ubisoft'),
(players[0], 'Rocket League', 'nordjiano', 'Epic'),
(players[0], 'Overwatch 2', 'nordjan', 'PC'),
(players[1], 'League of Legends', 'emmagarcia_lol', None),
(players[1], 'Valorant', 'emmagarcia_val', None),
(players[2], 'Apex Legends', 'liambrown_apex', 'PC'),
(players[2], 'Fortnite', 'liambrown_fn', 'PC'),
(players[3], 'Overwatch 2', 'sophialee_ow', 'PC'),
(players[3], 'Valorant', 'sophialee_val', None),
(players[4], 'Counter-Strike 2', 'noahtaylor_cs', None),
(players[4], 'Rainbow Six Siege', 'noahtaylor_r6', 'Ubisoft'),
(players[5], 'Rocket League', 'oliviamartin_rl', 'Epic'),
(players[5], 'Fortnite', 'oliviamartin_fn', 'PC'),
(players[6], 'Valorant', 'ethanclark_val', None),
(players[6], 'Apex Legends', 'ethanclark_apex', 'PC'),
(players[7], 'League of Legends', 'avawhite_lol', None),
(players[7], 'Counter-Strike 2', 'avawhite_cs', None),
(players[8], 'Call of Duty', 'masonhall_cod', 'PC'),
(players[8], 'Rocket League', 'masonhall_rl', 'Epic'),
(players[9], 'Overwatch 2', 'isabellaadams_ow', 'PC'),
(players[9], 'Dota 2', 'isabellaadams_dota', None),
]
for p, game, tag, platform in gt_map:
db.session.add(UserGamertag(user_id=p.id, game=game, gamertag=tag, platform=platform))
db.session.commit()
print(f"[OK] Created {len(gt_map)} gamertags")
# -----------------------------------------------------------------------
# Organisation Teams
# -----------------------------------------------------------------------
org_teams_data = [
{'name': 'Rocket League main', 'coach': coaches[0], 'creator': admin},
{'name': 'CS2', 'coach': coaches[1], 'creator': admin},
{'name': 'Valorant', 'coach': coaches[2], 'creator': admin},
{'name': 'Rocket League acad', 'coach': None, 'creator': manager1},
]
org_teams = []
for data in org_teams_data:
ot = OrgTeam(
name=data['name'],
coach_id=data['coach'].id if data['coach'] else None,
created_by=data['creator'].id)
db.session.add(ot)
org_teams.append(ot)
db.session.commit()
print(f"[OK] Created {len(org_teams)} organisation teams")
# -----------------------------------------------------------------------
# Tryouts
# -----------------------------------------------------------------------
tryouts_data = [
{'title': 'Rocket Leauge Tryouts', 'game': 'Rocket League',
'date': datetime.utcnow(), 'location': 'En ligne',
'description': 'Tryouts for the spring competitive season. All positions welcome.',
'status': 'in_progress', 'creator': manager1, 'target_team': org_teams[0]},
{'title': 'CS2 Tryouts', 'game': 'Counter-Strike 2',
'date': datetime.utcnow() + timedelta(days=4), 'location': 'En ligne',
'description': 'Trials for the fall select team. High skill level required.',
'status': 'upcoming', 'creator': manager1, 'target_team': org_teams[3]},
{'title': 'Valorant Tryouts', 'game': 'Valorant',
'date': datetime.utcnow() - timedelta(days=2), 'location': 'En ligne',
'description': "Séléction pour l'équipe de Valorant",
'status': 'in_progress', 'creator': manager2, 'target_team': org_teams[2]},
]
tryouts = []
for data in tryouts_data:
t = Tryout(
title=data['title'], game=data['game'],
date=data['date'].date(),
location=data['location'],
description=data['description'],
status=data['status'], max_players=15,
created_by=data['creator'].id,
target_org_team_id=data['target_team'].id if data['target_team'] else None)
db.session.add(t)
tryouts.append(t)
db.session.commit()
print(f"[OK] Created {len(tryouts)} tryouts")
# -----------------------------------------------------------------------
# Registrations
# -----------------------------------------------------------------------
reg_assignments = [
(tryouts[0], players[:3]),
(tryouts[1], players[3:5]),
(tryouts[2], players),
]
regs = []
for tryout, plist in reg_assignments:
for player in plist:
reg = TryoutRegistration(
tryout_id=tryout.id, player_id=player.id,
status=random.choice(['registered', 'attended', 'attended', 'no_show']))
db.session.add(reg)
regs.append(reg)
db.session.commit()
print(f"[OK] Created {len(regs)} registrations")
# -----------------------------------------------------------------------
# Evaluations
# -----------------------------------------------------------------------
eval_count = 0
rl_positions = ['None needed', 'None', 'N/A']
for player in players[:8]:
for coach in coaches:
if random.random() > 0.3:
ms = [random.randint(4, 10) for _ in range(9)]
overall = round(sum(ms) / 9, 1)
db.session.add(Evaluation(
tryout_id=tryouts[0].id, player_id=player.id,
evaluator_id=coach.id,
mecanics_score=ms[0], cohesion_score=ms[1],
communication_score=ms[2], gamesense_score=ms[3],
versatility_score=ms[4], discipline_score=ms[5],
analysis_score=ms[6], sport_ethics_score=ms[7],
mental_score=ms[8], overall_score=overall,
comments=f"{'Great' if overall > 7 else 'Good'} performance. {'Shows promise.' if overall > 6 else 'Needs improvement in some areas.'}",
position_recommendation=random.choice(rl_positions)))
eval_count += 1
val_positions = ['Controller', 'Initiator', 'Duelist', 'Sentinel']
for player in players[:6]:
for coach in coaches[:2]:
ms = [random.randint(3, 10) for _ in range(9)]
overall = round(sum(ms) / 9, 1)
db.session.add(Evaluation(
tryout_id=tryouts[2].id, player_id=player.id,
evaluator_id=coach.id,
mecanics_score=ms[0], cohesion_score=ms[1],
communication_score=ms[2], gamesense_score=ms[3],
versatility_score=ms[4], discipline_score=ms[5],
analysis_score=ms[6], sport_ethics_score=ms[7],
mental_score=ms[8], overall_score=overall,
comments=f"{'Excellent' if overall > 8 else 'Solid'} display of skills during the camp.",
position_recommendation=random.choice(val_positions)))
eval_count += 1
db.session.commit()
print(f"[OK] Created {eval_count} evaluations")
# -----------------------------------------------------------------------
# Tryout-specific teams
# -----------------------------------------------------------------------
team1 = Team(tryout_id=tryouts[2].id, name='Alpha Team', created_by=admin.id)
team2 = Team(tryout_id=tryouts[2].id, name='Bravo Team', created_by=admin.id)
db.session.add(team1)
db.session.add(team2)
db.session.commit()
val_positions = ['Controller', 'Initiator', 'Duelist', 'Sentinel']
team_members_tuples = [
(team1, players[0]), (team1, players[1]),
(team1, players[2]), (team1, players[3]),
(team2, players[4]), (team2, players[5]),
]
for t, p in team_members_tuples:
db.session.add(TeamMember(team_id=t.id, player_id=p.id,
position=random.choice(val_positions)))
db.session.commit()
print("[OK] Created 2 tryout-specific teams with player assignments")
# -----------------------------------------------------------------------
# Org-team player assignments
# -----------------------------------------------------------------------
assignments = [
(org_teams[0], players[0], 'starter'),
(org_teams[0], players[1], 'starter'),
(org_teams[0], players[2], 'substitute'),
(org_teams[1], players[3], 'starter'),
(org_teams[1], players[4], 'starter'),
(org_teams[1], players[5], 'substitute'),
(org_teams[2], players[6], 'starter'),
(org_teams[2], players[7], 'substitute'),
]
for team, player, status in assignments:
db.session.add(TeamPlayer(player_id=player.id, org_team_id=team.id, status=status))
db.session.commit()
print(f"[OK] Created {len(assignments)} org-team player assignments")
# -----------------------------------------------------------------------
# Disponibilities
# -----------------------------------------------------------------------
time_slots = [(17, 0), (17, 30), (18, 0), (18, 30), (19, 0), (19, 30),
(20, 0), (20, 30), (21, 0), (21, 30), (22, 0), (22, 30), (23, 0)]
disp_count = 0
for player in players:
for day in range(7):
num = random.randint(3, 8)
for hour, minute in random.sample(time_slots, min(num, len(time_slots))):
end_h, end_m = hour, minute + 30
if end_m >= 60:
end_m -= 60; end_h += 1
db.session.add(PlayerDisponibility(
player_id=player.id, day_of_week=day,
start_time=time(hour, minute), end_time=time(end_h, end_m)))
disp_count += 1
db.session.commit()
print(f"[OK] Created {disp_count} player disponibilities")
# -----------------------------------------------------------------------
# Coach availabilities
# -----------------------------------------------------------------------
coach_slots = [(16, 0), (16, 30), (17, 0), (17, 30), (18, 0), (18, 30),
(19, 0), (19, 30), (20, 0), (20, 30), (21, 0), (21, 30)]
ca_count = 0
for coach, _ in zip(coaches[:3], org_teams[:3]):
days = [0, 1, 2, 3, 4] if coach.id == coaches[2].id else [0, 1, 2, 3, 4, 5]
for day in days:
num = random.randint(3, 5)
for hour, minute in random.sample(coach_slots, min(num, len(coach_slots))):
end_h, end_m = hour, minute + 30
if end_m >= 60:
end_m -= 60; end_h += 1
db.session.add(CoachAvailability(
coach_id=coach.id, day_of_week=day,
start_time=time(hour, minute), end_time=time(end_h, end_m)))
ca_count += 1
db.session.commit()
print(f"[OK] Created {ca_count} coach availabilities")
# -----------------------------------------------------------------------
# Team notes
# -----------------------------------------------------------------------
notes = [
(org_teams[0], coaches[0],
'Team, focus on rotation and positioning during scrims. '
'We need to improve our mechanical consistency and work on post-platoon transitions. '
'Remember to communicate clearly and stay positive!'),
(org_teams[1], coaches[1],
'Great progress this week! Keep working on your smoke lineups and utility usage. '
'Individual practice on aim trainers is paying off. Next week we focus on map control and trading.'),
(org_teams[2], coaches[2],
'Agent comp needs work. Make sure to stick to your roles and trust your teammates. '
'Work on your crosshair placement and pre-aim common angles. Team chemistry is key!'),
]
for team, coach, content in notes:
db.session.add(TeamNote(org_team_id=team.id, coach_id=coach.id, content=content))
db.session.commit()
print(f"[OK] Created {len(notes)} team notes")
# -----------------------------------------------------------------------
# Personal notes
# -----------------------------------------------------------------------
pnotes = [
(players[0], coaches[0], 'Your mechanics are improving! Focus on staying calm during high-pressure situations. Keep practicing those flip resets.'),
(players[0], coaches[0], 'Good positioning in last scrim. Work on your kickoffs - consistency will help the team.'),
(players[1], coaches[0], 'Your aerial game is strong. Try to be more aggressive on the ball when you have space.'),
(players[3], coaches[1], 'Need to work on your smoke grenade placement. Practice pre-aiming and strafe stopping.'),
(players[4], coaches[1], 'Good clutch performance! Keep your utility management consistent throughout rounds.'),
(players[6], coaches[2], 'Your aim trainer routine is paying off. Work on your agent abilities usage timing.'),
(players[7], coaches[2], 'Focus on communication in matches. Call out enemy positions clearly and ask for help when needed.'),
]
for player, coach, content in pnotes:
db.session.add(PersonalNote(player_id=player.id, coach_id=coach.id, content=content))
db.session.commit()
print(f"[OK] Created {len(pnotes)} personal notes")
# -----------------------------------------------------------------------
# Matches
# -----------------------------------------------------------------------
team1 = Team.query.filter_by(name='Alpha Team').first()
team2 = Team.query.filter_by(name='Bravo Team').first()
m1 = Match(tryout_id=tryouts[0].id, title='Alpha vs Bravo',
date=tryouts[0].date, start_time=time(18, 0), end_time=time(18, 30),
match_type='team_vs_team', created_by=admin.id,
team1_id=team1.id if team1 else None, team2_id=team2.id if team2 else None)
m2 = Match(tryout_id=tryouts[0].id, title='Bravo vs Alpha',
date=tryouts[0].date, start_time=time(19, 0), end_time=time(19, 30),
match_type='team_vs_team', created_by=admin.id,
team1_id=team2.id if team2 else None, team2_id=team1.id if team1 else None)
m3 = Match(tryout_id=tryouts[1].id, title='Scrimmage',
date=tryouts[1].date, start_time=time(17, 0), end_time=time(17, 30),
match_type='player_scrim', created_by=admin.id)
m4 = Match(tryout_id=tryouts[2].id, title='Team Alpha Scrim',
date=tryouts[2].date, start_time=time(18, 30), end_time=time(19, 0),
match_type='player_vs_player', created_by=admin.id)
db.session.add_all([m1, m2, m3, m4])
db.session.commit()
print("[OK] Created 4 matches")
# Match participants
for player in players[3:5]:
db.session.add(MatchParticipant(match_id=m3.id, player_id=player.id))
for player in players[:2]:
db.session.add(MatchParticipant(match_id=m4.id, player_id=player.id, team_side=1))
for player in players[2:4]:
db.session.add(MatchParticipant(match_id=m4.id, player_id=player.id, team_side=2))
db.session.commit()
print("[OK] Created match participants")
print("\n[SUCCESS] Database seeded successfully!")
print("\n=== Login Credentials ===")
print("Admin: username='admin', password='password'")
print("Manager: username='manager1', password='password'")
print("Coach: username='coach1', password='password'")
print("Player: username='jplayer1', password='password'")
print("Scout: username='scout1', password='password'")
if __name__ == '__main__':
from app.app import create_app
app = create_app()
with app.app_context():
seed_database()
+3 -2
View File
@@ -12,6 +12,7 @@ Usage:
import re
from marshmallow import Schema, fields, validate, ValidationError, pre_load, validates_schema, EXCLUDE
from app.models import USER_TYPES
# =============================================================================
@@ -265,7 +266,7 @@ class CreateUserSchema(StripMixin):
role = fields.String(
required=True,
validate=validate.OneOf(
['admin', 'manager', 'coach', 'player', 'scout'],
USER_TYPES,
error='Invalid role selected.'
),
)
@@ -302,7 +303,7 @@ class EditUserSchema(StripMixin):
role = fields.String(
required=True,
validate=validate.OneOf(
['admin', 'manager', 'coach', 'player', 'scout'],
USER_TYPES,
error='Invalid role selected.'
),
)
+1 -1
View File
@@ -14,7 +14,7 @@ Configuration via environment variables:
import os
import multiprocessing
from app import create_app
from app.app import create_app
app = create_app()
Binary file not shown.
-58
View File
@@ -1,58 +0,0 @@
"""Data migration script to fix corrupted username fields.
This script fixes the bug where user.username was incorrectly set to full_name
instead of preserving the actual username. It migrates existing users by:
1. Setting username to a slug version of full_name (e.g., 'sarah-johnson')
2. Clearing full_name to empty string (will be collected via profile edit)
Run this script once to fix existing database records.
"""
from app import create_app
from extensions import db
from models import User
def slugify(name):
"""Convert a name to a username-friendly slug.
Args:
name (str): Full name to convert.
Returns:
str: Slugified username.
"""
return name.lower().replace(' ', '-').replace("'", '')
def migrate():
"""Migrate existing users to fix corrupted username/full_name fields."""
with app.app_context():
users = User.query.all()
migrated = 0
for user in users:
# If username looks like a full name (contains spaces), migrate it
if ' ' in user.username:
# Save the current username (which is actually the full name)
actual_full_name = user.username
# Generate a username from the full name
new_username = slugify(actual_full_name)
# Ensure uniqueness
base_username = new_username
counter = 1
while User.query.filter_by(username=new_username).first() and User.query.get(user.id).username != new_username:
new_username = f"{base_username}-{counter}"
counter += 1
user.username = new_username
user.full_name = actual_full_name
migrated += 1
print(f"Migrated: '{actual_full_name}' -> username='{new_username}', full_name='{actual_full_name}'")
db.session.commit()
print(f"\n[MIGRATION] Migrated {migrated} users")
print("Done! Usernames are now properly stored.")
print("Users should edit their profile to set a proper username and full name.")
if __name__ == '__main__':
app = create_app()
migrate()
-1011
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-1638
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
"""Entry point for the Team Tryouts application.
Usage (from the project root):
python run.py
Or for production with Waitress:
python app/wsgi.py
"""
import os
import sys
# Ensure the project root is on sys.path so 'app' is importable as a package
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app.app import create_app
if __name__ == '__main__':
app = create_app()
debug_mode = os.getenv('FLASK_DEBUG', 'false').lower() == 'true'
if debug_mode:
app.logger.warning(
'Running in DEBUG mode with Flask built-in server. '
'This is NOT suitable for production. Use wsgi.py instead.'
)
app.run(debug=debug_mode, host='127.0.0.2', port=5000)
-523
View File
@@ -1,523 +0,0 @@
"""Database seeding script for Team Tryouts application.
This module provides functions to seed the database with sample data including
users, tryouts, teams, evaluations, and player disponibilities.
"""
from sqlalchemy import text
from extensions import db, hash_password
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, PlayerDisponibility, UserGamertag, CoachAvailability, TeamNote, PersonalNote, Match, MatchParticipant, TeamPlayer
from datetime import datetime, timedelta, time
import random
def seed_database():
"""Seed the database with sample data for development and testing.
Creates sample users with different roles (president, manager, coach, player, scout),
organization teams, tryouts, registrations, evaluations, and player disponibilities.
All existing data is cleared before seeding.
The function creates:
- 7 admin users (president, 2 managers, 3 coaches, 1 scout)
- 10 player users with E-Sports profiles
- 4 organization teams
- 4 tryouts
- Registrations and evaluations
- Player disponibilities for match scheduling
"""
# Clear existing data
db.session.execute(text('DELETE FROM player_disponibilities'))
db.session.execute(text('DELETE FROM match_participants'))
db.session.execute(text('DELETE FROM matches'))
db.session.execute(text('DELETE FROM team_players'))
db.session.execute(text('DELETE FROM team_members'))
db.session.execute(text('DELETE FROM teams'))
db.session.execute(text('DELETE FROM evaluations'))
db.session.execute(text('DELETE FROM tryout_registrations'))
db.session.execute(text('DELETE FROM tryouts'))
db.session.execute(text('DELETE FROM org_teams'))
db.session.execute(text('DELETE FROM user_gamertags'))
db.session.execute(text('DELETE FROM users'))
db.session.commit()
# Create users with different roles
users_data = [
{'username': 'admin', 'password': 'password', 'role': 'admin', 'full_name': 'Sarah Johnson', 'email': '[email protected]', 'phone': '555-0101'},
{'username': 'manager1', 'password': 'password', 'role': 'manager', 'full_name': 'Mike Williams', 'email': '[email protected]', 'phone': '555-0102'},
{'username': 'manager2', 'password': 'password', 'role': 'manager', 'full_name': 'Emily Davis', 'email': '[email protected]', 'phone': '555-0103'},
{'username': 'coach1', 'password': 'password', 'role': 'coach', 'full_name': 'Coach Thompson', 'email': '[email protected]', 'phone': '555-0104', 'discord_user_id': '484107446298738689'},
{'username': 'coach2', 'password': 'password', 'role': 'coach', 'full_name': 'Coach Martinez', 'email': '[email protected]', 'phone': '555-0105'},
{'username': 'coach3', 'password': 'password', 'role': 'coach', 'full_name': 'Coach Anderson', 'email': '[email protected]', 'phone': '555-0106'},
{'username': 'scout1', 'password': 'password', 'role': 'scout', 'full_name': 'Alex Rivera', 'email': '[email protected]', 'phone': '555-0107'},
]
# Create players with E-Sports profile data (gamertags per game)
player_data = [
{'username': 'jplayer1', 'full_name': 'nordjan', 'email': '[email protected]', 'games': 'Valorant, Counter-Strike 2, Rainbow Six Siege, Rocket League, Overwatch 2',
'gamertags': {'Valorant': 'nordjan#bad', 'Counter-Strike 2': 'nordjan', 'Rainbow Six Siege': 'n0rd-vpn', 'Rocket League': 'nordjiano'},
'discord': 'nordjan', 'discord_user_id': '484107446298738689', 'league_os': 'https://leagueos.gg/player/nordjan'},
{'username': 'jplayer2', 'full_name': 'Emma Garcia', 'email': '[email protected]', 'games': 'League of Legends,Valorant',
'gamertags': {'League of Legends': 'emmagarcia_lol', 'Valorant': 'emmagarcia_val'},
'discord': 'EmmaG#4452', 'discord_user_id': '', 'league_os': 'https://leagueos.gg/player/emmagarcia'},
{'username': 'jplayer3', 'full_name': 'Liam Brown', 'email': '[email protected]', 'games': 'Apex Legends,Fortnite',
'gamertags': {'Apex Legends': 'liambrown_apex', 'platform': 'PC', 'Fortnite': 'liambrown_fn', 'platform_fn': 'PC'},
'discord': 'LiamB#8103', 'discord_user_id': '', 'league_os': 'https://leagueos.gg/player/liambrown'},
{'username': 'jplayer4', 'full_name': 'Sophia Lee', 'email': '[email protected]', 'games': 'Overwatch 2,Valorant',
'gamertags': {'Overwatch 2': 'sophialee_ow', 'platform_ow': 'PC', 'Valorant': 'sophialee_val'},
'discord': 'SophiaL#3327', 'discord_user_id': '', 'league_os': 'https://leagueos.gg/player/sophialee'},
{'username': 'jplayer5', 'full_name': 'Noah Taylor', 'email': '[email protected]', 'games': 'Counter-Strike 2,Rainbow Six Siege',
'gamertags': {'Counter-Strike 2': 'noahtaylor_cs', 'Rainbow Six Siege': 'noahtaylor_r6', 'platform_r6': 'PC'},
'discord': 'NoahT#6614', 'discord_user_id': '', 'league_os': 'https://leagueos.gg/player/noahtaylor'},
{'username': 'jplayer6', 'full_name': 'Olivia Martin', 'email': '[email protected]', 'games': 'Rocket League,Fortnite',
'gamertags': {'Rocket League': 'oliviamartin_rl', 'platform_rl': 'PC', 'Fortnite': 'oliviamartin_fn', 'platform_fn': 'PC'},
'discord': 'OliviaM#2298', 'discord_user_id': '', 'league_os': 'https://leagueos.gg/player/oliviamartin'},
{'username': 'jplayer7', 'full_name': 'Ethan Clark', 'email': '[email protected]', 'games': 'Valorant,Apex Legends',
'gamertags': {'Valorant': 'ethanclark_val', 'Apex Legends': 'ethanclark_apex', 'platform_apex': 'PC'},
'discord': 'EthanC#7743', 'discord_user_id': '', 'league_os': 'https://leagueos.gg/player/ethanclark'},
{'username': 'jplayer8', 'full_name': 'Ava White', 'email': '[email protected]', 'games': 'League of Legends,Counter-Strike 2',
'gamertags': {'League of Legends': 'avawhite_lol', 'Counter-Strike 2': 'avawhite_cs'},
'discord': 'AvaW#5561', 'discord_user_id': '', 'league_os': 'https://leagueos.gg/player/avawhite'},
{'username': 'jplayer9', 'full_name': 'Mason Hall', 'email': '[email protected]', 'games': 'Call of Duty,Rocket League',
'gamertags': {'Call of Duty': 'masonhall_cod', 'platform_cod': 'PC', 'Rocket League': 'masonhall_rl', 'platform_rl': 'PC'},
'discord': 'MasonH#1189', 'discord_user_id': '', 'league_os': 'https://leagueos.gg/player/masonhall'},
{'username': 'jplayer10', 'full_name': 'Isabella Adams', 'email': '[email protected]', 'games': 'Overwatch 2,Dota 2',
'gamertags': {'Overwatch 2': 'isabellaadams_ow', 'platform_ow': 'PC', 'Dota 2': 'isabellaadams_dota'},
'discord': 'IsabellaA#4437', 'discord_user_id': '', 'league_os': 'https://leagueos.gg/player/isabellaadams'},
]
for i, data in enumerate(player_data, start=10):
users_data.append({
'username': data['username'], 'password': 'password', 'role': 'player',
'full_name': data['full_name'], 'email': data['email'], 'phone': f'555-01{i:02d}',
'games': data['games'], 'gamertags': data.get('gamertags', {}),
'discord_username': data['discord'], 'discord_user_id': data['discord_user_id'], 'league_os_profile': data['league_os']
})
users = []
for data in users_data:
hashed_pw = hash_password(data['password'])
user = User(
username=data['username'],
password_hash=hashed_pw,
role=data['role'],
full_name=data['full_name'],
email=data['email'],
phone=data.get('phone', ''),
games=data.get('games'),
discord_username=data.get('discord_username'),
discord_user_id=data.get('discord_user_id'),
league_os_profile=data.get('league_os_profile')
)
db.session.add(user)
users.append(user)
db.session.commit()
print(f"[OK] Created {len(users)} users")
# Map users for easy access
user_map = {u.username: u for u in users}
president = user_map['admin']
manager1 = user_map['manager1']
manager2 = user_map['manager2']
coaches = [user_map['coach1'], user_map['coach2'], user_map['coach3']]
scout = user_map['scout1']
players = [user_map[f'jplayer{i}'] for i in range(1, 11)]
# Create gamertags for players
gamertag_data = [
{'user': users[7], 'game': 'Valorant', 'gamertag': 'nordjan#bad'},
{'user': users[7], 'game': 'Counter-Strike 2', 'gamertag': 'nordjan'},
{'user': users[7], 'game': 'Rainbow Six Siege', 'gamertag': 'nordjan', 'platform': 'Ubisoft'},
{'user': users[7], 'game': 'Rocket League', 'gamertag': 'nordjiano', 'platform': 'Epic'},
{'user': users[7], 'game': 'Overwatch 2', 'gamertag': 'nordjan', 'platform': 'PC'},
{'user': users[8], 'game': 'League of Legends', 'gamertag': 'emmagarcia_lol'},
{'user': users[8], 'game': 'Valorant', 'gamertag': 'emmagarcia_val'},
{'user': users[9], 'game': 'Apex Legends', 'gamertag': 'liambrown_apex', 'platform': 'PC'},
{'user': users[9], 'game': 'Fortnite', 'gamertag': 'liambrown_fn', 'platform': 'PC'},
{'user': users[10], 'game': 'Overwatch 2', 'gamertag': 'sophialee_ow', 'platform': 'PC'},
{'user': users[10], 'game': 'Valorant', 'gamertag': 'sophialee_val'},
{'user': users[11], 'game': 'Counter-Strike 2', 'gamertag': 'noahtaylor_cs'},
{'user': users[11], 'game': 'Rainbow Six Siege', 'gamertag': 'noahtaylor_r6', 'platform': 'Ubisoft'},
{'user': users[12], 'game': 'Rocket League', 'gamertag': 'oliviamartin_rl', 'platform': 'Epic'},
{'user': users[12], 'game': 'Fortnite', 'gamertag': 'oliviamartin_fn', 'platform': 'PC'},
{'user': users[13], 'game': 'Valorant', 'gamertag': 'ethanclark_val'},
{'user': users[13], 'game': 'Apex Legends', 'gamertag': 'ethanclark_apex', 'platform': 'PC'},
{'user': users[14], 'game': 'League of Legends', 'gamertag': 'avawhite_lol'},
{'user': users[14], 'game': 'Counter-Strike 2', 'gamertag': 'avawhite_cs'},
{'user': users[15], 'game': 'Call of Duty', 'gamertag': 'masonhall_cod', 'platform': 'PC'},
{'user': users[15], 'game': 'Rocket League', 'gamertag': 'masonhall_rl', 'platform': 'Epic'},
{'user': users[16], 'game': 'Overwatch 2', 'gamertag': 'isabellaadams_ow', 'platform': 'PC'},
{'user': users[16], 'game': 'Dota 2', 'gamertag': 'isabellaadams_dota'},
]
for gt in gamertag_data:
gamertag = UserGamertag(
user_id=gt['user'].id,
game=gt['game'],
gamertag=gt['gamertag'],
platform=gt.get('platform')
)
db.session.add(gamertag)
db.session.commit()
print(f"[OK] Created {len(gamertag_data)} user gamertags")
# Create Organization Teams (OrgTeams)
org_teams_data = [
{'name': 'Rocket League main', 'coach': coaches[0], 'creator': president},
{'name': 'CS2', 'coach': coaches[1], 'creator': president},
{'name': 'Valorant', 'coach': coaches[2], 'creator': president},
{'name': 'Rocket League acad', 'coach': None, 'creator': manager1},
]
org_teams = []
for data in org_teams_data:
ot = OrgTeam(
name=data['name'],
coach_id=data['coach'].id if data['coach'] else None,
created_by=data['creator'].id
)
db.session.add(ot)
org_teams.append(ot)
db.session.commit()
print(f"[OK] Created {len(org_teams)} organization teams")
# Create tryouts
tryouts_data = [
{'title': 'Rocket Leauge Tryouts', 'game': 'Rocket League', 'date': datetime.utcnow(), 'location': 'En ligne', 'description': 'Tryouts for the spring competitive season. All positions welcome.', 'status': 'in_progress', 'creator': manager1, 'target_team': org_teams[0]},
{'title': 'CS2 Tryouts', 'game': 'Counter-Strike 2', 'date': datetime.utcnow() + timedelta(days=4), 'location': 'En ligne', 'description': 'Trials for the fall select team. High skill level required.', 'status': 'upcoming', 'creator': manager1, 'target_team': org_teams[3]},
{'title': 'Valorant Tryouts', 'game': 'Valorant', 'date': datetime.utcnow() - timedelta(days=2), 'location': 'En ligne', 'description': 'Séléction pour l\'équipe de Valorant', 'status': 'in_progress', 'creator': manager2, 'target_team': org_teams[2]},
]
tryouts = []
for data in tryouts_data:
t = Tryout(
title=data['title'],
game=data['game'],
date=data['date'].date(),
location=data['location'],
description=data['description'],
status=data['status'],
max_players=15,
created_by=data['creator'].id,
target_org_team_id=data['target_team'].id if data['target_team'] else None
)
db.session.add(t)
tryouts.append(t)
db.session.commit()
print(f"[OK] Created {len(tryouts)} tryouts")
# Register players for tryouts
registrations_data = [
(tryouts[0], players[:3]),
(tryouts[1], players[3:5]),
(tryouts[2], players)
]
regs = []
for tryout, player_list in registrations_data:
for player in player_list:
reg = TryoutRegistration(
tryout_id=tryout.id,
player_id=player.id,
status=random.choice(['registered', 'attended', 'attended', 'no_show'])
)
db.session.add(reg)
regs.append(reg)
db.session.commit()
print(f"[OK] Created {len(regs)} registrations")
# Create evaluations (for in_progress and completed tryouts)
eval_data = []
# Spring tryout (Rocket League) - some evaluations
rl_positions = ['None needed', 'None', 'N/A']
for player in players[:8]:
for coach in coaches:
if random.random() > 0.3:
mecanics = random.randint(4, 10)
cohesion = random.randint(4, 10)
communication = random.randint(3, 10)
gamesense = random.randint(5, 10)
versatility = random.randint(5, 10)
discipline = random.randint(4, 10)
analysis = random.randint(4, 10)
sport_ethics = random.randint(5, 10)
mental = random.randint(5, 10)
overall = round((mecanics + cohesion + communication + gamesense + versatility + discipline + analysis + sport_ethics + mental) / 9, 1)
eval_entry = Evaluation(
tryout_id=tryouts[0].id,
player_id=player.id,
evaluator_id=coach.id,
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,
overall_score=overall,
comments=f"{'Great' if overall > 7 else 'Good'} performance. {'Shows promise.' if overall > 6 else 'Needs improvement in some areas.'}",
position_recommendation=random.choice(rl_positions)
)
db.session.add(eval_entry)
eval_data.append(eval_entry)
# Completed tryout (Valorant) - full evaluations
val_positions = ['Controller', 'Initiator', 'Duelist', 'Sentinel']
for player in players[:6]:
for coach in coaches[:2]:
mecanics = random.randint(3, 10)
cohesion = random.randint(3, 10)
communication = random.randint(3, 10)
gamesense = random.randint(4, 10)
versatility = random.randint(4, 10)
discipline = random.randint(3, 10)
analysis = random.randint(3, 10)
sport_ethics = random.randint(4, 10)
mental = random.randint(4, 10)
overall = round((mecanics + cohesion + communication + gamesense + versatility + discipline + analysis + sport_ethics + mental) / 9, 1)
eval_entry = Evaluation(
tryout_id=tryouts[2].id,
player_id=player.id,
evaluator_id=coach.id,
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,
overall_score=overall,
comments=f"{'Excellent' if overall > 8 else 'Solid'} display of skills during the camp.",
position_recommendation=random.choice(val_positions)
)
db.session.add(eval_entry)
eval_data.append(eval_entry)
db.session.commit()
print(f"[OK] Created {len(eval_data)} evaluations")
# Create teams for the completed tryout (tryout-specific teams)
team1 = Team(tryout_id=tryouts[2].id, name='Alpha Team', created_by=president.id)
team2 = Team(tryout_id=tryouts[2].id, name='Bravo Team', created_by=president.id)
db.session.add(team1)
db.session.add(team2)
db.session.commit()
# Assign players to teams (Valorant tryout uses Valorant positions)
val_positions = ['Controller', 'Initiator', 'Duelist', 'Sentinel']
team_members_data = [
(team1.id, players[0].id, random.choice(val_positions)),
(team1.id, players[1].id, random.choice(val_positions)),
(team1.id, players[2].id, random.choice(val_positions)),
(team1.id, players[3].id, random.choice(val_positions)),
(team2.id, players[4].id, random.choice(val_positions)),
(team2.id, players[5].id, random.choice(val_positions)),
]
for team_id, player_id, position in team_members_data:
tm = TeamMember(team_id=team_id, player_id=player_id, position=position)
db.session.add(tm)
db.session.commit()
print("[OK] Created 2 tryout-specific teams with player assignments")
# Assign players to organization teams (using TeamPlayer for many-to-many)
org_team_assignments = [
(org_teams[0], players[0], 'starter'),
(org_teams[0], players[1], 'starter'),
(org_teams[0], players[2], 'substitute'),
(org_teams[1], players[3], 'starter'),
(org_teams[1], players[4], 'starter'),
(org_teams[1], players[5], 'substitute'),
(org_teams[2], players[6], 'starter'),
(org_teams[2], players[7], 'substitute'),
]
for org_team, player, status in org_team_assignments:
tp = TeamPlayer(player_id=player.id, org_team_id=org_team.id, status=status)
db.session.add(tp)
db.session.commit()
# Create sample disponibilities for players
# Time slots from 5pm to 11pm (stored as 17:00-23:00)
time_slots = [(17, 0), (17, 30), (18, 0), (18, 30), (19, 0), (19, 30), (20, 0), (20, 30), (21, 0), (21, 30), (22, 0), (22, 30), (23, 0)]
disponibilities = []
for player in players:
# Each player gets random disponibilities
for day in range(7): # All days of the week
num_slots = random.randint(3, 8)
chosen_slots = random.sample(time_slots, min(num_slots, len(time_slots)))
for hour, minute in chosen_slots:
start_time = time(hour, minute)
# End time is start_time + 30 minutes
end_minute = minute + 30
end_hour = hour
if end_minute >= 60:
end_minute -= 60
end_hour += 1
end_time = time(end_hour, end_minute)
d = PlayerDisponibility(
player_id=player.id,
day_of_week=day,
start_time=start_time,
end_time=end_time
)
db.session.add(d)
disponibilities.append(d)
# Create sample coach availabilities
coach_availabilities = []
coach_avail_time_slots = [(16, 0), (16, 30), (17, 0), (17, 30), (18, 0), (18, 30), (19, 0), (19, 30), (20, 0), (20, 30), (21, 0), (21, 30)]
# Only coaches with teams get availabilities
for coach, org_team in zip(coaches[:3], org_teams[:3]):
if coach.id == coaches[2].id: # Skip coach3 as they don't have all days
days_available = [0, 1, 2, 3, 4] # Mon-Fri
else:
days_available = [0, 1, 2, 3, 4, 5] # Mon-Sat
for day in days_available:
num_slots = random.randint(3, 5)
chosen_slots = random.sample(coach_avail_time_slots, min(num_slots, len(coach_avail_time_slots)))
for hour, minute in chosen_slots:
start_time = time(hour, minute)
end_minute = minute + 30
end_hour = hour
if end_minute >= 60:
end_minute -= 60
end_hour += 1
end_time = time(end_hour, end_minute)
ca = CoachAvailability(
coach_id=coach.id,
day_of_week=day,
start_time=start_time,
end_time=end_time
)
db.session.add(ca)
coach_availabilities.append(ca)
db.session.commit()
print(f"[OK] Created {len(coach_availabilities)} coach availabilities")
# Create team notes for each org team
team_notes_data = [
{
'team': org_teams[0],
'coach': coaches[0],
'content': 'Team, focus on rotation and positioning during scrims. We need to improve our mechanical consistency and work on post-platoon transitions. Remember to communicate clearly and stay positive!'
},
{
'team': org_teams[1],
'coach': coaches[1],
'content': 'Great progress this week! Keep working on your smoke lineups and utility usage. Individual practice on aim trainers is paying off. Next week we focus on map control and trading.'
},
{
'team': org_teams[2],
'coach': coaches[2],
'content': 'Agent comp needs work. Make sure to stick to your roles and trust your teammates. Work on your crosshair placement and pre-aim common angles. Team chemistry is key!'
},
]
for note_data in team_notes_data:
note = TeamNote(
org_team_id=note_data['team'].id,
coach_id=note_data['coach'].id,
content=note_data['content']
)
db.session.add(note)
db.session.commit()
print(f"[OK] Created {len(team_notes_data)} team notes")
# Create personal notes for players
personal_notes_data = [
{'player': players[0], 'coach': coaches[0], 'content': 'Your mechanics are improving! Focus on staying calm during high-pressure situations. Keep practicing those flip resets.'},
{'player': players[0], 'coach': coaches[0], 'content': 'Good positioning in last scrim. Work on your kickoffs - consistency will help the team.'},
{'player': players[1], 'coach': coaches[0], 'content': 'Your aerial game is strong. Try to be more aggressive on the ball when you have space.'},
{'player': players[3], 'coach': coaches[1], 'content': 'Need to work on your smoke grenade placement. Practice pre-aiming and strafe stopping.'},
{'player': players[4], 'coach': coaches[1], 'content': 'Good clutch performance! Keep your utility management consistent throughout rounds.'},
{'player': players[6], 'coach': coaches[2], 'content': 'Your aim trainer routine is paying off. Work on your agent abilities usage timing.'},
{'player': players[7], 'coach': coaches[2], 'content': 'Focus on communication in matches. Call out enemy positions clearly and ask for help when needed.'},
]
for note_data in personal_notes_data:
note = PersonalNote(
player_id=note_data['player'].id,
coach_id=note_data['coach'].id,
content=note_data['content']
)
db.session.add(note)
db.session.commit()
print(f"[OK] Created {len(personal_notes_data)} personal notes")
# Create sample matches for tryouts (for calendar testing)
matches_data = [
{'tryout': tryouts[0], 'title': 'Alpha vs Bravo', 'date': tryouts[0].date, 'start_time': time(18, 0), 'end_time': time(18, 30), 'match_type': 'team_vs_team', 'team1_id': team1.id if 'team1' in dir() else None},
{'tryout': tryouts[0], 'title': 'Bravo vs Alpha', 'date': tryouts[0].date, 'start_time': time(19, 0), 'end_time': time(19, 30), 'match_type': 'team_vs_team'},
{'tryout': tryouts[1], 'title': 'Scrimmage', 'date': tryouts[1].date, 'start_time': time(17, 0), 'end_time': time(17, 30), 'match_type': 'player_scrim'},
{'tryout': tryouts[2], 'title': 'Team Alpha Scrim', 'date': tryouts[2].date, 'start_time': time(18, 30), 'end_time': time(19, 0), 'match_type': 'player_vs_player'},
]
# Re-fetch teams after commit
team1 = Team.query.filter_by(name='Alpha Team').first()
team2 = Team.query.filter_by(name='Bravo Team').first()
matches = []
for i, m_data in enumerate(matches_data):
m = Match(
tryout_id=m_data['tryout'].id,
title=m_data['title'],
date=m_data['date'],
start_time=m_data['start_time'],
end_time=m_data['end_time'],
match_type=m_data['match_type'],
created_by=president.id,
team1_id=m_data.get('team1_id') or (team1.id if i < 2 else None),
team2_id=team2.id if i < 2 else None
)
db.session.add(m)
matches.append(m)
db.session.commit()
print(f"[OK] Created {len(matches)} matches")
# Add match participants for scrim matches
player_scrim_match = matches[2] if len(matches) > 2 else None
if player_scrim_match:
for player in players[3:5]:
mp = MatchParticipant(match_id=player_scrim_match.id, player_id=player.id)
db.session.add(mp)
pvp_match = matches[3] if len(matches) > 3 else None
if pvp_match and team1:
for player in players[:2]:
mp = MatchParticipant(match_id=pvp_match.id, player_id=player.id, team_side=1)
db.session.add(mp)
for player in players[2:4]:
mp = MatchParticipant(match_id=pvp_match.id, player_id=player.id, team_side=2)
db.session.add(mp)
db.session.commit()
print("[OK] Created match participants")
print("\n[SUCCESS] Database seeded successfully!")
print("\n=== Login Credentials ===")
print("President: username='admin', password='password'")
print("Manager: username='manager1', password='password'")
print("Coach: username='coach1', password='password' (assigned to Varsity)")
print("Coach: username='coach2', password='password' (assigned to Junior Varsity)")
print("Coach: username='coach3', password='password' (assigned to U14 Development)")
print("Player: username='jplayer1', password='password'")
print("Scout: username='scout1', password='password'")
if __name__ == '__main__':
from app import create_app
app = create_app()
with app.app_context():
seed_database()