Ajout d'une catégorie jeux dans le tryouts, chacun des jeux offre des rôles différents pour les membres d'équipes. Remodelage du code un peu et ajout de docu

This commit is contained in:
cedrick2711
2026-07-15 21:27:18 -04:00
parent a129f218b6
commit 195f3ce312
29 changed files with 1501 additions and 269 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22
View File
@@ -1,9 +1,30 @@
"""Team Tryouts Application - Flask Application Factory.
This module provides the application factory for creating and configuring
the Flask application instance.
"""
import os import os
from flask import Flask from flask import Flask
from extensions import db, login_manager, csrf, hash_password, check_password from extensions import db, login_manager, csrf, hash_password, check_password
from sqlalchemy import text from sqlalchemy import text
def create_app(): def create_app():
"""Create and configure the Flask application.
Initializes Flask with:
- Secret key for session security
- SQLite database configuration
- CSRF protection
- Login manager
- All route blueprints
Handles database initialization and seeding with sample data if empty.
Returns:
Flask: Configured Flask application instance.
"""
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'team-tryouts-secret-key-change-in-production') app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'team-tryouts-secret-key-change-in-production')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///team_tryouts.db' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///team_tryouts.db'
@@ -51,6 +72,7 @@ def create_app():
return app return app
if __name__ == '__main__': if __name__ == '__main__':
app = create_app() app = create_app()
app.run(debug=True, host='0.0.0.0', port=5000) app.run(debug=True, host='0.0.0.0', port=5000)
+24 -1
View File
@@ -3,14 +3,37 @@ from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect from flask_wtf.csrf import CSRFProtect
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
# Database and extension initialization
db = SQLAlchemy() db = SQLAlchemy()
login_manager = LoginManager() login_manager = LoginManager()
login_manager.login_view = 'auth.login' login_manager.login_view = 'auth.login'
login_manager.login_message_category = 'info' login_manager.login_message_category = 'info'
csrf = CSRFProtect() csrf = CSRFProtect()
def hash_password(password): def hash_password(password):
"""
Hash a plain text password using werkzeug's security functions.
Args:
password (str): The plain text password to hash.
Returns:
str: The hashed password string.
"""
return generate_password_hash(password) return generate_password_hash(password)
def check_password(password_hash, password): def check_password(password_hash, password):
return check_password_hash(password_hash, password) """
Verify a password against its hash.
Args:
password_hash (str): The stored password hash.
password (str): The plain text password to verify.
Returns:
bool: True if the password matches the hash, False otherwise.
"""
return check_password_hash(password_hash, password)
Binary file not shown.
+330 -45
View File
@@ -1,11 +1,19 @@
"""Database models for the Team Tryouts application.
This module defines all SQLAlchemy models including User, Tryout, Evaluation,
Team, Match, and Contract entities with their relationships and helper methods.
"""
from extensions import db, login_manager from extensions import db, login_manager
from flask_login import UserMixin from flask_login import UserMixin
from datetime import datetime from datetime import datetime
from urllib.parse import quote from urllib.parse import quote
# Available user roles in the system
ROLES = ['president', 'manager', 'coach', 'player', 'scout'] ROLES = ['president', 'manager', 'coach', 'player', 'scout']
# Popular E-Sports games list # Popular E-Sports games list for player profiles
ESPORT_GAMES = [ ESPORT_GAMES = [
'Valorant', 'Valorant',
'League of Legends', 'League of Legends',
@@ -13,15 +21,56 @@ ESPORT_GAMES = [
'Apex Legends', 'Apex Legends',
'Overwatch 2', 'Overwatch 2',
'Rainbow Six Siege', 'Rainbow Six Siege',
'Fortnite',
'Rocket League', 'Rocket League',
'Call of Duty', 'Super Smash Bros.'
'Dota 2',
'Super Smash Bros.',
'Street Fighter 6',
] ]
# Game-specific positions for tryouts
GAME_POSITIONS = {
'League of Legends': ['Top Lane', 'Jungle', 'Mid Lane', 'ADC', 'Support'],
'Valorant': ['Controller', 'Initiator', 'Duelist', 'Sentinel'],
'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.': []
}
@login_manager.user_loader
def load_user(user_id):
"""Load a user by ID for Flask-Login session management.
Args:
user_id (int): The user's unique identifier.
Returns:
User: The User object if found, None otherwise.
"""
return User.query.get(int(user_id))
class User(UserMixin, db.Model): class User(UserMixin, db.Model):
"""User model representing all users in the system.
Users can have different roles: president, manager, coach, player, or scout.
Each user has E-Sports specific fields for competitive gaming profiles.
Attributes:
id: Unique identifier for the user.
username: Unique username for login.
password_hash: Hashed password for authentication.
role: User role determining permissions.
full_name: User's display name.
email: User's email address.
phone: Optional phone number.
team_id: Foreign key to the user's organization team.
is_active_account: Whether the account is active.
games: Comma-separated list of games the user plays.
discord_username: User's Discord handle.
league_os_profile: Link to League OS profile.
"""
__tablename__ = 'users' __tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False) username = db.Column(db.String(80), unique=True, nullable=False)
@@ -36,8 +85,8 @@ class User(UserMixin, db.Model):
# E-Sports specific fields # E-Sports specific fields
games = db.Column(db.Text, nullable=True) # Comma-separated list of games games = db.Column(db.Text, nullable=True) # Comma-separated list of games
discord_username = db.Column(db.String(100), nullable=True) # Discord handle discord_username = db.Column(db.String(128), nullable=True) # Discord handle
league_os_profile = db.Column(db.String(255), nullable=True) # League OS profile URL or ID league_os_profile = db.Column(db.String(256), nullable=True) # League OS profile URL or ID
evaluations_given = db.relationship('Evaluation', foreign_keys='Evaluation.evaluator_id', backref='evaluator', lazy='dynamic') 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') evaluations_received = db.relationship('Evaluation', foreign_keys='Evaluation.player_id', backref='player', lazy='dynamic')
@@ -45,31 +94,78 @@ class User(UserMixin, db.Model):
team_assignments = db.relationship('TeamMember', foreign_keys='TeamMember.player_id', backref='player_ref', lazy='dynamic') team_assignments = db.relationship('TeamMember', foreign_keys='TeamMember.player_id', backref='player_ref', lazy='dynamic')
def get_games_list(self): def get_games_list(self):
"""Return games as a list.""" """Return the user's games as a list.
Returns:
list: List of game names the user plays, or empty list if none.
"""
if self.games: if self.games:
return [g.strip() for g in self.games.split(',') if g.strip()] return [g.strip() for g in self.games.split(',') if g.strip()]
return [] return []
def has_role(self, *roles): def has_role(self, *roles):
"""Check if the user has one of the specified roles.
Args:
*roles: Variable number of role names to check.
Returns:
bool: True if user has any of the specified roles.
"""
return self.role in roles return self.role in roles
def can_evaluate(self): def can_evaluate(self):
"""Check if user can evaluate other players.
Returns:
bool: True if user is president, manager, or coach.
"""
return self.role in ['president', 'manager', 'coach'] return self.role in ['president', 'manager', 'coach']
def can_manage_users(self): def can_manage_users(self):
"""Check if user can manage (create/delete) other users.
Returns:
bool: True if user is president.
"""
return self.role == 'president' return self.role == 'president'
def can_manage_tryouts(self): def can_manage_tryouts(self):
return self.role in ['president', 'manager', 'coach', 'scout'] """Check if user can manage tryouts.
Returns:
bool: True if user is president, manager or coach.
"""
return self.role in ['president', 'manager', 'coach']
def can_manage_teams(self): def can_manage_teams(self):
"""Check if user can manage organization teams.
Returns:
bool: True if user is president or manager.
"""
return self.role in ['president', 'manager'] return self.role in ['president', 'manager']
def can_schedule_matches(self): def can_schedule_matches(self):
"""Check if user can schedule matches.
Returns:
bool: True if user is president, manager, or coach.
"""
return self.role in ['president', 'manager', 'coach'] return self.role in ['president', 'manager', 'coach']
def can_manage_this_tryout(self, tryout): def can_manage_this_tryout(self, tryout):
"""Check if user can manage a specific tryout (president, or manager/coach in charge of it).""" """Check if user can manage a specific tryout.
Presidents can manage all tryouts. Managers can manage their own tryouts.
Coaches can manage tryouts targeting their coached team.
Args:
tryout: The Tryout object to check permissions for.
Returns:
bool: True if user has permission to manage the tryout.
"""
if self.role == 'president': if self.role == 'president':
return True return True
if self.role == 'manager' and tryout.created_by == self.id: if self.role == 'manager' and tryout.created_by == self.id:
@@ -81,7 +177,17 @@ class User(UserMixin, db.Model):
return False return False
def can_manage_this_org_team(self, org_team): def can_manage_this_org_team(self, org_team):
"""Check if user can manage a specific org team (president, or manager/coach in charge of it).""" """Check if user can manage a specific org team.
Presidents can manage all org teams. Managers can manage all org teams.
Coaches can only manage their own coached team.
Args:
org_team: The OrgTeam object to check permissions for.
Returns:
bool: True if user has permission to manage the org team.
"""
if self.role == 'president': if self.role == 'president':
return True return True
if self.role == 'manager': if self.role == 'manager':
@@ -91,15 +197,14 @@ class User(UserMixin, db.Model):
return False return False
def get_gamertags(self): def get_gamertags(self):
"""Return gamertags as a dict keyed by game.""" """Return gamertags as a dictionary keyed by game.
Returns:
dict: Dictionary with game names as keys and gamertag info as values.
"""
return {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in self.gamertags} return {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in self.gamertags}
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# Platform options for games that require platform specification # Platform options for games that require platform specification
GAME_PLATFORMS = { GAME_PLATFORMS = {
'Valorant': [], # No platform needed 'Valorant': [], # No platform needed
@@ -108,12 +213,8 @@ GAME_PLATFORMS = {
'Apex Legends': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'], 'Apex Legends': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'],
'Overwatch 2': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'], 'Overwatch 2': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'],
'Rainbow Six Siege': ['Ubisoft', 'PlayStation', 'Xbox'], # Ubisoft = Uplay/Steam 'Rainbow Six Siege': ['Ubisoft', 'PlayStation', 'Xbox'], # Ubisoft = Uplay/Steam
'Fortnite': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch', 'Mobile'], 'Rocket League': ['Epic', 'PlayStation', 'Xbox', 'Nintendo Switch'], # Epic uses EpicID, others use username
'Rocket League': ['Epic', 'PlayStation', 'Xbox', 'Nintendo Switch'], # Epic uses EpicID, others use username
'Call of Duty': ['PC', 'PlayStation', 'Xbox'],
'Dota 2': [],
'Super Smash Bros.': ['Nintendo Switch'], 'Super Smash Bros.': ['Nintendo Switch'],
'Street Fighter 6': ['PC', 'PlayStation', 'Xbox'],
} }
@@ -124,7 +225,6 @@ PLATFORM_CODES = {
'Xbox': 'xbl', 'Xbox': 'xbl',
'Nintendo Switch': 'switch', 'Nintendo Switch': 'switch',
'PC': 'pc', 'PC': 'pc',
'Mobile': 'mobile',
'Steam': 'steam', 'Steam': 'steam',
'Epic': 'epic', 'Epic': 'epic',
} }
@@ -137,17 +237,24 @@ TRN_URLS = {
'Apex Legends': 'https://tracker.gg/apex/profile/{platform}/{username}', 'Apex Legends': 'https://tracker.gg/apex/profile/{platform}/{username}',
'Overwatch 2': 'https://tracker.gg/overwatch/profile/{platform}/{username}', 'Overwatch 2': 'https://tracker.gg/overwatch/profile/{platform}/{username}',
'Rainbow Six Siege': 'https://r6.tracker.network/r6siege/profile/{platform_code}/{username}', 'Rainbow Six Siege': 'https://r6.tracker.network/r6siege/profile/{platform_code}/{username}',
'Fortnite': 'https://tracker.gg/fortnite/profile/{platform}/{username}',
'Rocket League': 'https://rocketleague.tracker.network/rocket-league/profile/{platform_code}/{username}', 'Rocket League': 'https://rocketleague.tracker.network/rocket-league/profile/{platform_code}/{username}',
'Call of Duty': 'https://tracker.gg/call-of-duty/profile/{platform}/{username}',
'Dota 2': 'https://tracker.gg/dota2/profile/steam/{username}',
'Super Smash Bros.': 'https://tracker.gg/smash/profile/{username}', 'Super Smash Bros.': 'https://tracker.gg/smash/profile/{username}',
'Street Fighter 6': 'https://tracker.gg/streetfighter/profile/{username}',
} }
class UserGamertag(db.Model): class UserGamertag(db.Model):
"""Store gamertag per game for each user.""" """Store gamertag per game for each user.
Allows users to link their gaming profiles for different games with optional
platform specification for cross-platform games.
Attributes:
id: Unique identifier.
user_id: Foreign key to the user.
game: Name of the game.
gamertag: Player's gamertag/username for the game.
platform: Platform for cross-platform games (optional).
"""
__tablename__ = 'user_gamertags' __tablename__ = 'user_gamertags'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
@@ -162,7 +269,14 @@ class UserGamertag(db.Model):
) )
def get_trn_url(self): def get_trn_url(self):
"""Generate the TRN URL for this gamertag.""" """Generate the TRN (Tracker Network) URL for this gamertag.
Builds the appropriate URL based on the game, handling platform codes
and URL encoding for special characters.
Returns:
str: The Tracker Network URL for the gamertag, or None if game not supported.
"""
if self.game not in TRN_URLS: if self.game not in TRN_URLS:
return None return None
@@ -182,7 +296,18 @@ class UserGamertag(db.Model):
class OrgTeam(db.Model): class OrgTeam(db.Model):
"""Persistent organization teams (e.g., Varsity, JV) that exist across tryouts.""" """Persistent organization teams (e.g., Varsity, JV) that exist across tryouts.
These teams are long-term organizational structures that persist beyond
individual tryouts, unlike tryout-specific Team entities.
Attributes:
id: Unique identifier.
name: Team name (e.g., "Varsity", "Junior Varsity").
coach_id: Foreign key to the assigned coach.
created_by: Foreign key to the user who created the team.
created_at: Timestamp of team creation.
"""
__tablename__ = 'org_teams' __tablename__ = 'org_teams'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True) name = db.Column(db.String(100), nullable=False, unique=True)
@@ -196,10 +321,29 @@ class OrgTeam(db.Model):
class Tryout(db.Model): class Tryout(db.Model):
"""Tryout event for player evaluations and team formation.
Represents a scheduled tryout session where players can register
and be evaluated by coaches/managers.
Attributes:
id: Unique identifier.
title: Tryout title/name.
description: Optional description of the tryout.
game: The game this tryout is for.
date: Date of the tryout.
location: Physical or virtual location.
status: Current status (upcoming, in_progress, completed).
max_players: Maximum number of players allowed.
created_by: Foreign key to the creating manager/president.
target_org_team_id: Foreign key to target organization team.
created_at: Timestamp of creation.
"""
__tablename__ = 'tryouts' __tablename__ = 'tryouts'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False) title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text, nullable=True) description = db.Column(db.Text, nullable=True)
game = db.Column(db.String(50), nullable=False)
date = db.Column(db.Date, nullable=False) date = db.Column(db.Date, nullable=False)
location = db.Column(db.String(200), nullable=True) location = db.Column(db.String(200), nullable=True)
status = db.Column(db.String(20), default='upcoming') # upcoming, in_progress, completed status = db.Column(db.String(20), default='upcoming') # upcoming, in_progress, completed
@@ -216,6 +360,19 @@ class Tryout(db.Model):
class TryoutRegistration(db.Model): class TryoutRegistration(db.Model):
"""Registration linking a player to a tryout.
Tracks which players have registered for which tryouts and their
attendance status.
Attributes:
id: Unique identifier.
tryout_id: Foreign key to the tryout.
player_id: Foreign key to the registered player.
registered_at: Timestamp of registration.
status: Registration status (registered, attended, no_show).
notes: Optional notes about the registration.
"""
__tablename__ = 'tryout_registrations' __tablename__ = 'tryout_registrations'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
@@ -226,16 +383,41 @@ class TryoutRegistration(db.Model):
class Evaluation(db.Model): class Evaluation(db.Model):
"""Player evaluation record.
Contains scored evaluations from coaches/managers for players
during tryouts.
Attributes:
id: Unique identifier.
tryout_id: Foreign key to the tryout.
player_id: Foreign key to the evaluated player.
evaluator_id: Foreign key to the evaluating coach/manager.
speed_score: Speed rating (1-10).
agility_score: Agility rating (1-10).
technique_score: Technique rating (1-10).
teamwork_score: Teamwork rating (1-10).
attitude_score: Attitude rating (1-10).
overall_score: Average of all scores.
comments: Optional evaluator comments.
position_recommendation: Recommended position.
created_at: Timestamp of creation.
updated_at: Timestamp of last update.
"""
__tablename__ = 'evaluations' __tablename__ = 'evaluations'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
player_id = db.Column(db.Integer, db.ForeignKey('users.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) evaluator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
speed_score = db.Column(db.Integer, nullable=True) mecanics_score = db.Column(db.Integer, nullable=True)
agility_score = db.Column(db.Integer, nullable=True) cohesion_score = db.Column(db.Integer, nullable=True)
technique_score = db.Column(db.Integer, nullable=True) communication_score = db.Column(db.Integer, nullable=True)
teamwork_score = db.Column(db.Integer, nullable=True) gamesense_score = db.Column(db.Integer, nullable=True)
attitude_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) overall_score = db.Column(db.Float, nullable=True)
comments = db.Column(db.Text, nullable=True) comments = db.Column(db.Text, nullable=True)
position_recommendation = db.Column(db.String(50), nullable=True) position_recommendation = db.Column(db.String(50), nullable=True)
@@ -248,7 +430,18 @@ class Evaluation(db.Model):
class Team(db.Model): class Team(db.Model):
"""Tryout-specific teams (e.g., Alpha, Bravo within a single tryout).""" """Tryout-specific team (e.g., Alpha, Bravo within a single tryout).
Teams created during a tryout for match scheduling purposes.
Different from OrgTeam which is a long-term organizational entity.
Attributes:
id: Unique identifier.
tryout_id: Foreign key to the parent tryout.
name: Team name.
created_by: Foreign key to the creator.
created_at: Timestamp of creation.
"""
__tablename__ = 'teams' __tablename__ = 'teams'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
@@ -261,6 +454,15 @@ class Team(db.Model):
class TeamMember(db.Model): class TeamMember(db.Model):
"""Link between a player and a tryout-specific team.
Attributes:
id: Unique identifier.
team_id: Foreign key to the team.
player_id: Foreign key to the player.
position: Player's position on the team.
added_at: Timestamp when player was added.
"""
__tablename__ = 'team_members' __tablename__ = 'team_members'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False) team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
@@ -272,7 +474,26 @@ class TeamMember(db.Model):
class Match(db.Model): class Match(db.Model):
"""Matches/scrimmages scheduled within tryouts.""" """Match/scrimmage scheduled within a tryout.
Can be team vs team or player scrim type.
Attributes:
id: Unique identifier.
tryout_id: Foreign key to the parent tryout.
title: Match title/name.
description: Optional description.
date: Match date.
start_time: Match start time.
end_time: Match end time.
location: Match location.
status: Match status (scheduled, completed, cancelled).
match_type: Type of match (team_vs_team, player_vs_player, player_scrim).
created_by: Foreign key to the creator.
created_at: Timestamp of creation.
team1_id: Foreign key to first team (for team_vs_team).
team2_id: Foreign key to second team (for team_vs_team).
"""
__tablename__ = 'matches' __tablename__ = 'matches'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
@@ -298,12 +519,28 @@ class Match(db.Model):
participants = db.relationship('MatchParticipant', backref='match', lazy='dynamic') participants = db.relationship('MatchParticipant', backref='match', lazy='dynamic')
def get_participating_players(self): def get_participating_players(self):
"""Return list of players participating in this match.""" """Return list of player IDs participating in this match.
Returns:
list: List of unique player IDs.
"""
return [p.player_id for p in self.participants.all()] return [p.player_id for p in self.participants.all()]
class PlayerDisponibility(db.Model): class PlayerDisponibility(db.Model):
"""Player availability in 30-minute time blocks for scheduling matches.""" """Player availability in 30-minute time blocks for scheduling matches.
Allows players to specify when they're available for matches.
Attributes:
id: Unique identifier.
player_id: Foreign key to the player.
day_of_week: Day of week (0=Monday, 6=Sunday).
start_time: Start time of availability block.
end_time: End time of availability block (always 30 min after start).
created_at: Timestamp of creation.
updated_at: Timestamp of last update.
"""
__tablename__ = 'player_disponibilities' __tablename__ = 'player_disponibilities'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
@@ -317,7 +554,18 @@ class PlayerDisponibility(db.Model):
class MatchParticipant(db.Model): class MatchParticipant(db.Model):
"""Players participating in player scrimmage matches.""" """Players participating in player scrimmage matches.
Links players to matches for player_scrim and player_vs_player match types.
Attributes:
id: Unique identifier.
match_id: Foreign key to the match.
player_id: Foreign key to the player.
team_side: Team side (1 or 2) for player_vs_player matches.
position: Player's position for this match.
added_at: Timestamp when added.
"""
__tablename__ = 'match_participants' __tablename__ = 'match_participants'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=False) match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=False)
@@ -330,7 +578,25 @@ class MatchParticipant(db.Model):
class Contract(db.Model): class Contract(db.Model):
"""Contract documents for players to sign.""" """Contract documents for players to sign.
Tracks contract uploads and signed documents for players.
Attributes:
id: Unique identifier.
player_id: Foreign key to the player.
team_id: Foreign key to the player's team.
uploaded_by_id: Foreign key to the uploader.
original_filename: Original uploaded file name.
stored_filename: Stored file name on disk.
file_path: Full path to the contract file.
signed_filename: Signed file name (if signed).
signed_file_path: Full path to signed contract (if signed).
status: Contract status (pending, signed).
notes: Optional notes.
uploaded_at: Timestamp of upload.
signed_at: Timestamp of signing (if signed).
"""
__tablename__ = 'contracts' __tablename__ = 'contracts'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
@@ -358,7 +624,17 @@ class Contract(db.Model):
uploader = db.relationship('User', foreign_keys=[uploaded_by_id]) uploader = db.relationship('User', foreign_keys=[uploaded_by_id])
def can_view(self, user): def can_view(self, user):
"""Check if a user can view this contract.""" """Check if a user can view this contract.
Players can always view their own contracts. Presidents and managers
have full access. Coaches can view contracts for players on their teams.
Args:
user: The User object requesting access.
Returns:
bool: True if user has permission to view the contract.
"""
# Player can always view their own contracts # Player can always view their own contracts
if user.id == self.player_id: if user.id == self.player_id:
return True return True
@@ -378,6 +654,15 @@ class Contract(db.Model):
return False return False
def can_upload_signed(self, user): def can_upload_signed(self, user):
"""Check if a user can upload a signed contract.""" """Check if a user can upload a signed contract.
Only the player themselves can upload their signed contract.
Args:
user: The User object requesting to upload.
Returns:
bool: True if user is the player associated with the contract.
"""
# Only the player can upload their signed contract # Only the player can upload their signed contract
return user.id == self.player_id return user.id == self.player_id
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.
+37
View File
@@ -1,3 +1,8 @@
"""Authentication routes for user login, logout, and registration.
This module handles user authentication including login, logout, and new user registration.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user from flask_login import login_user, logout_user, login_required, current_user
from extensions import db, hash_password, check_password from extensions import db, hash_password, check_password
@@ -5,8 +10,20 @@ from models import User, ESPORT_GAMES
auth_bp = Blueprint('auth', __name__, url_prefix='/auth') auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/login', methods=['GET', 'POST']) @auth_bp.route('/login', methods=['GET', 'POST'])
def login(): def login():
"""Handle user login authentication.
GET: Render the login form.
POST: Authenticate user credentials and log them in.
Redirects authenticated users to dashboard. Validates credentials and checks
account status before login.
Returns:
Response: Login form or redirect to dashboard/next page.
"""
if current_user.is_authenticated: if current_user.is_authenticated:
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
@@ -28,8 +45,20 @@ def login():
return render_template('pages/login.html') return render_template('pages/login.html')
@auth_bp.route('/register', methods=['GET', 'POST']) @auth_bp.route('/register', methods=['GET', 'POST'])
def register(): def register():
"""Handle new player registration.
GET: Render the registration form with E-Sports games list.
POST: Create a new player account with provided details.
Only players can register through this form. Validates username/email
uniqueness and password confirmation.
Returns:
Response: Registration form or redirect to login.
"""
if current_user.is_authenticated: if current_user.is_authenticated:
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
@@ -80,9 +109,17 @@ def register():
return render_template('pages/register.html', esport_games=ESPORT_GAMES) return render_template('pages/register.html', esport_games=ESPORT_GAMES)
@auth_bp.route('/logout') @auth_bp.route('/logout')
@login_required @login_required
def logout(): def logout():
"""Log out the current user.
Clears the user session and redirects to the login page.
Returns:
Response: Redirect to login page with logout message.
"""
logout_user() logout_user()
flash('You have been logged out.', 'info') flash('You have been logged out.', 'info')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
+77 -21
View File
@@ -1,4 +1,9 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify """Evaluation routes for assessing player performance during tryouts.
This module handles player evaluation creation, management, and viewing.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
from extensions import db from extensions import db
from models import User, Tryout, Evaluation, TryoutRegistration from models import User, Tryout, Evaluation, TryoutRegistration
@@ -6,9 +11,19 @@ from sqlalchemy import func
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations') evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
@evaluations_bp.route('') @evaluations_bp.route('')
@login_required @login_required
def list_evaluations(): def list_evaluations():
"""List all evaluations accessible to the current user.
President: All evaluations with player score summaries.
Evaluators (coach/manager): Their given evaluations.
Players: Their received evaluations.
Returns:
Response: Rendered evaluations list template.
"""
user = current_user user = current_user
if user.role == 'president': if user.role == 'president':
@@ -33,9 +48,22 @@ def list_evaluations():
return render_template('pages/evaluations.html', evaluations=evaluations, player_scores=player_scores) return render_template('pages/evaluations.html', evaluations=evaluations, player_scores=player_scores)
@evaluations_bp.route('/<int:tryout_id>/<int:player_id>', methods=['GET', 'POST']) @evaluations_bp.route('/<int:tryout_id>/<int:player_id>', methods=['GET', 'POST'])
@login_required @login_required
def evaluate_player(tryout_id, player_id): 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.
"""
if not current_user.can_evaluate(): if not current_user.can_evaluate():
flash('You do not have permission to evaluate players.', 'danger') flash('You do not have permission to evaluate players.', 'danger')
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
@@ -54,29 +82,41 @@ def evaluate_player(tryout_id, player_id):
).first() ).first()
if request.method == 'POST': if request.method == 'POST':
speed = request.form.get('speed_score') mecanics = request.form.get('mecanics_score')
agility = request.form.get('agility_score') cohesion = request.form.get('cohesion_score')
technique = request.form.get('technique_score') communication = request.form.get('communication_score')
teamwork = request.form.get('teamwork_score') gamesense = request.form.get('gamesense_score')
attitude = request.form.get('attitude_score') versatility = request.form.get('versatility_score')
discipline = request.form.get('discipline_score')
analysis = request.form.get('analysis_score')
sport_ethics = request.form.get('sport_ethics_score')
mental = request.form.get('mental_score')
comments = request.form.get('comments') comments = request.form.get('comments')
position = request.form.get('position_recommendation') position = request.form.get('position_recommendation')
scores = [] scores = []
if speed: scores.append(int(speed)) if mecanics: scores.append(int(mecanics))
if agility: scores.append(int(agility)) if cohesion: scores.append(int(cohesion))
if technique: scores.append(int(technique)) if communication: scores.append(int(communication))
if teamwork: scores.append(int(teamwork)) if gamesense: scores.append(int(gamesense))
if attitude: scores.append(int(attitude)) if versatility: scores.append(int(versatility))
if discipline: scores.append(int(discipline))
if analysis: scores.append(int(analysis))
if sport_ethics: scores.append(int(sport_ethics))
if mental: scores.append(int(mental))
overall = sum(scores) / len(scores) if scores else None overall = sum(scores) / len(scores) if scores else None
if existing_eval: if existing_eval:
existing_eval.speed_score = int(speed) if speed else None existing_eval.mecanics_score = int(mecanics) if mecanics else None
existing_eval.agility_score = int(agility) if agility else None existing_eval.cohesion_score = int(cohesion) if cohesion else None
existing_eval.technique_score = int(technique) if technique else None existing_eval.communication_score = int(communication) if communication else None
existing_eval.teamwork_score = int(teamwork) if teamwork else None existing_eval.gamesense_score = int(gamesense) if gamesense else None
existing_eval.attitude_score = int(attitude) if attitude else None existing_eval.versatility_score = int(versatility) if versatility else None
existing_eval.discipline_score = int(discipline) if discipline else None
existing_eval.analysis_score = int(analysis) if analysis else None
existing_eval.sport_ethics_score = int(sport_ethics) if sport_ethics else None
existing_eval.mental_score = int(mental) if mental else None
existing_eval.overall_score = overall existing_eval.overall_score = overall
existing_eval.comments = comments existing_eval.comments = comments
existing_eval.position_recommendation = position existing_eval.position_recommendation = position
@@ -86,11 +126,15 @@ def evaluate_player(tryout_id, player_id):
tryout_id=tryout_id, tryout_id=tryout_id,
player_id=player_id, player_id=player_id,
evaluator_id=current_user.id, evaluator_id=current_user.id,
speed_score=int(speed) if speed else None, mecanics_score=int(mecanics) if mecanics else None,
agility_score=int(agility) if agility else None, cohesion_score=int(cohesion) if cohesion else None,
technique_score=int(technique) if technique else None, communication_score=int(communication) if communication else None,
teamwork_score=int(teamwork) if teamwork else None, gamesense_score=int(gamesense) if gamesense else None,
attitude_score=int(attitude) if attitude else None, versatility_score=int(versatility) if versatility else None,
discipline_score=int(discipline) if discipline else None,
analysis_score=int(analysis) if analysis else None,
sport_ethics_score=int(sport_ethics) if sport_ethics else None,
mental_score=int(mental) if mental else None,
overall_score=overall, overall_score=overall,
comments=comments, comments=comments,
position_recommendation=position position_recommendation=position
@@ -112,9 +156,21 @@ def evaluate_player(tryout_id, player_id):
existing_eval=existing_eval, existing_eval=existing_eval,
evaluators=evaluators) evaluators=evaluators)
@evaluations_bp.route('/<int:tryout_id>/players') @evaluations_bp.route('/<int:tryout_id>/players')
@login_required @login_required
def players_to_evaluate(tryout_id): 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.
"""
if not current_user.can_evaluate(): if not current_user.can_evaluate():
flash('Permission denied.', 'danger') flash('Permission denied.', 'danger')
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
+30 -7
View File
@@ -1,3 +1,8 @@
"""Main dashboard routes for the Team Tryouts application.
This module provides the main dashboard view with role-specific statistics.
"""
from flask import Blueprint, render_template, redirect, url_for, flash from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_required, current_user from flask_login import login_required, current_user
from extensions import db from extensions import db
@@ -7,13 +12,34 @@ from datetime import datetime, date
main_bp = Blueprint('main', __name__) main_bp = Blueprint('main', __name__)
@main_bp.route('/') @main_bp.route('/')
def index(): 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.
"""
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
@main_bp.route('/dashboard') @main_bp.route('/dashboard')
@login_required @login_required
def dashboard(): 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.
"""
user = current_user user = current_user
stats = {} stats = {}
@@ -23,9 +49,9 @@ def dashboard():
stats['total_tryouts'] = Tryout.query.count() stats['total_tryouts'] = Tryout.query.count()
stats['total_evaluations'] = Evaluation.query.count() stats['total_evaluations'] = Evaluation.query.count()
stats['active_tryouts'] = Tryout.query.filter_by(status='in_progress').count() stats['active_tryouts'] = Tryout.query.filter_by(status='in_progress').count()
stats['upcoming_tryouts'] = Tryout.query.filter_by(status='upcoming').count() stats['completed_tryouts'] = Tryout.query.filter_by(status='completed').count()
stats['recent_users'] = User.query.order_by(User.created_at.desc()).limit(5).all() stats['recent_users'] = User.query.order_by(User.created_at.desc()).limit(10).all()
stats['recent_tryouts'] = Tryout.query.order_by(Tryout.created_at.desc()).limit(5).all() stats['recent_tryouts'] = Tryout.query.order_by(Tryout.created_at.desc()).limit(10).all()
elif user.role == 'manager': elif user.role == 'manager':
stats['total_tryouts'] = Tryout.query.filter_by(created_by=user.id).count() stats['total_tryouts'] = Tryout.query.filter_by(created_by=user.id).count()
@@ -40,14 +66,11 @@ def dashboard():
registered_player_ids = [r.player_id for r in registrations] 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()] 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['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(5).all() stats['my_recent_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).order_by(Evaluation.created_at.desc()).limit(10).all()
elif user.role == 'player': elif user.role == 'player':
stats['my_tryouts'] = TryoutRegistration.query.filter_by(player_id=user.id).count() stats['my_tryouts'] = TryoutRegistration.query.filter_by(player_id=user.id).count()
stats['my_evaluations'] = Evaluation.query.filter_by(player_id=user.id).count()
stats['average_score'] = db.session.query(func.avg(Evaluation.overall_score)).filter_by(player_id=user.id).scalar() or 0
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()
stats['my_eval_list'] = Evaluation.query.filter_by(player_id=user.id).order_by(Evaluation.created_at.desc()).limit(5).all()
# Get next match for each tryout the player is in # Get next match for each tryout the player is in
today = date.today() today = date.today()
+88 -13
View File
@@ -1,3 +1,8 @@
"""Match scheduling routes for managing scrimmages and matches within tryouts.
This module handles calendar views, match creation, and player availability.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from extensions import db from extensions import db
@@ -8,21 +13,35 @@ matches_bp = Blueprint('matches', __name__, url_prefix='/matches')
def can_schedule_match(): def can_schedule_match():
"""Check if user can schedule matches (coaches and above).""" """Check if user can schedule matches (coaches and above).
Returns:
bool: True if user is president, manager, coach, or scout.
"""
return current_user.role in ['president', 'manager', 'coach', 'scout'] return current_user.role in ['president', 'manager', 'coach', 'scout']
@matches_bp.route('/calendar') @matches_bp.route('/calendar')
@login_required @login_required
def calendar(): def calendar():
"""Calendar view showing tryouts and matches.""" """Render the calendar view showing all tryouts and matches.
Returns:
Response: Rendered calendar template.
"""
return render_template('pages/calendar.html') return render_template('pages/calendar.html')
@matches_bp.route('/api/events') @matches_bp.route('/api/events')
@login_required @login_required
def api_events(): def api_events():
"""API endpoint returning calendar events.""" """API endpoint returning calendar events for FullCalendar.
Returns tryout events and match events with participant information.
Returns:
Response: JSON array of calendar events.
"""
events = [] events = []
# Get tryouts based on user permissions # Get tryouts based on user permissions
@@ -96,7 +115,14 @@ def api_events():
@matches_bp.route('/api/events/<int:tryout_id>') @matches_bp.route('/api/events/<int:tryout_id>')
@login_required @login_required
def api_events_for_tryout(tryout_id): def api_events_for_tryout(tryout_id):
"""API endpoint returning calendar events for a specific tryout.""" """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.
"""
tryout = Tryout.query.get_or_404(tryout_id) tryout = Tryout.query.get_or_404(tryout_id)
# Check if user can view this tryout # Check if user can view this tryout
@@ -201,7 +227,18 @@ def api_events_for_tryout(tryout_id):
def get_visible_tryouts_for_user(): def get_visible_tryouts_for_user():
"""Get tryouts that the current user can see based on their role.""" """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': if current_user.role == 'president':
return Tryout.query.order_by(Tryout.date).all() return Tryout.query.order_by(Tryout.date).all()
elif current_user.role == 'manager': elif current_user.role == 'manager':
@@ -236,7 +273,17 @@ def get_visible_tryouts_for_user():
@matches_bp.route('/create/<int:tryout_id>', methods=['GET', 'POST']) @matches_bp.route('/create/<int:tryout_id>', methods=['GET', 'POST'])
@login_required @login_required
def create_match(tryout_id): def create_match(tryout_id):
"""Create a new match/scrimmage within a tryout.""" """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.
"""
tryout = Tryout.query.get_or_404(tryout_id) tryout = Tryout.query.get_or_404(tryout_id)
# Check if user can manage this tryout (president, manager, or coach) # Check if user can manage this tryout (president, manager, or coach)
@@ -332,7 +379,17 @@ def create_match(tryout_id):
@matches_bp.route('/<int:match_id>/edit', methods=['GET', 'POST']) @matches_bp.route('/<int:match_id>/edit', methods=['GET', 'POST'])
@login_required @login_required
def edit_match(match_id): def edit_match(match_id):
"""Edit an existing match.""" """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.
"""
match = Match.query.get_or_404(match_id) match = Match.query.get_or_404(match_id)
tryout = match.tryout tryout = match.tryout
@@ -421,7 +478,14 @@ def edit_match(match_id):
@matches_bp.route('/<int:match_id>/delete', methods=['POST']) @matches_bp.route('/<int:match_id>/delete', methods=['POST'])
@login_required @login_required
def delete_match(match_id): def delete_match(match_id):
"""Delete a match.""" """Delete a match.
Args:
match_id: The ID of the match to delete.
Returns:
Response: Redirect to tryout view with status message.
"""
match = Match.query.get_or_404(match_id) match = Match.query.get_or_404(match_id)
tryout = match.tryout tryout = match.tryout
@@ -438,12 +502,15 @@ def delete_match(match_id):
def get_players_available_at_time(date_str, time_str): def get_players_available_at_time(date_str, time_str):
"""Get list of player IDs available at a specific date and time. """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: Args:
date_str: Date in YYYY-MM-DD format date_str: Date in YYYY-MM-DD format.
time_str: Time in HH:MM format time_str: Time in HH:MM format.
Returns: Returns:
List of player IDs who are available at that time list: List of available player IDs.
""" """
try: try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
@@ -487,9 +554,17 @@ def get_players_available_at_time(date_str, time_str):
@matches_bp.route('/api/available_players/<date>/<time>') @matches_bp.route('/api/available_players/<date>/<time>')
@login_required @login_required
def api_available_players(date, time): def api_available_players(date, time):
"""API endpoint to get players available at a specific date/time slot.""" """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.
"""
if not current_user.can_manage_teams() and not current_user.can_schedule_matches(): if not current_user.can_manage_teams() and not current_user.can_schedule_matches():
return jsonify({'error': 'Unauthorized'}), 403 return jsonify({'error': 'Unauthorized'}), 403
player_ids = get_players_available_at_time(date, time) player_ids = get_players_available_at_time(date, time)
return jsonify({'available_player_ids': player_ids}) return jsonify({'available_player_ids': player_ids})
+70 -1
View File
@@ -1,3 +1,8 @@
"""Organization team management routes.
This module handles CRUD operations for organization teams and player assignments.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
from extensions import db from extensions import db
@@ -5,9 +10,18 @@ from models import OrgTeam, User
teams_bp = Blueprint('teams', __name__, url_prefix='/teams') teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
@teams_bp.route('') @teams_bp.route('')
@login_required @login_required
def list_teams(): def list_teams():
"""List all organization teams visible to the current user.
Coaches see only their assigned team. Players and scouts see all teams.
Managers and presidents see all teams and can manage them.
Returns:
Response: Rendered teams list template.
"""
can_manage = current_user.can_manage_teams() can_manage = current_user.can_manage_teams()
if current_user.role == 'coach': if current_user.role == 'coach':
@@ -24,9 +38,19 @@ def list_teams():
all_players = User.query.filter_by(role='player').order_by(User.full_name).all() all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
return render_template('pages/teams.html', teams=teams, coaches=coaches, all_players=all_players, can_manage=can_manage) return render_template('pages/teams.html', teams=teams, coaches=coaches, all_players=all_players, can_manage=can_manage)
@teams_bp.route('/create', methods=['POST']) @teams_bp.route('/create', methods=['POST'])
@login_required @login_required
def create_team(): def create_team():
"""Create a new organization team.
Args:
name: Team name from form.
coach_id: Optional coach assignment from form.
Returns:
Response: Redirect to teams list with status message.
"""
if not current_user.can_manage_teams(): if not current_user.can_manage_teams():
flash('You do not have permission to create teams.', 'danger') flash('You do not have permission to create teams.', 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@@ -53,9 +77,20 @@ def create_team():
flash(f'Team "{name}" created successfully!', 'success') flash(f'Team "{name}" created successfully!', 'success')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/edit', methods=['POST']) @teams_bp.route('/<int:team_id>/edit', methods=['POST'])
@login_required @login_required
def edit_team(team_id): def edit_team(team_id):
"""Edit an existing organization team.
Args:
team_id: The ID of the team to edit.
name: New team name from form.
coach_id: New coach assignment from form.
Returns:
Response: Redirect to teams list with status message.
"""
team = OrgTeam.query.get_or_404(team_id) team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team): if not current_user.can_manage_this_org_team(team):
flash('You do not have permission to edit this team.', 'danger') flash('You do not have permission to edit this team.', 'danger')
@@ -79,9 +114,21 @@ def edit_team(team_id):
flash(f'Team "{name}" updated successfully!', 'success') flash(f'Team "{name}" updated successfully!', 'success')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/delete', methods=['POST']) @teams_bp.route('/<int:team_id>/delete', methods=['POST'])
@login_required @login_required
def delete_team(team_id): def delete_team(team_id):
"""Delete an organization team.
Removes the team and clears its target_org_team_id reference from
any linked tryouts before deletion.
Args:
team_id: The ID of the team to delete.
Returns:
Response: Redirect to teams list with status message.
"""
if not current_user.can_manage_teams(): if not current_user.can_manage_teams():
flash('You do not have permission to delete teams.', 'danger') flash('You do not have permission to delete teams.', 'danger')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@@ -102,9 +149,21 @@ def delete_team(team_id):
flash(f'Team "{name}" deleted successfully.', 'success') flash(f'Team "{name}" deleted successfully.', 'success')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/add_player', methods=['POST']) @teams_bp.route('/<int:team_id>/add_player', methods=['POST'])
@login_required @login_required
def add_player(team_id): def add_player(team_id):
"""Add a player to an organization team.
If player is already on another team, they will be moved.
Args:
team_id: The ID of the team to add the player to.
player_id: The ID of the player to add.
Returns:
Response: Redirect to teams list with status message.
"""
team = OrgTeam.query.get_or_404(team_id) team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team): if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger') flash('Permission denied.', 'danger')
@@ -134,9 +193,19 @@ def add_player(team_id):
flash(f'{player.full_name} added to {team.name}!', 'success') flash(f'{player.full_name} added to {team.name}!', 'success')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/remove_player/<int:player_id>', methods=['POST']) @teams_bp.route('/<int:team_id>/remove_player/<int:player_id>', methods=['POST'])
@login_required @login_required
def remove_player(team_id, player_id): def remove_player(team_id, player_id):
"""Remove a player from an organization team.
Args:
team_id: The ID of the team.
player_id: The ID of the player to remove.
Returns:
Response: Redirect to teams list with status message.
"""
team = OrgTeam.query.get_or_404(team_id) team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team): if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger') flash('Permission denied.', 'danger')
@@ -151,4 +220,4 @@ def remove_player(team_id, player_id):
player.team_id = None player.team_id = None
db.session.commit() db.session.commit()
flash(f'{player.full_name} removed from {team.name}.', 'success') flash(f'{player.full_name} removed from {team.name}.', 'success')
return redirect(url_for('teams.list_teams')) return redirect(url_for('teams.list_teams'))
+133 -5
View File
@@ -1,17 +1,40 @@
"""Tryout management routes for creating, viewing, and managing tryout events.
This module handles CRUD operations for tryouts and player registrations.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
from extensions import db from extensions import db
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, Match, MatchParticipant from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, Match, MatchParticipant, ESPORT_GAMES, GAME_POSITIONS
from datetime import datetime from datetime import datetime
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts') tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
def can_manage(): 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'] return current_user.role in ['president', 'manager']
@tryouts_bp.route('') @tryouts_bp.route('')
@login_required @login_required
def list_tryouts(): 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: Upcoming and in-progress tryouts
Returns:
Response: Rendered tryouts list template.
"""
if current_user.role == 'president': if current_user.role == 'president':
tryouts = Tryout.query.order_by(Tryout.date.desc()).all() tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
elif current_user.role == 'manager': elif current_user.role == 'manager':
@@ -29,9 +52,20 @@ def list_tryouts():
tryouts = Tryout.query.order_by(Tryout.date.desc()).all() tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
return render_template('pages/tryouts.html', tryouts=tryouts, now=datetime.utcnow()) return render_template('pages/tryouts.html', tryouts=tryouts, now=datetime.utcnow())
@tryouts_bp.route('/create', methods=['GET', 'POST']) @tryouts_bp.route('/create', methods=['GET', 'POST'])
@login_required @login_required
def create_tryout(): 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.
"""
if not can_manage(): if not can_manage():
flash('You do not have permission to create tryouts.', 'danger') flash('You do not have permission to create tryouts.', 'danger')
return redirect(url_for('tryouts.list_tryouts')) return redirect(url_for('tryouts.list_tryouts'))
@@ -41,6 +75,7 @@ def create_tryout():
if request.method == 'POST': if request.method == 'POST':
title = request.form.get('title') title = request.form.get('title')
description = request.form.get('description') description = request.form.get('description')
game = request.form.get('game')
date_str = request.form.get('date') date_str = request.form.get('date')
location = request.form.get('location') location = request.form.get('location')
max_players = request.form.get('max_players') max_players = request.form.get('max_players')
@@ -55,6 +90,7 @@ def create_tryout():
tryout = Tryout( tryout = Tryout(
title=title, title=title,
description=description, description=description,
game=game,
date=date_obj, date=date_obj,
location=location, location=location,
max_players=int(max_players) if max_players else None, max_players=int(max_players) if max_players else None,
@@ -67,11 +103,25 @@ def create_tryout():
flash('Tryout created successfully!', 'success') flash('Tryout created successfully!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
return render_template('pages/create_tryout.html', org_teams=org_teams) return render_template('pages/create_tryout.html', org_teams=org_teams, esport_games=ESPORT_GAMES)
@tryouts_bp.route('/<int:tryout_id>/edit', methods=['GET', 'POST']) @tryouts_bp.route('/<int:tryout_id>/edit', methods=['GET', 'POST'])
@login_required @login_required
def edit_tryout(tryout_id): 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.
"""
tryout = Tryout.query.get_or_404(tryout_id) tryout = Tryout.query.get_or_404(tryout_id)
# Permission: president, manager (own tryouts), or coach (targets their team) # Permission: president, manager (own tryouts), or coach (targets their team)
@@ -84,6 +134,7 @@ def edit_tryout(tryout_id):
if request.method == 'POST': if request.method == 'POST':
title = request.form.get('title') title = request.form.get('title')
description = request.form.get('description') description = request.form.get('description')
game = request.form.get('game')
date_str = request.form.get('date') date_str = request.form.get('date')
location = request.form.get('location') location = request.form.get('location')
max_players = request.form.get('max_players') max_players = request.form.get('max_players')
@@ -93,10 +144,11 @@ def edit_tryout(tryout_id):
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
except (ValueError, TypeError): except (ValueError, TypeError):
flash('Invalid date format.', 'danger') flash('Invalid date format.', 'danger')
return render_template('pages/edit_tryout.html', tryout=tryout, org_teams=org_teams) return render_template('pages/edit_tryout.html', tryout=tryout, org_teams=org_teams, esport_games=ESPORT_GAMES)
tryout.title = title tryout.title = title
tryout.description = description tryout.description = description
tryout.game = game
tryout.date = date_obj tryout.date = date_obj
tryout.location = location tryout.location = location
tryout.max_players = int(max_players) if max_players else None tryout.max_players = int(max_players) if max_players else None
@@ -105,11 +157,23 @@ def edit_tryout(tryout_id):
flash('Tryout updated successfully!', 'success') flash('Tryout updated successfully!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
return render_template('pages/edit_tryout.html', tryout=tryout, org_teams=org_teams) return render_template('pages/edit_tryout.html', tryout=tryout, org_teams=org_teams, esport_games=ESPORT_GAMES)
@tryouts_bp.route('/<int:tryout_id>') @tryouts_bp.route('/<int:tryout_id>')
@login_required @login_required
def view_tryout(tryout_id): 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.
"""
tryout = Tryout.query.get_or_404(tryout_id) tryout = Tryout.query.get_or_404(tryout_id)
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all() registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
registered_players = [User.query.get(r.player_id) for r in registrations if r.player_id] registered_players = [User.query.get(r.player_id) for r in registrations if r.player_id]
@@ -156,7 +220,7 @@ def view_tryout(tryout_id):
).first() is not None ).first() is not None
can_view_calendar = is_registered or player_in_match can_view_calendar = is_registered or player_in_match
# Get all players (for manager registration dropdown) # Get all players (for manager registration dropdown)
all_players = User.query.filter_by(role='player').order_by(User.full_name).all() all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
@@ -200,11 +264,24 @@ def view_tryout(tryout_id):
all_players=all_players, all_players=all_players,
matches=matches, matches=matches,
match_data=match_data, match_data=match_data,
game_positions=GAME_POSITIONS,
now=datetime.utcnow()) now=datetime.utcnow())
@tryouts_bp.route('/<int:tryout_id>/register', methods=['POST']) @tryouts_bp.route('/<int:tryout_id>/register', methods=['POST'])
@login_required @login_required
def register_for_tryout(tryout_id): 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.
"""
tryout = Tryout.query.get_or_404(tryout_id) tryout = Tryout.query.get_or_404(tryout_id)
if current_user.role != 'player': if current_user.role != 'player':
flash('Only players can register for tryouts.', 'danger') flash('Only players can register for tryouts.', 'danger')
@@ -231,9 +308,20 @@ def register_for_tryout(tryout_id):
flash('Successfully registered for tryout!', 'success') flash('Successfully registered for tryout!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/status', methods=['POST']) @tryouts_bp.route('/<int:tryout_id>/status', methods=['POST'])
@login_required @login_required
def update_status(tryout_id): 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.
"""
tryout = Tryout.query.get_or_404(tryout_id) tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout): if not current_user.can_manage_this_tryout(tryout):
flash('Permission denied.', 'danger') flash('Permission denied.', 'danger')
@@ -245,9 +333,19 @@ def update_status(tryout_id):
flash(f'Tryout status updated to {new_status}.', 'success') flash(f'Tryout status updated to {new_status}.', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/registration/<int:player_id>/status', methods=['POST']) @tryouts_bp.route('/<int:tryout_id>/registration/<int:player_id>/status', methods=['POST'])
@login_required @login_required
def update_registration_status(tryout_id, player_id): 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.
"""
tryout = Tryout.query.get_or_404(tryout_id) tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout): if not current_user.can_manage_this_tryout(tryout):
flash('Permission denied.', 'danger') flash('Permission denied.', 'danger')
@@ -261,9 +359,20 @@ def update_registration_status(tryout_id, player_id):
flash('Registration status updated.', 'success') flash('Registration status updated.', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/register_player', methods=['POST']) @tryouts_bp.route('/<int:tryout_id>/register_player', methods=['POST'])
@login_required @login_required
def register_player(tryout_id): 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.
"""
tryout = Tryout.query.get_or_404(tryout_id) tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout): if not current_user.can_manage_this_tryout(tryout):
flash('Permission denied.', 'danger') flash('Permission denied.', 'danger')
@@ -296,9 +405,18 @@ def register_player(tryout_id):
flash(f'{player.full_name} registered for tryout!', 'success') flash(f'{player.full_name} registered for tryout!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/team/create', methods=['POST']) @tryouts_bp.route('/<int:tryout_id>/team/create', methods=['POST'])
@login_required @login_required
def create_team(tryout_id): 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.
"""
tryout = Tryout.query.get_or_404(tryout_id) tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout): if not current_user.can_manage_this_tryout(tryout):
flash('Permission denied.', 'danger') flash('Permission denied.', 'danger')
@@ -312,9 +430,19 @@ def create_team(tryout_id):
flash(f'Team "{team_name}" created!', 'success') flash(f'Team "{team_name}" created!', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/team/<int:team_id>/add', methods=['POST']) @tryouts_bp.route('/<int:tryout_id>/team/<int:team_id>/add', methods=['POST'])
@login_required @login_required
def add_to_team(tryout_id, team_id): 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.
"""
team = Team.query.get_or_404(team_id) team = Team.query.get_or_404(team_id)
tryout = Tryout.query.get_or_404(tryout_id) tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout): if not current_user.can_manage_this_tryout(tryout):
+202 -62
View File
@@ -1,3 +1,9 @@
"""User management routes for profiles, disponibilities, and contracts.
This module handles user CRUD operations, profile editing, player availability,
and contract management.
"""
import os import os
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
from flask_login import login_required, current_user from flask_login import login_required, current_user
@@ -6,11 +12,54 @@ from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag,
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from datetime import datetime, timedelta from datetime import datetime, timedelta
users_bp = Blueprint('users', __name__, url_prefix='/users') users_bp = Blueprint('users', __name__, url_prefix='/users')
def update_user_gamertags(user, selected_games):
"""Update gamertags for a user based on form input.
Handles creating, updating, and deleting gamertag records for the specified games.
Used by both edit_user and edit_profile routes to avoid code duplication.
Args:
user: The User object to update gamertags for.
selected_games: List of game names that were selected in the form.
"""
# Get all existing gamertags for this user
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)
# Remove gamertags for games that are no longer selected
for game in existing_gamertags:
if game not in selected_games:
db.session.delete(existing_gamertags[game])
@users_bp.route('') @users_bp.route('')
@login_required @login_required
def list_users(): def list_users():
"""List all users for management (president only).
Displays all users ordered by role and name. Only accessible to presidents.
Returns:
Response: Rendered users list template or redirect to dashboard.
"""
if current_user.role != 'president': if current_user.role != 'president':
flash('Only the president can manage users.', 'danger') flash('Only the president can manage users.', 'danger')
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
@@ -18,9 +67,21 @@ def list_users():
users = User.query.order_by(User.role, User.full_name).all() users = User.query.order_by(User.role, User.full_name).all()
return render_template('pages/users.html', users=users, roles=ROLES) return render_template('pages/users.html', users=users, roles=ROLES)
@users_bp.route('/<int:user_id>/edit', methods=['GET', 'POST']) @users_bp.route('/<int:user_id>/edit', methods=['GET', 'POST'])
@login_required @login_required
def edit_user(user_id): def edit_user(user_id):
"""Edit an existing user (president only).
GET: Render the user edit form.
POST: Update user details including gamertags and password.
Args:
user_id: The ID of the user to edit.
Returns:
Response: Edit form or redirect to users list.
"""
if current_user.role != 'president': if current_user.role != 'president':
flash('Only the president can edit users.', 'danger') flash('Only the president can edit users.', 'danger')
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
@@ -51,29 +112,8 @@ def edit_user(user_id):
user.discord_username = discord_username or None user.discord_username = discord_username or None
user.league_os_profile = league_os_profile or None user.league_os_profile = league_os_profile or None
# Handle gamertags - save or delete based on form input # Update gamertags using shared function
# First, get all existing gamertags for this user update_user_gamertags(user, selected_games)
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)
# Remove gamertags for games that are no longer selected
for game in existing_gamertags:
if game not in selected_games:
db.session.delete(existing_gamertags[game])
password = request.form.get('password') password = request.form.get('password')
if password: if password:
@@ -86,9 +126,18 @@ def edit_user(user_id):
user_gamertags = {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in user.gamertags} 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=ROLES, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, user_gamertags=user_gamertags) return render_template('pages/edit_user.html', user=user, roles=ROLES, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, user_gamertags=user_gamertags)
@users_bp.route('/<int:user_id>/delete', methods=['POST']) @users_bp.route('/<int:user_id>/delete', methods=['POST'])
@login_required @login_required
def delete_user(user_id): def delete_user(user_id):
"""Delete a user (president only).
Args:
user_id: The ID of the user to delete.
Returns:
Response: Redirect to users list with status message.
"""
if current_user.role != 'president': if current_user.role != 'president':
flash('Only the president can delete users.', 'danger') flash('Only the president can delete users.', 'danger')
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
@@ -103,9 +152,18 @@ def delete_user(user_id):
flash(f'User {user.full_name} has been removed.', 'success') flash(f'User {user.full_name} has been removed.', 'success')
return redirect(url_for('users.list_users')) return redirect(url_for('users.list_users'))
@users_bp.route('/create', methods=['GET', 'POST']) @users_bp.route('/create', methods=['GET', 'POST'])
@login_required @login_required
def create_user(): def create_user():
"""Create a new user (president only).
GET: Render the user creation form.
POST: Create a new user with the provided details.
Returns:
Response: Create form or redirect to users list.
"""
if current_user.role != 'president': if current_user.role != 'president':
flash('Only the president can create users.', 'danger') flash('Only the president can create users.', 'danger')
return redirect(url_for('main.dashboard')) return redirect(url_for('main.dashboard'))
@@ -146,18 +204,35 @@ def create_user():
return render_template('pages/create_user.html', roles=ROLES) return render_template('pages/create_user.html', roles=ROLES)
@users_bp.route('/profile') @users_bp.route('/profile')
@login_required @login_required
def profile(): def profile():
"""View the current user's profile.
Players see their contracts along with their profile information.
Returns:
Response: Rendered profile template.
"""
# Get contracts ordered by uploaded_at desc for the current user # Get contracts ordered by uploaded_at desc for the current user
contracts = None contracts = None
if current_user.role == 'player': if current_user.role == 'player':
contracts = Contract.query.filter_by(player_id=current_user.id).order_by(Contract.uploaded_at.desc()).all() 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) return render_template('pages/profile.html', user=current_user, contracts=contracts)
@users_bp.route('/profile/edit', methods=['GET', 'POST']) @users_bp.route('/profile/edit', methods=['GET', 'POST'])
@login_required @login_required
def edit_profile(): def edit_profile():
"""Edit the current user's profile.
GET: Render the profile edit form.
POST: Update profile details including gamertags and password.
Returns:
Response: Edit form or redirect to profile.
"""
if request.method == 'POST': if request.method == 'POST':
full_name = request.form.get('full_name') full_name = request.form.get('full_name')
email = request.form.get('email') email = request.form.get('email')
@@ -178,29 +253,8 @@ def edit_profile():
current_user.discord_username = discord_username or None current_user.discord_username = discord_username or None
current_user.league_os_profile = league_os_profile or None current_user.league_os_profile = league_os_profile or None
# Handle gamertags - save or delete based on form input # Update gamertags using shared function
# First, get all existing gamertags for this user update_user_gamertags(current_user, selected_games)
existing_gamertags = {gt.game: gt for gt in current_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=current_user.id, game=game, gamertag=gamertag, platform=platform)
db.session.add(gt)
elif existing:
db.session.delete(existing)
# Remove gamertags for games that are no longer selected
for game in existing_gamertags:
if game not in selected_games:
db.session.delete(existing_gamertags[game])
password = request.form.get('password') password = request.form.get('password')
if password: if password:
@@ -214,19 +268,32 @@ def edit_profile():
return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, user_gamertags=user_gamertags) return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, user_gamertags=user_gamertags)
# Day names for disponibility # Day names for disponibility display
DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
def add_30_minutes(t): def add_30_minutes(t):
"""Add 30 minutes to a time object.""" """Add 30 minutes to a time object.
Args:
t (datetime.time): The time object to add 30 minutes to.
Returns:
datetime.time: New time 30 minutes later.
"""
return (datetime.combine(datetime.today(), t) + timedelta(minutes=30)).time() return (datetime.combine(datetime.today(), t) + timedelta(minutes=30)).time()
@users_bp.route('/disponibilities') @users_bp.route('/disponibilities')
@login_required @login_required
def get_disponibilities(): def get_disponibilities():
"""API endpoint to get all player disponibilities for scheduling.""" """API endpoint to get all player disponibilities for scheduling.
Only accessible to managers, coaches, and scouts. Used for match scheduling.
Returns:
Response: JSON with disponibility data for all players.
"""
# Only managers and above can view disponibilities for scheduling # Only managers and above can view disponibilities for scheduling
if not current_user.can_manage_teams() and not current_user.can_schedule_matches(): if not current_user.can_manage_teams() and not current_user.can_schedule_matches():
return jsonify({'error': 'Unauthorized'}), 403 return jsonify({'error': 'Unauthorized'}), 403
@@ -256,7 +323,11 @@ def get_disponibilities():
@users_bp.route('/disponibilities/my') @users_bp.route('/disponibilities/my')
@login_required @login_required
def get_my_disponibilities(): def get_my_disponibilities():
"""API endpoint for players to get their own disponibilities.""" """API endpoint for players to get their own disponibilities.
Returns:
Response: JSON with disponibility data grouped by day.
"""
disponibilities = PlayerDisponibility.query.filter_by(player_id=current_user.id).all() disponibilities = PlayerDisponibility.query.filter_by(player_id=current_user.id).all()
# Group by day for easier display # Group by day for easier display
@@ -279,7 +350,11 @@ def get_my_disponibilities():
@users_bp.route('/disponibilities/add', methods=['POST']) @users_bp.route('/disponibilities/add', methods=['POST'])
@login_required @login_required
def add_disponibility(): def add_disponibility():
"""Add a disponibility block for the current player.""" """Add a disponibility block for the current player.
Returns:
Response: JSON with the created disponibility data.
"""
day_of_week = request.form.get('day_of_week', type=int) day_of_week = request.form.get('day_of_week', type=int)
start_time_str = request.form.get('start_time') start_time_str = request.form.get('start_time')
@@ -315,7 +390,14 @@ def add_disponibility():
@users_bp.route('/disponibilities/add_bulk', methods=['POST']) @users_bp.route('/disponibilities/add_bulk', methods=['POST'])
@login_required @login_required
def add_disponibilities_bulk(): def add_disponibilities_bulk():
"""Add multiple disponibility blocks at once (for grid selection).""" """Add multiple disponibility blocks at once (for grid selection).
Used for the disponibility grid UI where players can select multiple
time slots at once.
Returns:
Response: JSON with success status and created slots.
"""
data = request.get_json() data = request.get_json()
slots = data.get('slots', []) # List of {day_of_week, start_time} slots = data.get('slots', []) # List of {day_of_week, start_time}
@@ -365,7 +447,11 @@ def add_disponibilities_bulk():
@users_bp.route('/disponibilities/clear', methods=['POST']) @users_bp.route('/disponibilities/clear', methods=['POST'])
@login_required @login_required
def clear_disponibilities(): def clear_disponibilities():
"""Clear all disponibilities for the current player (for resetting).""" """Clear all disponibilities for the current player (for resetting).
Returns:
Response: JSON with success status.
"""
PlayerDisponibility.query.filter_by(player_id=current_user.id).delete() PlayerDisponibility.query.filter_by(player_id=current_user.id).delete()
db.session.commit() db.session.commit()
return jsonify({'success': True}) return jsonify({'success': True})
@@ -374,7 +460,14 @@ def clear_disponibilities():
@users_bp.route('/disponibilities/<int:disponibility_id>/delete', methods=['POST']) @users_bp.route('/disponibilities/<int:disponibility_id>/delete', methods=['POST'])
@login_required @login_required
def delete_disponibility(disponibility_id): def delete_disponibility(disponibility_id):
"""Delete a disponibility block.""" """Delete a disponibility block.
Args:
disponibility_id: The ID of the disponibility to delete.
Returns:
Response: JSON with success status or error.
"""
disponibility = PlayerDisponibility.query.get_or_404(disponibility_id) disponibility = PlayerDisponibility.query.get_or_404(disponibility_id)
# Only the owner can delete their disponibility # Only the owner can delete their disponibility
@@ -387,10 +480,22 @@ def delete_disponibility(disponibility_id):
return jsonify({'success': True}) return jsonify({'success': True})
# Contract Dropbox Functions # Contract Management Functions
def can_manage_player_contract(user, player_id): def can_manage_player_contract(user, player_id):
"""Check if a user can upload contracts for a specific player.""" """Check if a user can upload contracts for a specific player.
Presidents and managers can manage all contracts. Coaches can only
manage contracts for players on their team.
Args:
user: The User requesting to manage contracts.
player_id: The ID of the player whose contract is being managed.
Returns:
bool: True if user has permission to manage the contract.
"""
# President can manage all contracts # President can manage all contracts
if user.role == 'president': if user.role == 'president':
return True return True
@@ -412,7 +517,14 @@ def can_manage_player_contract(user, player_id):
@users_bp.route('/contracts') @users_bp.route('/contracts')
@login_required @login_required
def list_contracts(): def list_contracts():
"""View contracts for the current user (player) or players they manage.""" """View contracts for the current user (player) or players they manage.
Players see their own contracts. Coaches/managers see contracts for
players on their teams. Presidents see all contracts.
Returns:
Response: Rendered contracts list template.
"""
contracts = None contracts = None
if current_user.role == 'player': if current_user.role == 'player':
@@ -438,7 +550,14 @@ def list_contracts():
@users_bp.route('/contracts/upload', methods=['GET', 'POST']) @users_bp.route('/contracts/upload', methods=['GET', 'POST'])
@login_required @login_required
def upload_contract(): def upload_contract():
"""Upload a contract for a player.""" """Upload a contract for a player.
GET: Render the contract upload form.
POST: Save the uploaded contract file and create database record.
Returns:
Response: Upload form or redirect to contracts list.
"""
if current_user.role not in ['president', 'manager', 'coach']: if current_user.role not in ['president', 'manager', 'coach']:
flash('Only presidents, managers, and coaches can upload contracts.', 'danger') flash('Only presidents, managers, and coaches can upload contracts.', 'danger')
return redirect(url_for('users.list_contracts')) return redirect(url_for('users.list_contracts'))
@@ -515,7 +634,14 @@ def upload_contract():
@users_bp.route('/contracts/<int:contract_id>/upload_signed', methods=['POST']) @users_bp.route('/contracts/<int:contract_id>/upload_signed', methods=['POST'])
@login_required @login_required
def upload_signed_contract(contract_id): def upload_signed_contract(contract_id):
"""Upload a signed contract.""" """Upload a signed contract (player only).
Args:
contract_id: The ID of the contract to upload signed version for.
Returns:
Response: Redirect to contracts list with status message.
"""
contract = Contract.query.get_or_404(contract_id) contract = Contract.query.get_or_404(contract_id)
if not contract.can_upload_signed(current_user): if not contract.can_upload_signed(current_user):
@@ -552,7 +678,14 @@ def upload_signed_contract(contract_id):
@users_bp.route('/contracts/<int:contract_id>/download') @users_bp.route('/contracts/<int:contract_id>/download')
@login_required @login_required
def download_contract(contract_id): def download_contract(contract_id):
"""Download a contract file.""" """Download a contract file.
Args:
contract_id: The ID of the contract to download.
Returns:
Response: File download response.
"""
contract = Contract.query.get_or_404(contract_id) contract = Contract.query.get_or_404(contract_id)
if not contract.can_view(current_user): if not contract.can_view(current_user):
@@ -565,7 +698,14 @@ def download_contract(contract_id):
@users_bp.route('/contracts/<int:contract_id>/download_signed') @users_bp.route('/contracts/<int:contract_id>/download_signed')
@login_required @login_required
def download_signed_contract(contract_id): def download_signed_contract(contract_id):
"""Download a signed contract file.""" """Download a signed contract file.
Args:
contract_id: The ID of the contract to download the signed version for.
Returns:
Response: File download response.
"""
contract = Contract.query.get_or_404(contract_id) contract = Contract.query.get_or_404(contract_id)
if not contract.can_view(current_user): if not contract.can_view(current_user):
@@ -576,4 +716,4 @@ def download_signed_contract(contract_id):
flash('No signed contract available.', 'danger') flash('No signed contract available.', 'danger')
return redirect(url_for('users.list_contracts')) return redirect(url_for('users.list_contracts'))
return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename) return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename)
+88 -51
View File
@@ -1,3 +1,9 @@
"""Database seeding script for Team Tryouts application.
This module provides functions to seed the database with sample data including
users, tryouts, teams, evaluations, and player disponibilities.
"""
from sqlalchemy import text from sqlalchemy import text
from extensions import db, hash_password from extensions import db, hash_password
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, PlayerDisponibility, UserGamertag from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, PlayerDisponibility, UserGamertag
@@ -6,6 +12,20 @@ import random
def seed_database(): def seed_database():
"""Seed the database with sample data for development and testing.
Creates sample users with different roles (president, manager, coach, player, scout),
organization teams, tryouts, registrations, evaluations, and player disponibilities.
All existing data is cleared before seeding.
The function creates:
- 7 admin users (president, 2 managers, 3 coaches, 1 scout)
- 10 player users with E-Sports profiles
- 4 organization teams
- 4 tryouts
- Registrations and evaluations
- Player disponibilities for match scheduling
"""
# Clear existing data # Clear existing data
db.session.execute(text('DELETE FROM player_disponibilities')) db.session.execute(text('DELETE FROM player_disponibilities'))
db.session.execute(text('DELETE FROM match_participants')) db.session.execute(text('DELETE FROM match_participants'))
@@ -33,9 +53,9 @@ def seed_database():
# Create players with E-Sports profile data (gamertags per game) # Create players with E-Sports profile data (gamertags per game)
player_data = [ player_data = [
{'username': 'jplayer1', 'full_name': 'James Wilson', 'email': 'james@email.com', 'games': 'Valorant,Counter-Strike 2', {'username': 'jplayer1', 'full_name': 'nordjan', 'email': 'nordjan27@gmail.com', 'games': 'Valorant,Counter-Strike 2, Rainbow Six Siege, Rocket League, Overwatch 2',
'gamertags': {'Valorant': 'jameswilson_val', 'Counter-Strike 2': 'jameswilson_cs'}, 'gamertags': {'Valorant': 'nordjan#bad', 'Counter-Strike 2': 'nordjan', 'Rainbow Six Siege': 'n0rd-vpn', 'Rocket League': 'nordjiano'},
'discord': 'JamesW#7291', 'league_os': 'https://leagueos.gg/player/jameswilson'}, 'discord': 'nordjan', 'league_os': 'https://leagueos.gg/player/nordjan'},
{'username': 'jplayer2', 'full_name': 'Emma Garcia', 'email': '[email protected]', 'games': 'League of Legends,Valorant', {'username': 'jplayer2', 'full_name': 'Emma Garcia', 'email': '[email protected]', 'games': 'League of Legends,Valorant',
'gamertags': {'League of Legends': 'emmagarcia_lol', 'Valorant': 'emmagarcia_val'}, 'gamertags': {'League of Legends': 'emmagarcia_lol', 'Valorant': 'emmagarcia_val'},
'discord': 'EmmaG#4452', 'league_os': 'https://leagueos.gg/player/emmagarcia'}, 'discord': 'EmmaG#4452', 'league_os': 'https://leagueos.gg/player/emmagarcia'},
@@ -139,10 +159,10 @@ def seed_database():
# Create Organization Teams (OrgTeams) # Create Organization Teams (OrgTeams)
org_teams_data = [ org_teams_data = [
{'name': 'Varsity', 'coach': coaches[0], 'creator': president}, {'name': 'Rocket League main', 'coach': coaches[0], 'creator': president},
{'name': 'Junior Varsity', 'coach': coaches[1], 'creator': president}, {'name': 'CS2', 'coach': coaches[1], 'creator': president},
{'name': 'U14 Development', 'coach': coaches[2], 'creator': president}, {'name': 'Valorant', 'coach': coaches[2], 'creator': president},
{'name': 'Select Team', 'coach': None, 'creator': president}, {'name': 'Rocket League acad', 'coach': None, 'creator': manager1},
] ]
org_teams = [] org_teams = []
@@ -159,16 +179,16 @@ def seed_database():
# Create tryouts # Create tryouts
tryouts_data = [ tryouts_data = [
{'title': 'Spring Season Tryouts', 'date': datetime.utcnow() - timedelta(days=5), 'location': 'Main Stadium', 'description': 'Tryouts for the spring competitive season. All positions welcome.', 'status': 'in_progress', 'creator': manager1, 'target_team': org_teams[0]}, {'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': 'Fall Select Team Trials', 'date': datetime.utcnow() + timedelta(days=20), 'location': 'Training Center', 'description': 'Trials for the fall select team. High skill level required.', 'status': 'upcoming', 'creator': manager1, 'target_team': org_teams[3]}, {'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': 'Youth Development Camp', 'date': datetime.utcnow() - timedelta(days=20), 'location': 'Community Field', 'description': 'Development camp for younger players to showcase their skills.', 'status': 'completed', 'creator': manager2, 'target_team': org_teams[2]}, {'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]},
{'title': 'Winter Indoor Showcase', 'date': datetime.utcnow() - timedelta(days=2), 'location': 'Indoor Arena', 'description': 'Indoor showcase event for scouting and team selection.', 'status': 'in_progress', 'creator': manager2, 'target_team': org_teams[1]},
] ]
tryouts = [] tryouts = []
for data in tryouts_data: for data in tryouts_data:
t = Tryout( t = Tryout(
title=data['title'], title=data['title'],
game=data['game'],
date=data['date'].date(), date=data['date'].date(),
location=data['location'], location=data['location'],
description=data['description'], description=data['description'],
@@ -184,10 +204,9 @@ def seed_database():
# Register players for tryouts # Register players for tryouts
registrations_data = [ registrations_data = [
(tryouts[0], players[:8]), (tryouts[0], players[:3]),
(tryouts[1], players), (tryouts[1], players[3:5]),
(tryouts[2], players[:6]), (tryouts[2], players)
(tryouts[3], players[2:9]),
] ]
regs = [] regs = []
@@ -196,7 +215,7 @@ def seed_database():
reg = TryoutRegistration( reg = TryoutRegistration(
tryout_id=tryout.id, tryout_id=tryout.id,
player_id=player.id, player_id=player.id,
status=random.choice(['registered', 'attended', 'attended']) status=random.choice(['registered', 'attended', 'attended', 'no_show'])
) )
db.session.add(reg) db.session.add(reg)
regs.append(reg) regs.append(reg)
@@ -205,55 +224,71 @@ def seed_database():
# Create evaluations (for in_progress and completed tryouts) # Create evaluations (for in_progress and completed tryouts)
eval_data = [] eval_data = []
# Spring tryout - some evaluations # Spring tryout (Rocket League) - some evaluations
rl_positions = ['None needed', 'None', 'N/A']
for player in players[:8]: for player in players[:8]:
for coach in coaches: for coach in coaches:
if random.random() > 0.3: if random.random() > 0.3:
speed = random.randint(4, 10) mecanics = random.randint(4, 10)
agility = random.randint(4, 10) cohesion = random.randint(4, 10)
technique = random.randint(3, 10) communication = random.randint(3, 10)
teamwork = random.randint(5, 10) gamesense = random.randint(5, 10)
attitude = random.randint(5, 10) versatility = random.randint(5, 10)
overall = round((speed + agility + technique + teamwork + attitude) / 5, 1) discipline = random.randint(4, 10)
positions = ['Forward', 'Midfield', 'Defense', 'Goalie', 'Wing', 'Center'] analysis = random.randint(4, 10)
sport_ethics = random.randint(5, 10)
mental = random.randint(5, 10)
overall = round((mecanics + cohesion + communication + gamesense + versatility + discipline + analysis + sport_ethics + mental) / 9, 1)
eval_entry = Evaluation( eval_entry = Evaluation(
tryout_id=tryouts[0].id, tryout_id=tryouts[0].id,
player_id=player.id, player_id=player.id,
evaluator_id=coach.id, evaluator_id=coach.id,
speed_score=speed, mecanics_score=mecanics,
agility_score=agility, cohesion_score=cohesion,
technique_score=technique, communication_score=communication,
teamwork_score=teamwork, gamesense_score=gamesense,
attitude_score=attitude, versatility_score=versatility,
discipline_score=discipline,
analysis_score=analysis,
sport_ethics_score=sport_ethics,
mental_score=mental,
overall_score=overall, overall_score=overall,
comments=f"{'Great' if overall > 7 else 'Good'} performance. {'Shows promise.' if overall > 6 else 'Needs improvement in some areas.'}", comments=f"{'Great' if overall > 7 else 'Good'} performance. {'Shows promise.' if overall > 6 else 'Needs improvement in some areas.'}",
position_recommendation=random.choice(positions) position_recommendation=random.choice(rl_positions)
) )
db.session.add(eval_entry) db.session.add(eval_entry)
eval_data.append(eval_entry) eval_data.append(eval_entry)
# Completed tryout - full evaluations # Completed tryout (Valorant) - full evaluations
val_positions = ['Controller', 'Initiator', 'Duelist', 'Sentinel']
for player in players[:6]: for player in players[:6]:
for coach in coaches[:2]: for coach in coaches[:2]:
speed = random.randint(3, 10) mecanics = random.randint(3, 10)
agility = random.randint(3, 10) cohesion = random.randint(3, 10)
technique = random.randint(3, 10) communication = random.randint(3, 10)
teamwork = random.randint(4, 10) gamesense = random.randint(4, 10)
attitude = random.randint(4, 10) versatility = random.randint(4, 10)
overall = round((speed + agility + technique + teamwork + attitude) / 5, 1) discipline = random.randint(3, 10)
positions = ['Forward', 'Midfield', 'Defense', 'Goalie', 'Wing', 'Center'] analysis = random.randint(3, 10)
sport_ethics = random.randint(4, 10)
mental = random.randint(4, 10)
overall = round((mecanics + cohesion + communication + gamesense + versatility + discipline + analysis + sport_ethics + mental) / 9, 1)
eval_entry = Evaluation( eval_entry = Evaluation(
tryout_id=tryouts[2].id, tryout_id=tryouts[2].id,
player_id=player.id, player_id=player.id,
evaluator_id=coach.id, evaluator_id=coach.id,
speed_score=speed, mecanics_score=mecanics,
agility_score=agility, cohesion_score=cohesion,
technique_score=technique, communication_score=communication,
teamwork_score=teamwork, gamesense_score=gamesense,
attitude_score=attitude, versatility_score=versatility,
discipline_score=discipline,
analysis_score=analysis,
sport_ethics_score=sport_ethics,
mental_score=mental,
overall_score=overall, overall_score=overall,
comments=f"{'Excellent' if overall > 8 else 'Solid'} display of skills during the camp.", comments=f"{'Excellent' if overall > 8 else 'Solid'} display of skills during the camp.",
position_recommendation=random.choice(positions) position_recommendation=random.choice(val_positions)
) )
db.session.add(eval_entry) db.session.add(eval_entry)
eval_data.append(eval_entry) eval_data.append(eval_entry)
@@ -268,14 +303,15 @@ def seed_database():
db.session.add(team2) db.session.add(team2)
db.session.commit() db.session.commit()
# Assign players to teams # Assign players to teams (Valorant tryout uses Valorant positions)
val_positions = ['Controller', 'Initiator', 'Duelist', 'Sentinel']
team_members_data = [ team_members_data = [
(team1.id, players[0].id, 'Forward'), (team1.id, players[0].id, random.choice(val_positions)),
(team1.id, players[1].id, 'Midfield'), (team1.id, players[1].id, random.choice(val_positions)),
(team1.id, players[2].id, 'Defense'), (team1.id, players[2].id, random.choice(val_positions)),
(team1.id, players[3].id, 'Goalie'), (team1.id, players[3].id, random.choice(val_positions)),
(team2.id, players[4].id, 'Midfield'), (team2.id, players[4].id, random.choice(val_positions)),
(team2.id, players[5].id, 'Forward'), (team2.id, players[5].id, random.choice(val_positions)),
] ]
for team_id, player_id, position in team_members_data: for team_id, player_id, position in team_members_data:
tm = TeamMember(team_id=team_id, player_id=player_id, position=position) tm = TeamMember(team_id=team_id, player_id=player_id, position=position)
@@ -340,6 +376,7 @@ def seed_database():
print("Player: username='jplayer1', password='password'") print("Player: username='jplayer1', password='password'")
print("Scout: username='scout1', password='password'") print("Scout: username='scout1', password='password'")
if __name__ == '__main__': if __name__ == '__main__':
from app import create_app from app import create_app
app = create_app() app = create_app()
+91 -8
View File
@@ -1,4 +1,19 @@
// Dark Mode Toggle /**
* Team Tryouts - Main JavaScript Module
*
* This module provides core UI functionality including:
* - Dark mode toggle and persistence
* - Sidebar mobile toggle
* - Draggable dashboard blocks
* - Auto-dismissing alerts
*/
/**
* Toggle dark mode theme.
*
* Switches between light and dark themes, updates the toggle button icon,
* and persists the preference in localStorage.
*/
function toggleDarkMode() { function toggleDarkMode() {
const body = document.documentElement; const body = document.documentElement;
const isDark = body.getAttribute('data-theme') === 'dark'; const isDark = body.getAttribute('data-theme') === 'dark';
@@ -15,7 +30,11 @@ function toggleDarkMode() {
} }
} }
// Load saved theme preference /**
* Load saved theme preference from localStorage.
*
* Called on page load to restore the user's preferred theme.
*/
function loadTheme() { function loadTheme() {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
const toggle = document.getElementById('darkModeToggle'); const toggle = document.getElementById('darkModeToggle');
@@ -28,13 +47,22 @@ function loadTheme() {
} }
} }
// Sidebar toggle for mobile /**
* Toggle sidebar visibility on mobile devices.
*
* Adds/removes 'open' class on sidebar to show/hide it.
*/
function toggleSidebar() { function toggleSidebar() {
const sidebar = document.getElementById('sidebar'); const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('open'); sidebar.classList.toggle('open');
} }
// Close sidebar when clicking outside on mobile /**
* Handle click outside sidebar to close it on mobile.
*
* Listens for document clicks and closes sidebar when clicking
* outside of it, but only on screens smaller than 768px.
*/
document.addEventListener('click', function(event) { document.addEventListener('click', function(event) {
const sidebar = document.getElementById('sidebar'); const sidebar = document.getElementById('sidebar');
const toggle = document.getElementById('sidebarToggle'); const toggle = document.getElementById('sidebarToggle');
@@ -45,7 +73,11 @@ document.addEventListener('click', function(event) {
} }
}); });
// Confirm delete actions /**
* Initialize confirmation dialogs for elements with data-confirm attribute.
*
* Adds click handler to show confirmation dialog before submitting forms.
*/
document.querySelectorAll('[data-confirm]').forEach(function(el) { document.querySelectorAll('[data-confirm]').forEach(function(el) {
el.addEventListener('click', function(e) { el.addEventListener('click', function(e) {
if (!confirm(this.getAttribute('data-confirm'))) { if (!confirm(this.getAttribute('data-confirm'))) {
@@ -54,7 +86,12 @@ document.querySelectorAll('[data-confirm]').forEach(function(el) {
}); });
}); });
// Draggable Blocks Functionality /**
* Initialize draggable blocks functionality on dashboard pages.
*
* Adds drag handles to card headers and enables drag-and-drop
* reordering of cards. Saves layout to localStorage.
*/
function initDraggableBlocks() { function initDraggableBlocks() {
const grids = document.querySelectorAll('.dashboard-grid'); const grids = document.querySelectorAll('.dashboard-grid');
@@ -90,6 +127,13 @@ function initDraggableBlocks() {
}); });
} }
/**
* Make a card header draggable.
*
* @param {HTMLElement} header - The card header element
* @param {HTMLElement} card - The card element being dragged
* @param {HTMLElement} grid - The parent grid container
*/
function makeHeaderDraggable(header, card, grid) { function makeHeaderDraggable(header, card, grid) {
let draggedElement = null; let draggedElement = null;
let placeholder = null; let placeholder = null;
@@ -147,6 +191,11 @@ function makeHeaderDraggable(header, card, grid) {
ghost.style.top = (e.clientY - offsetY) + 'px'; ghost.style.top = (e.clientY - offsetY) + 'px';
ghost.style.left = (e.clientX - offsetX) + 'px'; ghost.style.left = (e.clientX - offsetX) + 'px';
/**
* Update ghost position during drag.
* @param {number} clientY - Mouse Y coordinate
* @param {number} clientX - Mouse X coordinate
*/
function updateGhostPosition(clientY, clientX) { function updateGhostPosition(clientY, clientX) {
// Position ghost directly at cursor position // Position ghost directly at cursor position
ghost.style.top = (clientY - offsetY) + 'px'; ghost.style.top = (clientY - offsetY) + 'px';
@@ -158,7 +207,11 @@ function makeHeaderDraggable(header, card, grid) {
const gridStyle = window.getComputedStyle(grid); const gridStyle = window.getComputedStyle(grid);
const gridGap = parseInt(gridStyle.gap) || 20; const gridGap = parseInt(gridStyle.gap) || 20;
// Calculate column positions for 2D grid /**
* Calculate which column the X position falls into.
* @param {number} x - X coordinate relative to grid
* @returns {number} Column index
*/
function getColumnFromX(x) { function getColumnFromX(x) {
// Calculate which column the x position falls into // Calculate which column the x position falls into
const relativeX = x - gridRect.left; const relativeX = x - gridRect.left;
@@ -166,6 +219,10 @@ function makeHeaderDraggable(header, card, grid) {
return Math.floor(relativeX / (colWidth + gridGap)); return Math.floor(relativeX / (colWidth + gridGap));
} }
/**
* Handle mouse movement during drag.
* @param {MouseEvent} e - Mouse event
*/
function onMouseMove(e) { function onMouseMove(e) {
e.preventDefault(); e.preventDefault();
@@ -249,6 +306,10 @@ function makeHeaderDraggable(header, card, grid) {
}); });
} }
/**
* Handle mouse release to complete drag.
* @param {MouseEvent} e - Mouse event
*/
function onMouseUp(e) { function onMouseUp(e) {
if (animationFrame) { if (animationFrame) {
cancelAnimationFrame(animationFrame); cancelAnimationFrame(animationFrame);
@@ -291,6 +352,11 @@ function makeHeaderDraggable(header, card, grid) {
}); });
} }
/**
* Save the current card layout order to localStorage.
*
* @param {HTMLElement} grid - The grid container to save layout for
*/
function saveLayout(grid) { function saveLayout(grid) {
const pageKey = getPageKey(); const pageKey = getPageKey();
const cards = grid.querySelectorAll('.card'); const cards = grid.querySelectorAll('.card');
@@ -298,6 +364,11 @@ function saveLayout(grid) {
localStorage.setItem('blockLayout_' + pageKey, JSON.stringify(order)); localStorage.setItem('blockLayout_' + pageKey, JSON.stringify(order));
} }
/**
* Load saved card layout from localStorage.
*
* @param {HTMLElement} grid - The grid container to load layout for
*/
function loadLayout(grid) { function loadLayout(grid) {
const pageKey = getPageKey(); const pageKey = getPageKey();
const saved = localStorage.getItem('blockLayout_' + pageKey); const saved = localStorage.getItem('blockLayout_' + pageKey);
@@ -320,6 +391,11 @@ function loadLayout(grid) {
} }
} }
/**
* Get the current page key for layout storage.
*
* @returns {string} Page identifier based on URL path
*/
function getPageKey() { function getPageKey() {
const path = window.location.pathname; const path = window.location.pathname;
if (path.includes('/tryouts/')) { if (path.includes('/tryouts/')) {
@@ -331,7 +407,11 @@ function getPageKey() {
return 'dashboard'; return 'dashboard';
} }
// Initialize draggable blocks on DOM ready /**
* Initialize on DOM ready.
*
* Loads theme preference and initializes draggable blocks.
*/
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Load theme preference // Load theme preference
loadTheme(); loadTheme();
@@ -339,6 +419,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize draggable blocks // Initialize draggable blocks
initDraggableBlocks(); initDraggableBlocks();
/**
* Auto-dismiss flash alerts after 5 seconds.
*/
const alerts = document.querySelectorAll('.alert-dismissible'); const alerts = document.querySelectorAll('.alert-dismissible');
alerts.forEach(function(alert) { alerts.forEach(function(alert) {
setTimeout(function() { setTimeout(function() {
+141
View File
@@ -0,0 +1,141 @@
{#
Jinja Macros for Team Tryouts Application
This file contains reusable HTML components to reduce code duplication
across templates. Import with {% import 'layouts/macros.html' as macros %}
#}
{# Page Header Macro - renders title and breadcrumb #}
{% macro page_header(title, breadcrumb) %}
{% block title %}{{ title }} - TryoutPro{% endblock %}
{% block page_title %}{{ title }}{% endblock %}
{% block breadcrumb %}<span class="breadcrumb">{{ breadcrumb }}</span>{% endblock %}
{% endmacro %}
{# Card Header Macro - renders card with optional actions #}
{% macro card_header(title, icon_class, actions=None) %}
<div class="card-header">
<h3><i class="{{ icon_class }}"></i> {{ title }}</h3>
{% if actions %}
<div class="card-actions">{{ actions }}</div>
{% endif %}
</div>
{% endmacro %}
{# Stat Card Macro - renders statistics card #}
{% macro stat_card(value, label, icon_class, bg_class) %}
<div class="stat-card">
<div class="stat-icon {{ bg_class }}">
<i class="{{ icon_class }}"></i>
</div>
<div class="stat-info">
<h3>{{ value }}</h3>
<p>{{ label }}</p>
</div>
</div>
{% endmacro %}
{# Badge Macro - renders status/role badges with appropriate styling #}
{% macro badge(text, type='default') %}
<span class="badge badge-{{ type }}">{{ text }}</span>
{% endmacro %}
{# User Avatar Macro - renders user avatar with name #}
{% macro user_avatar(user, size='sm') %}
<div class="user-mini">
<div class="avatar-{{ size }}">{{ user.full_name[:2] | upper }}</div>
<span>{{ user.full_name }}</span>
</div>
{% endmacro %}
{# Form Field Macro - renders labeled form input #}
{% macro form_field(label, type, name, value='', placeholder='', required=false, extra_classes='') %}
<div class="form-group">
<label for="{{ name }}">{{ label }}</label>
<input type="{{ type }}" id="{{ name }}" name="{{ name }}" value="{{ value }}" placeholder="{{ placeholder }}" {% if required %}required{% endif %} class="{{ extra_classes }}">
</div>
{% endmacro %}
{# Form Row Macro - renders a row of form fields #}
{% macro form_row(fields) %}
<div class="form-row">
{% for field in fields %}
<div class="form-group {{ field.col_class | default('col-6') }}">
<label for="{{ field.name }}">{{ field.label }}</label>
{% if field.type == 'select' %}
<select id="{{ field.name }}" name="{{ field.name }}" {% if field.required %}required{% endif %}>
{% for option in field.options %}
<option value="{{ option.value }}" {% if option.selected %}selected{% endif %}>{{ option.label }}</option>
{% endfor %}
</select>
{% else %}
<input type="{{ field.type }}" id="{{ field.name }}" name="{{ field.name }}" value="{{ field.value }}" placeholder="{{ field.placeholder }}" {% if field.required %}required{% endif %}>
{% endif %}
</div>
{% endfor %}
</div>
{% endmacro %}
{# Table Macro - renders a table with headers and optional empty state #}
{% macro table(headers, rows, empty_message='No records found') %}
<div class="table-container">
<table class="table">
<thead>
<tr>
{% for header in headers %}
<th>{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% if rows %}
{{ rows }}
{% else %}
<tr>
<td colspan="{{ headers | length }}" class="text-center">
<div class="empty-state">
<i class="fas fa-info-circle"></i>
<h3>{{ empty_message }}</h3>
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% endmacro %}
{# Modal Macro - renders a modal dialog #}
{% macro modal(id, title, content, footer_buttons=None) %}
<div id="{{ id }}" class="modal hidden">
<div class="modal-backdrop" onclick="hideModal('{{ id }}')"></div>
<div class="modal-content">
<div class="modal-header">
<h3>{{ title }}</h3>
<button class="modal-close" onclick="hideModal('{{ id }}')">&times;</button>
</div>
<div class="modal-body">
{{ content }}
{% if footer_buttons %}
<div class="form-actions mt-3">{{ footer_buttons }}</div>
{% endif %}
</div>
</div>
</div>
{% endmacro %}
{# Detail Item Macro - renders a key-value pair in detail view #}
{% macro detail_item(label, value) %}
<div class="detail-item">
<span class="detail-label">{{ label }}</span>
<span class="detail-value">{{ value }}</span>
</div>
{% endmacro %}
{# Action Button Macro - renders a button link #}
{% macro action_button(url, text, icon, style='outline', size='sm') %}
<a href="{{ url }}" class="btn btn-{{ size }} btn-{{ style }}">
{% if icon %}<i class="{{ icon }}"></i>{% endif %}
{{ text }}
</a>
{% endmacro %}
+15 -4
View File
@@ -16,19 +16,30 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-6"> <div class="form-group col-6">
<label for="date">Date</label> <label for="game">Game</label>
<input type="date" id="date" name="date" required> <select id="game" name="game" class="form-select" required>
<option value="">-- Select a game --</option>
{% for game in esport_games %}
<option value="{{ game }}">{{ game }}</option>
{% endfor %}
</select>
</div> </div>
<div class="form-group col-6"> <div class="form-group col-6">
<label for="location">Location</label> <label for="date">Date</label>
<input type="text" id="location" name="location" placeholder="e.g., Main Field"> <input type="date" id="date" name="date" required>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-6">
<label for="location">Location</label>
<input type="text" id="location" name="location" placeholder="e.g., Online">
</div>
<div class="form-group col-6"> <div class="form-group col-6">
<label for="max_players">Max Players</label> <label for="max_players">Max Players</label>
<input type="number" id="max_players" name="max_players" placeholder="Leave blank for unlimited" min="1"> <input type="number" id="max_players" name="max_players" placeholder="Leave blank for unlimited" min="1">
</div> </div>
</div>
<div class="form-row">
<div class="form-group col-6"> <div class="form-group col-6">
<label for="target_org_team_id">Target Team</label> <label for="target_org_team_id">Target Team</label>
<select id="target_org_team_id" name="target_org_team_id" class="form-select"> <select id="target_org_team_id" name="target_org_team_id" class="form-select">
+15 -4
View File
@@ -16,19 +16,30 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-6"> <div class="form-group col-6">
<label for="date">Date</label> <label for="game">Game</label>
<input type="date" id="date" name="date" value="{{ tryout.date.strftime('%Y-%m-%d') }}" required> <select id="game" name="game" class="form-select" required>
<option value="">-- Select a game --</option>
{% for game in esport_games %}
<option value="{{ game }}" {% if tryout.game == game %}selected{% endif %}>{{ game }}</option>
{% endfor %}
</select>
</div> </div>
<div class="form-group col-6"> <div class="form-group col-6">
<label for="location">Location</label> <label for="date">Date</label>
<input type="text" id="location" name="location" value="{{ tryout.location or '' }}" placeholder="e.g., Main Field"> <input type="date" id="date" name="date" value="{{ tryout.date.strftime('%Y-%m-%d') }}" required>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-6">
<label for="location">Location</label>
<input type="text" id="location" name="location" value="{{ tryout.location or '' }}" placeholder="e.g., Online">
</div>
<div class="form-group col-6"> <div class="form-group col-6">
<label for="max_players">Max Players</label> <label for="max_players">Max Players</label>
<input type="number" id="max_players" name="max_players" value="{{ tryout.max_players or '' }}" placeholder="Leave blank for unlimited" min="1"> <input type="number" id="max_players" name="max_players" value="{{ tryout.max_players or '' }}" placeholder="Leave blank for unlimited" min="1">
</div> </div>
</div>
<div class="form-row">
<div class="form-group col-6"> <div class="form-group col-6">
<label for="target_org_team_id">Target Team</label> <label for="target_org_team_id">Target Team</label>
<select id="target_org_team_id" name="target_org_team_id" class="form-select"> <select id="target_org_team_id" name="target_org_team_id" class="form-select">
+84 -23
View File
@@ -8,7 +8,7 @@
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3><i class="fas fa-clipboard-list"></i> Player Evaluation</h3> <h3><i class="fas fa-clipboard-list"></i> Player Evaluation</h3>
<span class="badge badge-info">{{ tryout.title }}</span> <span class="badge badge-info">{{ tryout.title }} - {{ tryout.game }}</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="eval-player-info mb-4"> <div class="eval-player-info mb-4">
@@ -30,46 +30,90 @@
<div class="form-row"> <div class="form-row">
<div class="form-group col-4"> <div class="form-group col-4">
<label for="speed_score">Speed (1-10)</label> <label for="mecanics_score">Mecanics (1-10)</label>
<div class="score-input"> <div class="score-input">
<input type="range" id="speed_score" name="speed_score" min="1" max="10" value="{{ existing_eval.speed_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value"> <input type="range" id="mecanics_score" name="mecanics_score" min="1" max="10" value="{{ existing_eval.mecanics_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
<span class="range-value">{{ existing_eval.speed_score or 5 }}</span> <span class="range-value">{{ existing_eval.mecanics_score or 5 }}</span>
</div> </div>
</div> </div>
<div class="form-group col-4"> <div class="form-group col-4">
<label for="agility_score">Agility (1-10)</label> <label for="cohesion_score">Cohesion (1-10)</label>
<div class="score-input"> <div class="score-input">
<input type="range" id="agility_score" name="agility_score" min="1" max="10" value="{{ existing_eval.agility_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value"> <input type="range" id="cohesion_score" name="cohesion_score" min="1" max="10" value="{{ existing_eval.cohesion_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
<span class="range-value">{{ existing_eval.agility_score or 5 }}</span> <span class="range-value">{{ existing_eval.cohesion_score or 5 }}</span>
</div> </div>
</div> </div>
<div class="form-group col-4"> <div class="form-group col-4">
<label for="technique_score">Technique (1-10)</label> <label for="communication_score">Communication (1-10)</label>
<div class="score-input"> <div class="score-input">
<input type="range" id="technique_score" name="technique_score" min="1" max="10" value="{{ existing_eval.technique_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value"> <input type="range" id="communication_score" name="communication_score" min="1" max="10" value="{{ existing_eval.communication_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
<span class="range-value">{{ existing_eval.technique_score or 5 }}</span> <span class="range-value">{{ existing_eval.communication_score or 5 }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-4"> <div class="form-group col-4">
<label for="teamwork_score">Teamwork (1-10)</label> <label for="gamesense_score">Gamesense (1-10)</label>
<div class="score-input"> <div class="score-input">
<input type="range" id="teamwork_score" name="teamwork_score" min="1" max="10" value="{{ existing_eval.teamwork_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value"> <input type="range" id="gamesense_score" name="gamesense_score" min="1" max="10" value="{{ existing_eval.gamesense_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
<span class="range-value">{{ existing_eval.teamwork_score or 5 }}</span> <span class="range-value">{{ existing_eval.gamesense_score or 5 }}</span>
</div> </div>
</div> </div>
<div class="form-group col-4"> <div class="form-group col-4">
<label for="attitude_score">Attitude (1-10)</label> <label for="versatility_score">Versatility (1-10)</label>
<div class="score-input"> <div class="score-input">
<input type="range" id="attitude_score" name="attitude_score" min="1" max="10" value="{{ existing_eval.attitude_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value"> <input type="range" id="versatility_score" name="versatility_score" min="1" max="10" value="{{ existing_eval.versatility_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
<span class="range-value">{{ existing_eval.attitude_score or 5 }}</span> <span class="range-value">{{ existing_eval.versatility_score or 5 }}</span>
</div> </div>
</div> </div>
<div class="form-group col-4"> <div class="form-group col-4">
<label for="discipline_score">Discipline (1-10)</label>
<div class="score-input">
<input type="range" id="discipline_score" name="discipline_score" min="1" max="10" value="{{ existing_eval.discipline_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
<span class="range-value">{{ existing_eval.discipline_score or 5 }}</span>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-4">
<label for="analysis_score">Analysis (1-10)</label>
<div class="score-input">
<input type="range" id="analysis_score" name="analysis_score" min="1" max="10" value="{{ existing_eval.analysis_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
<span class="range-value">{{ existing_eval.analysis_score or 5 }}</span>
</div>
</div>
<div class="form-group col-4">
<label for="sport_ethics_score">Sport Ethics (1-10)</label>
<div class="score-input">
<input type="range" id="sport_ethics_score" name="sport_ethics_score" min="1" max="10" value="{{ existing_eval.sport_ethics_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
<span class="range-value">{{ existing_eval.sport_ethics_score or 5 }}</span>
</div>
</div>
<div class="form-group col-4">
<label for="mental_score">Mental (1-10)</label>
<div class="score-input">
<input type="range" id="mental_score" name="mental_score" min="1" max="10" value="{{ existing_eval.mental_score or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
<span class="range-value">{{ existing_eval.mental_score or 5 }}</span>
</div>
</div>
</div>
{% set positions = GAME_POSITIONS.get(tryout.game, []) %}
<div class="form-row">
<div class="form-group col-12">
<label for="position_recommendation">Recommended Position</label> <label for="position_recommendation">Recommended Position</label>
<input type="text" id="position_recommendation" name="position_recommendation" value="{{ existing_eval.position_recommendation or '' }}" placeholder="e.g., Forward, Defense"> {% if positions %}
<select id="position_recommendation" name="position_recommendation" class="form-select">
<option value="">-- Select Position --</option>
{% for pos in positions %}
<option value="{{ pos }}" {% if existing_eval.position_recommendation == pos %}selected{% endif %}>{{ pos }}</option>
{% endfor %}
</select>
{% else %}
<input type="text" id="position_recommendation" name="position_recommendation" value="{{ existing_eval.position_recommendation or '' }}" placeholder="Enter position (optional)">
{% endif %}
</div> </div>
</div> </div>
@@ -96,17 +140,34 @@
<div class="card-body"> <div class="card-body">
<table class="table"> <table class="table">
<thead> <thead>
<tr><th>Evaluator</th><th>Speed</th><th>Agility</th><th>Technique</th><th>Teamwork</th><th>Attitude</th><th>Overall</th><th>Position</th></tr> <tr>
<th>Evaluator</th>
<th>Mecanics</th>
<th>Cohesion</th>
<th>Communication</th>
<th>Gamesense</th>
<th>Versatility</th>
<th>Discipline</th>
<th>Analysis</th>
<th>Sport Ethics</th>
<th>Mental</th>
<th>Overall</th>
<th>Position</th>
</tr>
</thead> </thead>
<tbody> <tbody>
{% for entry in evaluators %} {% for entry in evaluators %}
<tr> <tr>
<td>{{ entry.evaluator.full_name }}</td> <td>{{ entry.evaluator.full_name }}</td>
<td>{{ entry.eval.speed_score or '-' }}</td> <td>{{ entry.eval.mecanics_score or '-' }}</td>
<td>{{ entry.eval.agility_score or '-' }}</td> <td>{{ entry.eval.cohesion_score or '-' }}</td>
<td>{{ entry.eval.technique_score or '-' }}</td> <td>{{ entry.eval.communication_score or '-' }}</td>
<td>{{ entry.eval.teamwork_score or '-' }}</td> <td>{{ entry.eval.gamesense_score or '-' }}</td>
<td>{{ entry.eval.attitude_score or '-' }}</td> <td>{{ entry.eval.versatility_score or '-' }}</td>
<td>{{ entry.eval.discipline_score or '-' }}</td>
<td>{{ entry.eval.analysis_score or '-' }}</td>
<td>{{ entry.eval.sport_ethics_score or '-' }}</td>
<td>{{ entry.eval.mental_score or '-' }}</td>
<td><span class="score">{{ entry.eval.overall_score or '-' }}</span></td> <td><span class="score">{{ entry.eval.overall_score or '-' }}</span></td>
<td>{{ entry.eval.position_recommendation or '-' }}</td> <td>{{ entry.eval.position_recommendation or '-' }}</td>
</tr> </tr>
+19 -11
View File
@@ -38,11 +38,15 @@
<th>Tryout</th> <th>Tryout</th>
<th>Player</th> <th>Player</th>
<th>Evaluator</th> <th>Evaluator</th>
<th>Speed</th> <th>Mecanics</th>
<th>Agility</th> <th>Cohesion</th>
<th>Technique</th> <th>Communication</th>
<th>Teamwork</th> <th>Gamesense</th>
<th>Attitude</th> <th>Versatility</th>
<th>Discipline</th>
<th>Analysis</th>
<th>Sport Ethics</th>
<th>Mental</th>
<th>Overall</th> <th>Overall</th>
<th>Position</th> <th>Position</th>
<th>Date</th> <th>Date</th>
@@ -54,18 +58,22 @@
<td>{{ eval.tryout.title }}</td> <td>{{ eval.tryout.title }}</td>
<td>{{ eval.player.full_name }}</td> <td>{{ eval.player.full_name }}</td>
<td>{{ eval.evaluator.full_name }}</td> <td>{{ eval.evaluator.full_name }}</td>
<td>{{ eval.speed_score or '-' }}</td> <td>{{ eval.mecanics_score or '-' }}</td>
<td>{{ eval.agility_score or '-' }}</td> <td>{{ eval.cohesion_score or '-' }}</td>
<td>{{ eval.technique_score or '-' }}</td> <td>{{ eval.communication_score or '-' }}</td>
<td>{{ eval.teamwork_score or '-' }}</td> <td>{{ eval.gamesense_score or '-' }}</td>
<td>{{ eval.attitude_score or '-' }}</td> <td>{{ eval.versatility_score or '-' }}</td>
<td>{{ eval.discipline_score or '-' }}</td>
<td>{{ eval.analysis_score or '-' }}</td>
<td>{{ eval.sport_ethics_score or '-' }}</td>
<td>{{ eval.mental_score or '-' }}</td>
<td><span class="score">{{ eval.overall_score or '-' }}</span></td> <td><span class="score">{{ eval.overall_score or '-' }}</span></td>
<td><span class="badge badge-info">{{ eval.position_recommendation or 'N/A' }}</span></td> <td><span class="badge badge-info">{{ eval.position_recommendation or 'N/A' }}</span></td>
<td>{{ eval.created_at.strftime('%m/%d/%Y') }}</td> <td>{{ eval.created_at.strftime('%m/%d/%Y') }}</td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td colspan="11" class="text-center"> <td colspan="15" class="text-center">
<div class="empty-state"> <div class="empty-state">
<i class="fas fa-clipboard"></i> <i class="fas fa-clipboard"></i>
<h3>No evaluations yet</h3> <h3>No evaluations yet</h3>
+35 -13
View File
@@ -32,7 +32,11 @@
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="detail-grid"> <div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Game</span>
<span class="detail-value">{{ tryout.game }}</span>
</div>
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">Date</span> <span class="detail-label">Date</span>
<span class="detail-value">{{ tryout.date.strftime('%A, %B %d, %Y') }}</span> <span class="detail-value">{{ tryout.date.strftime('%A, %B %d, %Y') }}</span>
@@ -209,6 +213,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% if can_edit and registered_players %} {% if can_edit and registered_players %}
{% set positions = game_positions.get(tryout.game, []) %}
<form method="POST" action="{{ url_for('tryouts.add_to_team', tryout_id=tryout.id, team_id=team.team.id) }}" class="form-inline mt-2"> <form method="POST" action="{{ url_for('tryouts.add_to_team', tryout_id=tryout.id, team_id=team.team.id) }}" class="form-inline mt-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<select name="player_id" class="form-select" required> <select name="player_id" class="form-select" required>
@@ -219,7 +224,16 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
{% if positions %}
<select name="position" class="form-select mx-2">
<option value="">-- Select Position --</option>
{% for pos in positions %}
<option value="{{ pos }}">{{ pos }}</option>
{% endfor %}
</select>
{% else %}
<input type="text" name="position" placeholder="Position" class="form-input mx-2"> <input type="text" name="position" placeholder="Position" class="form-input mx-2">
{% endif %}
<button type="submit" class="btn btn-sm btn-primary">Add</button> <button type="submit" class="btn btn-sm btn-primary">Add</button>
</form> </form>
{% endif %} {% endif %}
@@ -378,11 +392,15 @@
<tr> <tr>
<th>Player</th> <th>Player</th>
<th>Evaluator</th> <th>Evaluator</th>
<th>Speed</th> <th>Mecanics</th>
<th>Agility</th> <th>Cohesion</th>
<th>Technique</th> <th>Communication</th>
<th>Teamwork</th> <th>Gamesense</th>
<th>Attitude</th> <th>Versatility</th>
<th>Discipline</th>
<th>Analysis</th>
<th>Sport Ethics</th>
<th>Mental</th>
<th>Overall</th> <th>Overall</th>
<th>Recommendation</th> <th>Recommendation</th>
</tr> </tr>
@@ -392,17 +410,21 @@
<tr> <tr>
<td>{{ eval.player.full_name }}</td> <td>{{ eval.player.full_name }}</td>
<td>{{ eval.evaluator.full_name }}</td> <td>{{ eval.evaluator.full_name }}</td>
<td>{{ eval.speed_score or '-' }}</td> <td>{{ eval.mecanics_score or '-' }}</td>
<td>{{ eval.agility_score or '-' }}</td> <td>{{ eval.cohesion_score or '-' }}</td>
<td>{{ eval.technique_score or '-' }}</td> <td>{{ eval.communication_score or '-' }}</td>
<td>{{ eval.teamwork_score or '-' }}</td> <td>{{ eval.gamesense_score or '-' }}</td>
<td>{{ eval.attitude_score or '-' }}</td> <td>{{ eval.versatility_score or '-' }}</td>
<td>{{ eval.discipline_score or '-' }}</td>
<td>{{ eval.analysis_score or '-' }}</td>
<td>{{ eval.sport_ethics_score or '-' }}</td>
<td>{{ eval.mental_score or '-' }}</td>
<td><span class="score">{{ eval.overall_score or '-' }}</span></td> <td><span class="score">{{ eval.overall_score or '-' }}</span></td>
<td>{{ eval.position_recommendation or '-' }}</td> <td>{{ eval.position_recommendation or '-' }}</td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td colspan="9" class="text-center">No evaluations yet.</td> <td colspan="13" class="text-center">No evaluations yet.</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -412,7 +434,7 @@
</div> </div>
</div> </div>
<script> <script>
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); } function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); }
function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); } function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); }