Merge branch 'dev' of https://github.com/cedrick2711/team-tryouts
This commit is contained in:
@@ -19,3 +19,6 @@ htmlcov/
|
||||
|
||||
.certs/
|
||||
*.pem
|
||||
|
||||
docs/
|
||||
*.html
|
||||
@@ -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.
+14
-22
@@ -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()
|
||||
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)
|
||||
@@ -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.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.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.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.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.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.models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest, db
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
now = datetime.now(self.timezone)
|
||||
@@ -0,0 +1,95 @@
|
||||
"""All models — split into individual files for maintainability.
|
||||
|
||||
Import this module to register all models with SQLAlchemy and expose every
|
||||
class, constant, and helper for use throughout the application.
|
||||
|
||||
Usage::
|
||||
|
||||
from app.models import User, Admin, Evaluation, ESPORT_GAMES, ...
|
||||
|
||||
Backward-compatible — no consumer changes needed.
|
||||
"""
|
||||
|
||||
# =========================================================================
|
||||
# Layer 0: constants (no app deps)
|
||||
# =========================================================================
|
||||
from app.models._constants import (
|
||||
USER_TYPES,
|
||||
ESPORT_GAMES,
|
||||
GAME_POSITIONS,
|
||||
GAME_PLATFORMS,
|
||||
PLATFORM_CODES,
|
||||
TRN_URLS,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Layer 1: loaders & associations
|
||||
# =========================================================================
|
||||
from app.models._loaders import load_user # noqa: F401 — registers Flask-Login callback
|
||||
|
||||
# =========================================================================
|
||||
# Layer 2: abstract base classes
|
||||
# =========================================================================
|
||||
from app.models.availability.base import BaseAvailability
|
||||
from app.models.match_model.base import BaseMatch
|
||||
from app.models.participant.base import BaseParticipant
|
||||
|
||||
# =========================================================================
|
||||
# Layer 3: user hierarchy (polymorphic)
|
||||
# =========================================================================
|
||||
from app.models.user_model.user import User
|
||||
from app.models.user_model.admin import Admin
|
||||
from app.models.user_model.manager import Manager
|
||||
from app.models.user_model.coach import Coach
|
||||
from app.models.user_model.player import Player
|
||||
from app.models.user_model.scout import Scout
|
||||
|
||||
# =========================================================================
|
||||
# Layer 4: org_team + junction
|
||||
# =========================================================================
|
||||
from app.models.org_team.org_team import OrgTeam
|
||||
from app.models.org_team.team_player import TeamPlayer
|
||||
|
||||
# =========================================================================
|
||||
# Layer 5: concrete availability models
|
||||
# =========================================================================
|
||||
from app.models.availability.player_disponibility import PlayerDisponibility
|
||||
from app.models.availability.coach_availability import CoachAvailability
|
||||
|
||||
# =========================================================================
|
||||
# Layer 6: tryout + registration
|
||||
# =========================================================================
|
||||
from app.models.tryout.tryout import Tryout
|
||||
from app.models.tryout.tryout_registration import TryoutRegistration
|
||||
|
||||
# =========================================================================
|
||||
# Layer 7: evaluation
|
||||
# =========================================================================
|
||||
from app.models.evaluation import Evaluation
|
||||
|
||||
# =========================================================================
|
||||
# Layer 8: tryout-specific teams
|
||||
# =========================================================================
|
||||
from app.models.team.team import Team
|
||||
from app.models.team.team_member import TeamMember
|
||||
|
||||
# =========================================================================
|
||||
# Layer 9: matches (tryout-scoped + regular-season)
|
||||
# =========================================================================
|
||||
from app.models.match_model.match import Match
|
||||
from app.models.match_model.team_match import TeamMatch
|
||||
|
||||
# =========================================================================
|
||||
# Layer 10: participants
|
||||
# =========================================================================
|
||||
from app.models.participant.match_participant import MatchParticipant
|
||||
from app.models.participant.team_match_participant import TeamMatchParticipant
|
||||
|
||||
# =========================================================================
|
||||
# Layer 11: remaining standalone models
|
||||
# =========================================================================
|
||||
from app.models.user_gamertag import UserGamertag
|
||||
from app.models.contract import Contract
|
||||
from app.models.team_note import TeamNote
|
||||
from app.models.personal_note import PersonalNote
|
||||
from app.models.one_on_one_request import OneOnOneRequest
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Many-to-many association tables for OrgTeam ↔ User relationships."""
|
||||
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
org_team_coaches = db.Table('org_team_coaches',
|
||||
db.Column('org_team_id', db.Integer, db.ForeignKey('org_teams.id', ondelete='CASCADE'),
|
||||
primary_key=True),
|
||||
db.Column('coach_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'),
|
||||
primary_key=True),
|
||||
)
|
||||
|
||||
org_team_managers = db.Table('org_team_managers',
|
||||
db.Column('org_team_id', db.Integer, db.ForeignKey('org_teams.id', ondelete='CASCADE'),
|
||||
primary_key=True),
|
||||
db.Column('manager_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'),
|
||||
primary_key=True),
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Global constants shared by all model files.
|
||||
|
||||
Contains game lists, position mappings, platform codes, and TRN URL templates.
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
USER_TYPES = ['admin', 'manager', 'coach', 'player', 'scout']
|
||||
|
||||
ESPORT_GAMES = [
|
||||
'Valorant',
|
||||
'League of Legends',
|
||||
'Counter-Strike 2',
|
||||
'Apex Legends',
|
||||
'Overwatch 2',
|
||||
'Rainbow Six Siege',
|
||||
'Rocket League',
|
||||
'Super Smash Bros.',
|
||||
]
|
||||
|
||||
GAME_POSITIONS = {
|
||||
'League of Legends': ['Top Lane', 'Jungle', 'Mid Lane', 'ADC', 'Support'],
|
||||
'Valorant': ['Controller', 'Initiator', 'Duelist', 'Sentinel', 'Flex'],
|
||||
'Counter-Strike 2': ['AWPer', 'Entry Fragger', 'Lurker', 'In-Game Leader', 'Support'],
|
||||
'Rainbow Six Siege': ['Entry', 'Support', 'Breacher', 'Anchor', 'Flex'],
|
||||
'Overwatch 2': ['Tank', 'Damage', 'Support'],
|
||||
'Apex Legends': [],
|
||||
'Rocket League': [],
|
||||
'Super Smash Bros.': [],
|
||||
}
|
||||
|
||||
GAME_PLATFORMS = {
|
||||
'Valorant': [],
|
||||
'League of Legends': [],
|
||||
'Counter-Strike 2': [],
|
||||
'Apex Legends': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'],
|
||||
'Overwatch 2': [],
|
||||
'Rainbow Six Siege': ['Ubisoft', 'PlayStation', 'Xbox'],
|
||||
'Rocket League': ['Epic', 'PlayStation', 'Xbox', 'Nintendo Switch'],
|
||||
'Super Smash Bros.': ['Nintendo Switch'],
|
||||
}
|
||||
|
||||
PLATFORM_CODES = {
|
||||
'Ubisoft': 'ubi',
|
||||
'PlayStation': 'psn',
|
||||
'Xbox': 'xbl',
|
||||
'Nintendo Switch': 'switch',
|
||||
'PC': 'pc',
|
||||
'Steam': 'steam',
|
||||
'Epic': 'epic',
|
||||
}
|
||||
|
||||
TRN_URLS = {
|
||||
'Valorant': 'https://tracker.gg/valorant/profile/riot/{username}',
|
||||
'League of Legends': 'https://tracker.gg/lol/profile/{username}',
|
||||
'Counter-Strike 2': 'https://tracker.gg/cs2/profile/steam/{username}',
|
||||
'Apex Legends': 'https://tracker.gg/apex/profile/{platform}/{username}',
|
||||
'Overwatch 2': 'https://tracker.gg/overwatch/profile/battlenet/{username}',
|
||||
'Rainbow Six Siege': 'https://r6.tracker.network/r6siege/profile/{platform_code}/{username}',
|
||||
'Rocket League': 'https://rocketleague.tracker.network/rocket-league/profile/{platform_code}/{username}',
|
||||
'Super Smash Bros.': 'https://tracker.gg/smash/profile/{username}',
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Flask-Login user loader — registered with login_manager in models.py."""
|
||||
|
||||
from app.extensions import login_manager
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
"""Load a user by ID for Flask-Login session management.
|
||||
|
||||
Returns the correct polymorphic subclass (Admin, Coach, Player, etc.)
|
||||
automatically because SQLAlchemy resolves the identity column.
|
||||
"""
|
||||
from app.models.user_model.user import User
|
||||
return User.query.get(int(user_id))
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Availability models — BaseAvailability and its concrete subclasses."""
|
||||
|
||||
from app.models.availability.base import BaseAvailability
|
||||
from app.models.availability.player_disponibility import PlayerDisponibility
|
||||
from app.models.availability.coach_availability import CoachAvailability
|
||||
|
||||
__all__ = ['BaseAvailability', 'PlayerDisponibility', 'CoachAvailability']
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Abstract base class for availability models (PlayerDisponibility + CoachAvailability)."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BaseAvailability(db.Model):
|
||||
"""Shared schema for player disponibilities and coach availabilities."""
|
||||
__abstract__ = True
|
||||
|
||||
day_of_week = db.Column(db.Integer, nullable=False)
|
||||
start_time = db.Column(db.Time, nullable=False)
|
||||
end_time = db.Column(db.Time, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Coach availability in 30-minute time blocks for One on One sessions."""
|
||||
from app.extensions import db
|
||||
from app.models.availability.base import BaseAvailability
|
||||
|
||||
|
||||
class CoachAvailability(BaseAvailability):
|
||||
"""Coach availability in 30-minute blocks for One on One sessions."""
|
||||
__tablename__ = 'coach_availabilities'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
|
||||
coach = db.relationship('User', backref='coach_availabilities')
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Player availability in 30-minute time blocks."""
|
||||
from app.extensions import db
|
||||
from app.models.availability.base import BaseAvailability
|
||||
|
||||
|
||||
class PlayerDisponibility(BaseAvailability):
|
||||
"""Player availability in 30-minute blocks."""
|
||||
__tablename__ = 'player_disponibilities'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
|
||||
player = db.relationship('User', backref='disponibilities')
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Contract documents for players to sign."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Contract(db.Model):
|
||||
"""Contract documents for players to sign."""
|
||||
__tablename__ = 'contracts'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
||||
uploaded_by_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
|
||||
original_filename = db.Column(db.String(255), nullable=False)
|
||||
stored_filename = db.Column(db.String(255), nullable=False)
|
||||
file_path = db.Column(db.String(500), nullable=False)
|
||||
|
||||
signed_filename = db.Column(db.String(255), nullable=True)
|
||||
signed_file_path = db.Column(db.String(500), nullable=True)
|
||||
|
||||
status = db.Column(db.String(20), default='pending')
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
signed_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
player = db.relationship('User', foreign_keys=[player_id], backref='contracts')
|
||||
team = db.relationship('OrgTeam', foreign_keys=[team_id])
|
||||
uploader = db.relationship('User', foreign_keys=[uploaded_by_id])
|
||||
|
||||
def can_view(self, user):
|
||||
if user.id == self.player_id:
|
||||
return True
|
||||
from app.models.user_model.admin import Admin
|
||||
from app.models.user_model.manager import Manager
|
||||
from app.models.user_model.coach import Coach
|
||||
from app.models.user_model.user import User
|
||||
from app.models.org_team.org_team import OrgTeam
|
||||
if isinstance(user, Admin):
|
||||
return True
|
||||
if isinstance(user, Manager):
|
||||
player = User.query.get(self.player_id)
|
||||
if player and player.get_org_teams():
|
||||
return True
|
||||
if isinstance(user, Coach):
|
||||
org_team = OrgTeam.query.filter_by(coach_id=user.id).first()
|
||||
if org_team and (not self.team_id or self.team_id == org_team.id):
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_upload_signed(self, user):
|
||||
return user.id == self.player_id
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Player evaluation record."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Evaluation(db.Model):
|
||||
"""Player evaluation record."""
|
||||
__tablename__ = 'evaluations'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
evaluator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
mecanics_score = db.Column(db.Integer, nullable=True)
|
||||
cohesion_score = db.Column(db.Integer, nullable=True)
|
||||
communication_score = db.Column(db.Integer, nullable=True)
|
||||
gamesense_score = db.Column(db.Integer, nullable=True)
|
||||
versatility_score = db.Column(db.Integer, nullable=True)
|
||||
discipline_score = db.Column(db.Integer, nullable=True)
|
||||
analysis_score = db.Column(db.Integer, nullable=True)
|
||||
sport_ethics_score = db.Column(db.Integer, nullable=True)
|
||||
mental_score = db.Column(db.Integer, nullable=True)
|
||||
overall_score = db.Column(db.Float, nullable=True)
|
||||
comments = db.Column(db.Text, nullable=True)
|
||||
position_recommendation = db.Column(db.String(50), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('tryout_id', 'player_id', 'evaluator_id', name='unique_evaluation'),
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Match models — BaseMatch and its concrete subclasses."""
|
||||
from app.models.match_model.base import BaseMatch
|
||||
from app.models.match_model.match import Match
|
||||
from app.models.match_model.team_match import TeamMatch
|
||||
|
||||
__all__ = ['BaseMatch', 'Match', 'TeamMatch']
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Abstract base class for match models (Match + TeamMatch)."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BaseMatch(db.Model):
|
||||
"""Shared schema for tryout-scoped matches and regular-season team matches."""
|
||||
__abstract__ = True
|
||||
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
date = db.Column(db.Date, nullable=False)
|
||||
start_time = db.Column(db.Time, nullable=True)
|
||||
end_time = db.Column(db.Time, nullable=True)
|
||||
location = db.Column(db.String(200), nullable=True)
|
||||
status = db.Column(db.String(20), default='scheduled')
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Match / scrimmage within a tryout."""
|
||||
from app.extensions import db
|
||||
from app.models.match_model.base import BaseMatch
|
||||
|
||||
|
||||
class Match(BaseMatch):
|
||||
"""Match / scrimmage within a tryout."""
|
||||
__tablename__ = 'matches'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
match_type = db.Column(db.String(20), nullable=False)
|
||||
team1_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
|
||||
team2_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
|
||||
|
||||
creator = db.relationship('User', backref='created_matches')
|
||||
tryout = db.relationship('Tryout', backref='matches')
|
||||
team1 = db.relationship('Team', foreign_keys=[team1_id], backref='matches_as_team1')
|
||||
team2 = db.relationship('Team', foreign_keys=[team2_id], backref='matches_as_team2')
|
||||
participants = db.relationship('MatchParticipant', backref='match', lazy='dynamic')
|
||||
|
||||
def get_participating_players(self):
|
||||
return [p.player_id for p in self.participants.all()]
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Regular-season match for an organisation team (not tied to a tryout)."""
|
||||
from app.extensions import db
|
||||
from app.models.match_model.base import BaseMatch
|
||||
|
||||
|
||||
class TeamMatch(BaseMatch):
|
||||
"""Regular-season match for an organisation team (not tied to a tryout)."""
|
||||
__tablename__ = 'team_matches'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False)
|
||||
opponent = db.Column(db.String(200), nullable=True)
|
||||
|
||||
org_team = db.relationship('OrgTeam', backref='team_matches')
|
||||
creator = db.relationship('User', backref='created_team_matches')
|
||||
participants = db.relationship(
|
||||
'TeamMatchParticipant', backref='team_match', lazy='dynamic',
|
||||
cascade='all, delete-orphan')
|
||||
|
||||
def get_confirmed_count(self):
|
||||
all_p = self.participants.all()
|
||||
confirmed = sum(1 for p in all_p if p.is_confirmed)
|
||||
return confirmed, len(all_p)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Request from player to coach for a One on One session."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class OneOnOneRequest(db.Model):
|
||||
"""Request from player to coach for a One on One session."""
|
||||
__tablename__ = 'one_on_one_requests'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
||||
date = db.Column(db.Date, nullable=False)
|
||||
start_time = db.Column(db.Time, nullable=False)
|
||||
end_time = db.Column(db.Time, nullable=False)
|
||||
points = db.Column(db.Text, nullable=True)
|
||||
status = db.Column(db.String(20), default='pending')
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
responded_at = db.Column(db.DateTime, nullable=True)
|
||||
discord_message_id = db.Column(db.BigInteger, nullable=True)
|
||||
coach_rejection_message = db.Column(db.Text, nullable=True)
|
||||
|
||||
player = db.relationship('User', foreign_keys=[player_id], backref='one_on_one_requests')
|
||||
coach = db.relationship('User', foreign_keys=[coach_id])
|
||||
team = db.relationship('OrgTeam', foreign_keys=[org_team_id])
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Organisation team models."""
|
||||
from app.models.org_team.org_team import OrgTeam
|
||||
from app.models.org_team.team_player import TeamPlayer
|
||||
|
||||
__all__ = ['OrgTeam', 'TeamPlayer']
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Persistent organisation team (e.g. Varsity, JV)."""
|
||||
from app.extensions import db
|
||||
from app.models._associations import org_team_coaches, org_team_managers
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class OrgTeam(db.Model):
|
||||
"""Persistent organisation team (e.g. Varsity, JV)."""
|
||||
__tablename__ = 'org_teams'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False, unique=True)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
|
||||
creator = db.relationship('User', foreign_keys=[created_by])
|
||||
coaches = db.relationship(
|
||||
'User', secondary=org_team_coaches, lazy='dynamic',
|
||||
backref=db.backref('coached_org_teams', lazy='dynamic'))
|
||||
managers = db.relationship(
|
||||
'User', secondary=org_team_managers, lazy='dynamic',
|
||||
backref=db.backref('managed_org_teams', lazy='dynamic'))
|
||||
|
||||
coach = db.relationship(
|
||||
'User', foreign_keys=[coach_id],
|
||||
backref=db.backref('coached_org_team_legacy', uselist=False),
|
||||
viewonly=True)
|
||||
manager = db.relationship(
|
||||
'User', foreign_keys=[manager_id],
|
||||
backref=db.backref('managed_org_team_legacy', uselist=False),
|
||||
viewonly=True)
|
||||
|
||||
def get_coaches(self):
|
||||
coach_list = self.coaches.all()
|
||||
if not coach_list and self.coach:
|
||||
return [self.coach]
|
||||
return coach_list
|
||||
|
||||
def get_managers(self):
|
||||
manager_list = self.managers.all()
|
||||
if not manager_list and self.manager:
|
||||
return [self.manager]
|
||||
return manager_list
|
||||
|
||||
@property
|
||||
def players(self):
|
||||
return [tp.player for tp in self.team_players]
|
||||
|
||||
def get_players_with_status(self):
|
||||
return [{'player': tp.player, 'status': tp.status,
|
||||
'position': tp.position} for tp in self.team_players]
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Many-to-many junction: player to org-team."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TeamPlayer(db.Model):
|
||||
"""Many-to-many: player to org-team."""
|
||||
__tablename__ = 'team_players'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False)
|
||||
status = db.Column(db.String(20), nullable=False, default='starter')
|
||||
position = db.Column(db.String(50), nullable=True)
|
||||
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
player = db.relationship('User', foreign_keys=[player_id], backref='team_placements')
|
||||
org_team = db.relationship('OrgTeam', foreign_keys=[org_team_id], backref='team_players')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('player_id', 'org_team_id', name='unique_player_org_team'),
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Participant models — BaseParticipant and its concrete subclasses."""
|
||||
from app.models.participant.base import BaseParticipant
|
||||
from app.models.participant.match_participant import MatchParticipant
|
||||
from app.models.participant.team_match_participant import TeamMatchParticipant
|
||||
|
||||
__all__ = ['BaseParticipant', 'MatchParticipant', 'TeamMatchParticipant']
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Abstract base class for match participant models."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BaseParticipant(db.Model):
|
||||
"""Shared schema for match participants."""
|
||||
__abstract__ = True
|
||||
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Participant in a tryout-scoped match."""
|
||||
from app.extensions import db
|
||||
from app.models.participant.base import BaseParticipant
|
||||
|
||||
|
||||
class MatchParticipant(BaseParticipant):
|
||||
"""Participant in a tryout-scoped match."""
|
||||
__tablename__ = 'match_participants'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=False)
|
||||
team_side = db.Column(db.Integer, nullable=True)
|
||||
position = db.Column(db.String(50), nullable=True)
|
||||
attendance_confirmed = db.Column(db.Boolean, default=False)
|
||||
|
||||
player = db.relationship('User')
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Participant in a regular-season team match."""
|
||||
from app.extensions import db
|
||||
from app.models.participant.base import BaseParticipant
|
||||
|
||||
|
||||
class TeamMatchParticipant(BaseParticipant):
|
||||
"""Participant in a regular-season team match."""
|
||||
__tablename__ = 'team_match_participants'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
team_match_id = db.Column(db.Integer, db.ForeignKey('team_matches.id'), nullable=False)
|
||||
is_confirmed = db.Column(db.Boolean, default=False)
|
||||
|
||||
player = db.relationship('User')
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Personal notes from coach to individual player."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class PersonalNote(db.Model):
|
||||
"""Personal notes from coach to individual player."""
|
||||
__tablename__ = 'personal_notes'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=True)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=True)
|
||||
|
||||
player = db.relationship('User', foreign_keys=[player_id], backref='personal_notes')
|
||||
coach = db.relationship('User', foreign_keys=[coach_id])
|
||||
match = db.relationship('Match', foreign_keys=[match_id])
|
||||
team = db.relationship('Team', foreign_keys=[team_id])
|
||||
tryout = db.relationship('Tryout', foreign_keys=[tryout_id])
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Tryout-specific temporary team models."""
|
||||
from app.models.team.team import Team
|
||||
from app.models.team.team_member import TeamMember
|
||||
|
||||
__all__ = ['Team', 'TeamMember']
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Tryout-specific team (e.g. Alpha, Bravo within a single tryout)."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Team(db.Model):
|
||||
"""Tryout-specific team (e.g. Alpha, Bravo within a single tryout)."""
|
||||
__tablename__ = 'teams'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
creator = db.relationship('User', backref='created_teams')
|
||||
members = db.relationship('TeamMember', backref='team', lazy='dynamic')
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Link between a player and a tryout-specific team."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TeamMember(db.Model):
|
||||
"""Link between a player and a tryout-specific team."""
|
||||
__tablename__ = 'team_members'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
position = db.Column(db.String(50), nullable=True)
|
||||
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
player = db.relationship('User', overlaps="player_ref,team_assignments")
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Team improvement notes from coach."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TeamNote(db.Model):
|
||||
"""Team improvement notes from coach."""
|
||||
__tablename__ = 'team_notes'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False)
|
||||
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
team = db.relationship('OrgTeam', backref='team_notes')
|
||||
coach = db.relationship('User', foreign_keys=[coach_id])
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Tryout models."""
|
||||
from app.models.tryout.tryout import Tryout
|
||||
from app.models.tryout.tryout_registration import TryoutRegistration
|
||||
|
||||
__all__ = ['Tryout', 'TryoutRegistration']
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Tryout event for player evaluations and team formation."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Tryout(db.Model):
|
||||
"""Tryout event for player evaluations and team formation."""
|
||||
__tablename__ = 'tryouts'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
game = db.Column(db.String(50), nullable=False)
|
||||
date = db.Column(db.Date, nullable=False)
|
||||
location = db.Column(db.String(200), nullable=True)
|
||||
status = db.Column(db.String(20), default='upcoming')
|
||||
max_players = db.Column(db.Integer, nullable=True)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
target_org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
||||
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
creator = db.relationship('User', foreign_keys=[created_by], backref='created_tryouts')
|
||||
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_tryouts')
|
||||
coach = db.relationship('User', foreign_keys=[coach_id], backref='coached_tryouts')
|
||||
registrations = db.relationship('TryoutRegistration', backref='tryout', lazy='dynamic')
|
||||
evaluations = db.relationship('Evaluation', backref='tryout', lazy='dynamic')
|
||||
teams = db.relationship('Team', backref='tryout', lazy='dynamic')
|
||||
target_org_team = db.relationship('OrgTeam', backref='tryouts', foreign_keys=[target_org_team_id])
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Registration linking a player to a tryout."""
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TryoutRegistration(db.Model):
|
||||
"""Registration linking a player to a tryout."""
|
||||
__tablename__ = 'tryout_registrations'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
registered_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
status = db.Column(db.String(20), default='registered')
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Store gamertag per game for each user."""
|
||||
from app.extensions import db
|
||||
from app.models._constants import TRN_URLS, PLATFORM_CODES
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
class UserGamertag(db.Model):
|
||||
"""Store gamertag per game for each user."""
|
||||
__tablename__ = 'user_gamertags'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
game = db.Column(db.String(50), nullable=False)
|
||||
gamertag = db.Column(db.String(120), nullable=False)
|
||||
platform = db.Column(db.String(30), nullable=True)
|
||||
|
||||
user = db.relationship('User', backref='gamertags')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('user_id', 'game', name='unique_user_game'),
|
||||
)
|
||||
|
||||
def get_trn_url(self):
|
||||
if self.game not in TRN_URLS:
|
||||
return None
|
||||
url = TRN_URLS[self.game]
|
||||
encoded_gamertag = quote(self.gamertag, safe='')
|
||||
if '{platform_code}' in url and '{username}' in url:
|
||||
platform_code = PLATFORM_CODES.get(
|
||||
self.platform,
|
||||
self.platform.lower().replace(' ', '-') if self.platform else '',
|
||||
)
|
||||
return url.format(platform_code=platform_code, username=encoded_gamertag)
|
||||
elif '{platform}' in url and '{username}' in url:
|
||||
return url.format(
|
||||
platform=self.platform.lower().replace(' ', '-'),
|
||||
username=encoded_gamertag,
|
||||
)
|
||||
elif '{username}' in url:
|
||||
return url.format(username=encoded_gamertag)
|
||||
return url
|
||||
@@ -0,0 +1,9 @@
|
||||
"""User hierarchy — single-table polymorphic inheritance (User → Admin, Manager, Coach, Player, Scout)."""
|
||||
from app.models.user_model.user import User
|
||||
from app.models.user_model.admin import Admin
|
||||
from app.models.user_model.manager import Manager
|
||||
from app.models.user_model.coach import Coach
|
||||
from app.models.user_model.player import Player
|
||||
from app.models.user_model.scout import Scout
|
||||
|
||||
__all__ = ['User', 'Admin', 'Manager', 'Coach', 'Player', 'Scout']
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Admin / President — full access to everything."""
|
||||
from app.models.user_model.user import User
|
||||
|
||||
|
||||
class Admin(User):
|
||||
"""President / super-admin — full access to everything."""
|
||||
__mapper_args__ = {'polymorphic_identity': 'admin'}
|
||||
|
||||
def can_evaluate(self):
|
||||
return True
|
||||
|
||||
def can_manage_users(self):
|
||||
return True
|
||||
|
||||
def can_manage_teams(self):
|
||||
return True
|
||||
|
||||
def can_manage_tryouts(self):
|
||||
return True
|
||||
|
||||
def can_schedule_matches(self):
|
||||
return True
|
||||
|
||||
def can_manage_this_tryout(self, tryout):
|
||||
return True
|
||||
|
||||
def can_manage_this_org_team(self, org_team):
|
||||
return True
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
from app.models.tryout.tryout import Tryout
|
||||
return Tryout.query.order_by(Tryout.date).all()
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Coach — evaluates, schedules matches, manages their own org team."""
|
||||
from app.models.user_model.user import User
|
||||
|
||||
|
||||
class Coach(User):
|
||||
"""Coach — evaluates, schedules matches, manages their own org team."""
|
||||
__mapper_args__ = {'polymorphic_identity': 'coach'}
|
||||
|
||||
def can_evaluate(self):
|
||||
return True
|
||||
|
||||
def can_schedule_matches(self):
|
||||
return True
|
||||
|
||||
def can_manage_tryouts(self):
|
||||
return True
|
||||
|
||||
def can_manage_this_tryout(self, tryout):
|
||||
from app.models.org_team.org_team import OrgTeam
|
||||
if tryout.target_org_team_id:
|
||||
is_coach_of_target = OrgTeam.query.filter(
|
||||
OrgTeam.id == tryout.target_org_team_id,
|
||||
OrgTeam.coaches.any(id=self.id),
|
||||
).first() is not None
|
||||
if is_coach_of_target:
|
||||
return True
|
||||
if tryout.coach_id == self.id:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_manage_this_org_team(self, org_team):
|
||||
if org_team.coaches.filter_by(id=self.id).first():
|
||||
return True
|
||||
if org_team.coach_id == self.id:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
from app.models.tryout.tryout import Tryout
|
||||
team_ids = [t.id for t in self.coached_org_teams.all()]
|
||||
if not team_ids:
|
||||
return Tryout.query.filter(Tryout.id == -1).all() # empty
|
||||
return Tryout.query.filter(
|
||||
Tryout.target_org_team_id.in_(team_ids)
|
||||
).order_by(Tryout.date).all()
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Manager — manages own tryouts, all org teams, all contracts."""
|
||||
from app.models.user_model.user import User
|
||||
|
||||
|
||||
class Manager(User):
|
||||
"""Manager — manages own tryouts, all org teams, all contracts."""
|
||||
__mapper_args__ = {'polymorphic_identity': 'manager'}
|
||||
|
||||
def can_evaluate(self):
|
||||
return True
|
||||
|
||||
def can_manage_teams(self):
|
||||
return True
|
||||
|
||||
def can_manage_tryouts(self):
|
||||
return True
|
||||
|
||||
def can_schedule_matches(self):
|
||||
return True
|
||||
|
||||
def can_manage_this_tryout(self, tryout):
|
||||
return tryout.created_by == self.id or tryout.manager_id == self.id
|
||||
|
||||
def can_manage_this_org_team(self, org_team):
|
||||
return True
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
from app.models.tryout.tryout import Tryout
|
||||
return Tryout.query.filter_by(created_by=self.id).order_by(Tryout.date).all()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Player — registers for tryouts, manages their own profile."""
|
||||
from app.models.user_model.user import User
|
||||
|
||||
|
||||
class Player(User):
|
||||
"""Player — registers for tryouts, manages their own profile."""
|
||||
__mapper_args__ = {'polymorphic_identity': 'player'}
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
from app.models.tryout.tryout import Tryout
|
||||
from app.models.match_model.match import Match
|
||||
from app.models.participant.match_participant import MatchParticipant
|
||||
|
||||
# tryouts they registered for
|
||||
player_tryout_ids = [r.tryout_id for r in self.tryout_registrations.all()]
|
||||
tryouts = Tryout.query.filter(
|
||||
Tryout.id.in_(player_tryout_ids)
|
||||
).order_by(Tryout.date).all() if player_tryout_ids else []
|
||||
|
||||
# plus tryouts where they participate in a match
|
||||
player_matches = Match.query.join(MatchParticipant).filter(
|
||||
MatchParticipant.player_id == self.id,
|
||||
).all()
|
||||
extra_ids = set(m.tryout_id for m in player_matches)
|
||||
extra = Tryout.query.filter(
|
||||
Tryout.id.in_(extra_ids),
|
||||
).order_by(Tryout.date).all() if extra_ids else []
|
||||
|
||||
all_ids = {t.id for t in tryouts}
|
||||
return tryouts + [t for t in extra if t.id not in all_ids]
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Scout — view-only access to tryouts and evaluations."""
|
||||
from app.models.user_model.user import User
|
||||
|
||||
|
||||
class Scout(User):
|
||||
"""Scout — view-only access to tryouts and evaluations."""
|
||||
__mapper_args__ = {'polymorphic_identity': 'scout'}
|
||||
|
||||
def can_evaluate(self):
|
||||
return True
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
from app.models.tryout.tryout import Tryout
|
||||
return Tryout.query.order_by(Tryout.date).all()
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Base User model — shared fields and polymorphic configuration."""
|
||||
from app.extensions import db
|
||||
from flask_login import UserMixin
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
"""Base user model — shared fields for every role.
|
||||
|
||||
Do not instantiate this class directly; use Admin, Manager, Coach, Player,
|
||||
or Scout so that `polymorphic_identity` is set correctly.
|
||||
"""
|
||||
__tablename__ = 'users'
|
||||
|
||||
# --- columns -----------------------------------------------------------
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
role = db.Column(db.String(20), nullable=False, default='player') # polymorphic discriminator
|
||||
full_name = db.Column(db.String(100), nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
phone = db.Column(db.String(20), nullable=True)
|
||||
is_active_account = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
failed_login_attempts = db.Column(db.Integer, default=0)
|
||||
locked_until = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# E-Sports fields
|
||||
games = db.Column(db.Text, nullable=True) # comma-separated (only meaningful for Player)
|
||||
discord_username = db.Column(db.String(128), nullable=True)
|
||||
discord_user_id = db.Column(db.String(64), nullable=True)
|
||||
league_os_profile = db.Column(db.String(256), nullable=True)
|
||||
|
||||
# --- polymorphic configuration -----------------------------------------
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'user',
|
||||
'polymorphic_on': role,
|
||||
}
|
||||
|
||||
# --- relationships (defined once on the base) --------------------------
|
||||
evaluations_given = db.relationship(
|
||||
'Evaluation', foreign_keys='Evaluation.evaluator_id',
|
||||
backref='evaluator', lazy='dynamic')
|
||||
evaluations_received = db.relationship(
|
||||
'Evaluation', foreign_keys='Evaluation.player_id',
|
||||
backref='player', lazy='dynamic')
|
||||
tryout_registrations = db.relationship(
|
||||
'TryoutRegistration', backref='player', lazy='dynamic')
|
||||
team_assignments = db.relationship(
|
||||
'TeamMember', foreign_keys='TeamMember.player_id',
|
||||
backref='player_ref', lazy='dynamic')
|
||||
|
||||
# --- shared helper methods ---------------------------------------------
|
||||
def get_games_list(self):
|
||||
"""Return the user's games as a list."""
|
||||
if self.games:
|
||||
return [g.strip() for g in self.games.split(',') if g.strip()]
|
||||
return []
|
||||
|
||||
def get_gamertags(self):
|
||||
"""Return gamertags as a dict keyed by game."""
|
||||
return {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform}
|
||||
for gt in self.gamertags}
|
||||
|
||||
def get_org_teams(self):
|
||||
"""Return all OrgTeams this player belongs to."""
|
||||
return [tp.org_team for tp in self.team_placements]
|
||||
|
||||
# --- stubs (overridden in subclasses) ----------------------------------
|
||||
def can_evaluate(self):
|
||||
return False
|
||||
|
||||
def can_manage_users(self):
|
||||
return False
|
||||
|
||||
def can_manage_teams(self):
|
||||
return False
|
||||
|
||||
def can_manage_tryouts(self):
|
||||
return False
|
||||
|
||||
def can_schedule_matches(self):
|
||||
return False
|
||||
|
||||
def can_manage_this_tryout(self, tryout):
|
||||
return False
|
||||
|
||||
def can_manage_this_org_team(self, org_team):
|
||||
return False
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
return []
|
||||
@@ -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 == 'president':
|
||||
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,13 +192,15 @@ def evaluate_player(tryout_id, player_id):
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
evaluators = None
|
||||
if current_user.role == 'president':
|
||||
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,
|
||||
tryout=tryout, player=player,
|
||||
existing_eval=existing_eval,
|
||||
evaluators=evaluators,
|
||||
game_positions=GAME_POSITIONS)
|
||||
@@ -242,24 +209,12 @@ def evaluate_player(tryout_id, player_id):
|
||||
@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'))
|
||||
@@ -268,12 +223,11 @@ def players_to_evaluate(tryout_id):
|
||||
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)
|
||||
@@ -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'))
|
||||
|
||||
|
||||
@@ -30,20 +28,12 @@ def index():
|
||||
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 == 'president':
|
||||
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()
|
||||
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
|
||||
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)
|
||||
@@ -1,51 +1,47 @@
|
||||
"""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).
|
||||
"""Check if user can schedule matches (Admin, Manager, Coach, Scout)."""
|
||||
return isinstance(current_user, (Admin, Manager, Coach, Scout))
|
||||
|
||||
Returns:
|
||||
bool: True if user is president, manager, coach, or 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 ['president', '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:
|
||||
@@ -53,21 +49,17 @@ def api_events():
|
||||
'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,43 +71,34 @@ 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)
|
||||
@@ -124,63 +107,40 @@ def api_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,27 +150,16 @@ 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
|
||||
|
||||
@@ -218,84 +167,24 @@ def api_events_for_tryout(tryout_id):
|
||||
'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 == 'president':
|
||||
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))
|
||||
@@ -304,8 +193,6 @@ def create_match(tryout_id):
|
||||
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':
|
||||
@@ -317,26 +204,25 @@ def create_match(tryout_id):
|
||||
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()
|
||||
@@ -345,30 +231,21 @@ def create_match(tryout_id):
|
||||
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
|
||||
db.session.flush()
|
||||
|
||||
# Collect player IDs for Discord notifications
|
||||
notified_player_ids = []
|
||||
notified_participant_ids = []
|
||||
|
||||
# Handle team vs team matches
|
||||
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,11 +276,8 @@ 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)
|
||||
@@ -416,54 +287,28 @@ def create_match(tryout_id):
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -472,12 +317,10 @@ def edit_match(match_id):
|
||||
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()]
|
||||
|
||||
@@ -494,20 +337,21 @@ def edit_match(match_id):
|
||||
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)
|
||||
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
|
||||
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()
|
||||
@@ -518,18 +362,15 @@ def edit_match(match_id):
|
||||
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,35 +390,30 @@ 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_player_ids = request.form.getlist('team1_player_ids')
|
||||
team2_player_ids = request.form.getlist('team2_player_ids')
|
||||
notified_participant_ids = []
|
||||
for pid in team1_player_ids:
|
||||
team1_str = request.form.get('team1_player_ids', '')
|
||||
team2_str = request.form.get('team2_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)
|
||||
@@ -587,9 +423,8 @@ def edit_match(match_id):
|
||||
|
||||
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
|
||||
# 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:
|
||||
@@ -598,67 +433,34 @@ def edit_match(match_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
|
||||
)
|
||||
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
|
||||
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([])
|
||||
|
||||
@@ -667,9 +469,8 @@ def api_manageable_tryouts():
|
||||
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)
|
||||
|
||||
@@ -677,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')
|
||||
@@ -699,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()
|
||||
date_for_day = datetime.strptime(date_str, '%Y-%m-%d')
|
||||
day_of_week = date_for_day.weekday()
|
||||
|
||||
# Convert Python weekday (Mon=0) to our format (Mon=0)
|
||||
day_of_week = js_day
|
||||
|
||||
# Get all active players
|
||||
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})
|
||||
|
||||
@@ -772,17 +530,7 @@ 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
|
||||
|
||||
@@ -790,16 +538,14 @@ def toggle_presence(match_id, 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 ['president']:
|
||||
"""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 == 'president':
|
||||
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,
|
||||
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
|
||||
@@ -146,13 +114,9 @@ def create_match(team_id):
|
||||
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)
|
||||
@@ -165,13 +129,16 @@ def create_match(team_id):
|
||||
|
||||
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
|
||||
@@ -186,28 +153,24 @@ 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
|
||||
db.session.flush()
|
||||
|
||||
# Auto-add all team players as participants
|
||||
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()
|
||||
@@ -215,19 +178,17 @@ def create_match(team_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')
|
||||
@@ -239,17 +200,7 @@ def create_match(team_id):
|
||||
@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
|
||||
|
||||
@@ -285,7 +236,6 @@ def edit_match(match_id):
|
||||
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
|
||||
@@ -295,29 +245,18 @@ def edit_match(match_id):
|
||||
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,23 +266,17 @@ 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 ['president', '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:
|
||||
@@ -355,17 +288,7 @@ def api_manageable_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
|
||||
|
||||
@@ -373,16 +296,14 @@ def toggle_presence(match_id, 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',
|
||||
})
|
||||
@@ -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,31 +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 == 'coach':
|
||||
if isinstance(current_user, Admin):
|
||||
teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
||||
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:
|
||||
@@ -48,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'))
|
||||
@@ -140,7 +119,7 @@ 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()
|
||||
@@ -181,7 +160,28 @@ def edit_team(team_id):
|
||||
flash(f'Team "{name}" already exists.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
team.name = name
|
||||
if request.form.get('sync_staff') == '1':
|
||||
coach_ids = request.form.getlist('coach_ids')
|
||||
manager_ids = request.form.getlist('manager_ids')
|
||||
|
||||
team.coaches = []
|
||||
for cid in coach_ids:
|
||||
if cid and cid.strip():
|
||||
coach_user = User.query.get(int(cid))
|
||||
if coach_user and isinstance(coach_user, Coach):
|
||||
team.coaches.append(coach_user)
|
||||
coach_list = team.coaches.all()
|
||||
team.coach_id = coach_list[0].id if coach_list else None
|
||||
|
||||
team.managers = []
|
||||
for mid in manager_ids:
|
||||
if mid and mid.strip():
|
||||
manager_user = User.query.get(int(mid))
|
||||
if manager_user and isinstance(manager_user, Manager):
|
||||
team.managers.append(manager_user)
|
||||
manager_list = team.managers.all()
|
||||
team.manager_id = manager_list[0].id if manager_list else None
|
||||
else:
|
||||
team.coach_id = int(coach_id) if coach_id else None
|
||||
team.manager_id = int(manager_id) if manager_id else None
|
||||
|
||||
@@ -210,9 +210,7 @@ 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()
|
||||
@@ -229,7 +227,7 @@ 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')
|
||||
@@ -241,7 +239,7 @@ def add_coach(team_id):
|
||||
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'))
|
||||
|
||||
@@ -260,7 +258,7 @@ 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')
|
||||
@@ -272,7 +270,7 @@ def add_manager(team_id):
|
||||
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'))
|
||||
|
||||
@@ -346,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'))
|
||||
|
||||
@@ -363,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')
|
||||
@@ -384,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')
|
||||
@@ -410,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'))
|
||||
|
||||
@@ -465,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'))
|
||||
@@ -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 ['president', '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 == 'president':
|
||||
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 == 'president':
|
||||
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,97 +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)
|
||||
|
||||
player_presence = []
|
||||
for p in all_participants:
|
||||
if p.player:
|
||||
player_presence.append({
|
||||
'participant_id': p.id, 'player_id': p.player_id,
|
||||
'player_name': p.player.username,
|
||||
'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
|
||||
'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))
|
||||
|
||||
@@ -342,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))
|
||||
@@ -363,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')
|
||||
@@ -388,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
|
||||
@@ -414,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))
|
||||
@@ -460,18 +361,7 @@ 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')
|
||||
@@ -479,27 +369,23 @@ def remove_player(tryout_id, player_id):
|
||||
|
||||
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()
|
||||
@@ -510,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')
|
||||
@@ -535,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):
|
||||
@@ -552,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')
|
||||
@@ -561,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))
|
||||
@@ -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)
|
||||
@@ -1231,9 +1231,9 @@ a:hover { color: var(--primary-dark); }
|
||||
}
|
||||
|
||||
.merged-disponibility-time-block.selected {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
border-color: var(--success);
|
||||
background: #6366f1 !important;
|
||||
color: white !important;
|
||||
border-color: #4f46e5 !important;
|
||||
}
|
||||
|
||||
.merged-disponibility-time-block.high-availability {
|
||||
@@ -1266,10 +1266,10 @@ a:hover { color: var(--primary-dark); }
|
||||
|
||||
/* Then specific states override the generic rule */
|
||||
[data-theme="dark"] .merged-disponibility-time-block.selected {
|
||||
background: var(--success) !important;
|
||||
background: #6366f1 !important;
|
||||
color: white !important;
|
||||
border-color: var(--success) !important;
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.5);
|
||||
border-color: #818cf8 !important;
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .merged-disponibility-time-block.high-availability {
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('users.profile') }}">Profile</a> / Contracts</span>{% endblock %}
|
||||
|
||||
{% block header_actions %}
|
||||
{% if current_user.role in ['president', 'manager', 'coach'] %}
|
||||
{% if current_user.role in ['admin', 'manager', 'coach'] %}
|
||||
<a href="{{ url_for('users.upload_contract') }}" class="btn btn-primary">
|
||||
<i class="fas fa-upload"></i> Upload Contract
|
||||
</a>
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="dashboard">
|
||||
{% if user.role == 'president' %}
|
||||
{% if user.role == 'admin' %}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-primary">
|
||||
@@ -21,7 +21,7 @@
|
||||
</th>
|
||||
{% endmacro %}
|
||||
|
||||
{% if current_user.role == 'president' and player_scores %}
|
||||
{% if current_user.role == 'admin' and player_scores %}
|
||||
<div class="stats-grid mb-4">
|
||||
{% for pid, data in player_scores.items() %}
|
||||
<div class="stat-card stat-card-sm">
|
||||
@@ -75,7 +75,12 @@
|
||||
{% if current_user.can_manage_teams() or current_user.can_schedule_matches() %}
|
||||
<hr class="section-divider">
|
||||
<h4 class="section-title"><i class="fas fa-clock"></i> Select Match Time</h4>
|
||||
<p class="text-muted small">Click time slots consecutively to set match duration. Players available in all selected time blocks are shown below.</p>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
||||
<p class="text-muted small" style="margin:0;">Click time slots consecutively to set match duration. Players available in all selected time blocks are shown below.</p>
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="clearTimeSelection()" title="Reset time selection">
|
||||
<i class="fas fa-undo"></i> Reset Time
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="merged-disponibility-grid" class="merged-disponibility-grid">
|
||||
<p class="text-muted">Loading...</p>
|
||||
@@ -77,12 +77,11 @@
|
||||
<!-- Player presence details -->
|
||||
<div class="presence-players">
|
||||
{% for p in item.participants %}
|
||||
<a href="{{ url_for('users.view_user', user_id=p.player.id) }}" class="presence-player-tag {% if p.is_confirmed %}confirmed{% else %}pending{% endif %}"
|
||||
title="{{ p.player.username }}{% if p.is_confirmed %} - Confirmed{% else %} - Pending{% endif %}"
|
||||
style="text-decoration: none;">
|
||||
<span class="presence-player-tag {% if p.is_confirmed %}confirmed{% else %}pending{% endif %}"
|
||||
title="{{ p.player.username }}{% if p.is_confirmed %} - Confirmed{% else %} - Pending{% endif %}">
|
||||
{{ p.player.username[:2] | upper }} {{ p.player.username }}
|
||||
{% if p.is_confirmed %}✅{% else %}⏳{% endif %}
|
||||
</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -93,7 +92,7 @@
|
||||
<span class="badge badge-{{ m.status }}">{{ m.status }}</span>
|
||||
</td>
|
||||
<td class="eval-actions">
|
||||
{% set can_manage_this = (current_user.role in ['president', 'manager']) or (current_user.role == 'coach' and m.org_team.coaches.filter_by(id=current_user.id).first()) or (current_user.role == 'coach' and m.org_team.coach_id == current_user.id) %}
|
||||
{% set can_manage_this = (current_user.role in ['admin', 'manager']) or (current_user.role == 'coach' and m.org_team.coaches.filter_by(id=current_user.id).first()) or (current_user.role == 'coach' and m.org_team.coach_id == current_user.id) %}
|
||||
{% if can_manage_this %}
|
||||
<a href="{{ url_for('team_matches.edit_match', match_id=m.id) }}" class="btn btn-sm btn-outline" title="Edit Match">
|
||||
<i class="fas fa-edit"></i>
|
||||
@@ -124,36 +124,10 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if can_manage %}
|
||||
<div class="staff-add-forms">
|
||||
<form method="POST" action="{{ url_for('teams.add_coach', team_id=team.id) }}" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<select name="coach_id" class="form-select form-select-sm" onchange="if(this.value) this.form.submit()">
|
||||
<option value="">+ Add Coach</option>
|
||||
{% for c in coaches %}
|
||||
{% if c.id not in (team_coaches | map(attribute='id') | list) %}
|
||||
<option value="{{ c.id }}">{{ c.username }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('teams.add_manager', team_id=team.id) }}" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<select name="manager_id" class="form-select form-select-sm" onchange="if(this.value) this.form.submit()">
|
||||
<option value="">+ Add Manager</option>
|
||||
{% for m in managers %}
|
||||
{% if m.id not in (team_managers | map(attribute='id') | list) %}
|
||||
<option value="{{ m.id }}">{{ m.username }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('team_matches.list_matches') }}?team_id={{ team.id }}" class="btn btn-sm btn-outline" title="View Team Matches">
|
||||
<i class="fas fa-futbol"></i> Matches
|
||||
</a>
|
||||
{% if current_user.can_schedule_matches() and (current_user.role in ['president', 'manager'] or (current_user.role == 'coach' and team.coaches.filter_by(id=current_user.id).first()) or (current_user.role == 'coach' and team.coach_id == current_user.id)) %}
|
||||
{% if current_user.can_schedule_matches() and (current_user.role in ['admin', 'manager'] or (current_user.role == 'coach' and team.coaches.filter_by(id=current_user.id).first()) or (current_user.role == 'coach' and team.coach_id == current_user.id)) %}
|
||||
<a href="{{ url_for('team_matches.create_match', team_id=team.id) }}" class="btn btn-sm btn-success" title="Schedule Team Match">
|
||||
<i class="fas fa-plus"></i> Match
|
||||
</a>
|
||||
@@ -286,28 +260,37 @@
|
||||
<div class="modal-body">
|
||||
<form id="editTeamForm" method="POST" action="" class="form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="sync_staff" value="1"/>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit_name">Team Name</label>
|
||||
<input type="text" id="edit_name" name="name" required>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- Manage Coaches -->
|
||||
<h5 class="mb-2"><i class="fas fa-chalkboard-teacher"></i> Coaches</h5>
|
||||
<div class="form-group">
|
||||
<label for="edit_coach_id">Assigned Coach</label>
|
||||
<select id="edit_coach_id" name="coach_id" class="form-select">
|
||||
<option value="">-- No coach assigned --</option>
|
||||
<select name="coach_ids" id="edit-coach-select" class="form-select" multiple style="min-height: 100px; width: 100%;">
|
||||
{% for coach in coaches %}
|
||||
<option value="{{ coach.id }}">{{ coach.username }}</option>
|
||||
<option value="{{ coach.id }}" class="coach-option">{{ coach.username }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text">Hold Ctrl/Cmd to select multiple. Only unassigned coaches shown.</small>
|
||||
</div>
|
||||
|
||||
<!-- Manage Managers -->
|
||||
<h5 class="mb-2"><i class="fas fa-user-tie"></i> Managers</h5>
|
||||
<div class="form-group">
|
||||
<label for="edit_manager_id">Assigned Manager</label>
|
||||
<select id="edit_manager_id" name="manager_id" class="form-select">
|
||||
<option value="">-- No manager assigned --</option>
|
||||
<select name="manager_ids" id="edit-manager-select" class="form-select" multiple style="min-height: 100px; width: 100%;">
|
||||
{% for manager in managers %}
|
||||
<option value="{{ manager.id }}">{{ manager.username }}</option>
|
||||
<option value="{{ manager.id }}" class="manager-option">{{ manager.username }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text">Hold Ctrl/Cmd to select multiple. Only unassigned managers shown.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="hideEditForm()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
@@ -355,28 +338,68 @@ function toggleStatus(btn) {
|
||||
});
|
||||
}
|
||||
|
||||
var currentEditTeamId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.edit-team-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var teamId = this.getAttribute('data-team-id');
|
||||
currentEditTeamId = this.getAttribute('data-team-id');
|
||||
var teamName = this.getAttribute('data-team-name');
|
||||
var coachId = this.getAttribute('data-coach-id');
|
||||
var managerId = this.getAttribute('data-manager-id');
|
||||
document.getElementById('editTeamForm').action = '/teams/' + teamId + '/edit';
|
||||
document.getElementById('editTeamForm').action = '/teams/' + currentEditTeamId + '/edit';
|
||||
document.getElementById('edit_name').value = teamName;
|
||||
document.getElementById('edit_coach_id').value = coachId || '';
|
||||
document.getElementById('edit_manager_id').value = managerId || '';
|
||||
document.getElementById('editTeamModal').classList.remove('hidden');
|
||||
populateEditSelects(currentEditTeamId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function showEditForm(teamId, teamName, coachId, managerId) {
|
||||
document.getElementById('editTeamForm').action = '/teams/' + teamId + '/edit';
|
||||
document.getElementById('edit_name').value = teamName;
|
||||
document.getElementById('edit_coach_id').value = coachId || '';
|
||||
document.getElementById('edit_manager_id').value = managerId || '';
|
||||
document.getElementById('editTeamModal').classList.remove('hidden');
|
||||
function getCurrentStaffIds(teamId, type) {
|
||||
// Get currently assigned coach/manager IDs from the staff bar pills
|
||||
var ids = [];
|
||||
var teamCard = document.querySelector('[data-team-id="' + teamId + '"]');
|
||||
if (!teamCard) return ids;
|
||||
|
||||
var staffBar = teamCard.closest('.card').querySelector('.team-staff-bar');
|
||||
if (!staffBar) return ids;
|
||||
|
||||
var groupIndex = type === 'coach' ? 0 : 1;
|
||||
var group = staffBar.querySelectorAll('.staff-group')[groupIndex];
|
||||
if (!group) return ids;
|
||||
|
||||
group.querySelectorAll('.staff-tag form input[name="coach_id"], .staff-tag form input[name="manager_id"]').forEach(function(input) {
|
||||
ids.push(input.value);
|
||||
});
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
function populateEditSelects(teamId) {
|
||||
var coachSelect = document.getElementById('edit-coach-select');
|
||||
var managerSelect = document.getElementById('edit-manager-select');
|
||||
|
||||
var currentCoachIds = getCurrentStaffIds(teamId, 'coach');
|
||||
var currentManagerIds = getCurrentStaffIds(teamId, 'manager');
|
||||
|
||||
// Show all coaches, but pre-select current ones and hide non-assigned
|
||||
// Actually: show only currently-assigned options (pre-selected)
|
||||
coachSelect.querySelectorAll('.coach-option').forEach(function(opt) {
|
||||
var isAssigned = currentCoachIds.includes(opt.value);
|
||||
opt.selected = isAssigned;
|
||||
// Always show all options so user can add/remove
|
||||
opt.style.display = '';
|
||||
});
|
||||
|
||||
managerSelect.querySelectorAll('.manager-option').forEach(function(opt) {
|
||||
var isAssigned = currentManagerIds.includes(opt.value);
|
||||
opt.selected = isAssigned;
|
||||
opt.style.display = '';
|
||||
});
|
||||
|
||||
// Also update text to reflect current count
|
||||
document.querySelector('#edit-coach-select + .form-text').textContent =
|
||||
'Currently assigned: ' + currentCoachIds.length + '. Hold Ctrl/Cmd to select multiple.';
|
||||
document.querySelector('#edit-manager-select + .form-text').textContent =
|
||||
'Currently assigned: ' + currentManagerIds.length + '. Hold Ctrl/Cmd to select multiple.';
|
||||
}
|
||||
|
||||
function hideEditForm() {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user