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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
from flask import Flask
|
||||
from extensions import db, login_manager, csrf, hash_password, check_password
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
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.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'team-tryouts-secret-key-change-in-production')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///team_tryouts.db'
|
||||
@@ -51,6 +72,7 @@ def create_app():
|
||||
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
@@ -3,14 +3,37 @@ from flask_login import LoginManager
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
|
||||
# Database and extension initialization
|
||||
db = SQLAlchemy()
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message_category = 'info'
|
||||
csrf = CSRFProtect()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def check_password(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.
@@ -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 flask_login import UserMixin
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
# Available user roles in the system
|
||||
ROLES = ['president', 'manager', 'coach', 'player', 'scout']
|
||||
|
||||
# Popular E-Sports games list
|
||||
# Popular E-Sports games list for player profiles
|
||||
ESPORT_GAMES = [
|
||||
'Valorant',
|
||||
'League of Legends',
|
||||
@@ -13,15 +21,56 @@ ESPORT_GAMES = [
|
||||
'Apex Legends',
|
||||
'Overwatch 2',
|
||||
'Rainbow Six Siege',
|
||||
'Fortnite',
|
||||
'Rocket League',
|
||||
'Call of Duty',
|
||||
'Dota 2',
|
||||
'Super Smash Bros.',
|
||||
'Street Fighter 6',
|
||||
'Super Smash Bros.'
|
||||
]
|
||||
|
||||
# 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):
|
||||
"""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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
@@ -36,8 +85,8 @@ class User(UserMixin, db.Model):
|
||||
|
||||
# E-Sports specific fields
|
||||
games = db.Column(db.Text, nullable=True) # Comma-separated list of games
|
||||
discord_username = db.Column(db.String(100), nullable=True) # Discord handle
|
||||
league_os_profile = db.Column(db.String(255), nullable=True) # League OS profile URL or ID
|
||||
discord_username = db.Column(db.String(128), nullable=True) # Discord handle
|
||||
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_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')
|
||||
|
||||
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:
|
||||
return [g.strip() for g in self.games.split(',') if g.strip()]
|
||||
return []
|
||||
|
||||
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
|
||||
|
||||
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']
|
||||
|
||||
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'
|
||||
|
||||
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):
|
||||
"""Check if user can manage organization teams.
|
||||
|
||||
Returns:
|
||||
bool: True if user is president or manager.
|
||||
"""
|
||||
return self.role in ['president', 'manager']
|
||||
|
||||
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']
|
||||
|
||||
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':
|
||||
return True
|
||||
if self.role == 'manager' and tryout.created_by == self.id:
|
||||
@@ -81,7 +177,17 @@ class User(UserMixin, db.Model):
|
||||
return False
|
||||
|
||||
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':
|
||||
return True
|
||||
if self.role == 'manager':
|
||||
@@ -91,15 +197,14 @@ class User(UserMixin, db.Model):
|
||||
return False
|
||||
|
||||
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}
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
|
||||
# Platform options for games that require platform specification
|
||||
GAME_PLATFORMS = {
|
||||
'Valorant': [], # No platform needed
|
||||
@@ -108,12 +213,8 @@ GAME_PLATFORMS = {
|
||||
'Apex Legends': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'],
|
||||
'Overwatch 2': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'],
|
||||
'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
|
||||
'Call of Duty': ['PC', 'PlayStation', 'Xbox'],
|
||||
'Dota 2': [],
|
||||
'Super Smash Bros.': ['Nintendo Switch'],
|
||||
'Street Fighter 6': ['PC', 'PlayStation', 'Xbox'],
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +225,6 @@ PLATFORM_CODES = {
|
||||
'Xbox': 'xbl',
|
||||
'Nintendo Switch': 'switch',
|
||||
'PC': 'pc',
|
||||
'Mobile': 'mobile',
|
||||
'Steam': 'steam',
|
||||
'Epic': 'epic',
|
||||
}
|
||||
@@ -137,17 +237,24 @@ TRN_URLS = {
|
||||
'Apex Legends': 'https://tracker.gg/apex/profile/{platform}/{username}',
|
||||
'Overwatch 2': 'https://tracker.gg/overwatch/profile/{platform}/{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}',
|
||||
'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}',
|
||||
'Street Fighter 6': 'https://tracker.gg/streetfighter/profile/{username}',
|
||||
}
|
||||
|
||||
|
||||
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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
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):
|
||||
"""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:
|
||||
return None
|
||||
|
||||
@@ -182,7 +296,18 @@ class UserGamertag(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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False, unique=True)
|
||||
@@ -196,10 +321,29 @@ class OrgTeam(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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
game = db.Column(db.String(50), nullable=False)
|
||||
date = db.Column(db.Date, nullable=False)
|
||||
location = db.Column(db.String(200), nullable=True)
|
||||
status = db.Column(db.String(20), default='upcoming') # upcoming, in_progress, completed
|
||||
@@ -216,6 +360,19 @@ class Tryout(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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
@@ -226,16 +383,41 @@ class TryoutRegistration(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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
evaluator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
speed_score = db.Column(db.Integer, nullable=True)
|
||||
agility_score = db.Column(db.Integer, nullable=True)
|
||||
technique_score = db.Column(db.Integer, nullable=True)
|
||||
teamwork_score = db.Column(db.Integer, nullable=True)
|
||||
attitude_score = db.Column(db.Integer, nullable=True)
|
||||
mecanics_score = db.Column(db.Integer, nullable=True)
|
||||
cohesion_score = db.Column(db.Integer, nullable=True)
|
||||
communication_score = db.Column(db.Integer, nullable=True)
|
||||
gamesense_score = db.Column(db.Integer, nullable=True)
|
||||
versatility_score = db.Column(db.Integer, nullable=True)
|
||||
discipline_score = db.Column(db.Integer, nullable=True)
|
||||
analysis_score = db.Column(db.Integer, nullable=True)
|
||||
sport_ethics_score = db.Column(db.Integer, nullable=True)
|
||||
mental_score = db.Column(db.Integer, nullable=True)
|
||||
overall_score = db.Column(db.Float, nullable=True)
|
||||
comments = db.Column(db.Text, nullable=True)
|
||||
position_recommendation = db.Column(db.String(50), nullable=True)
|
||||
@@ -248,7 +430,18 @@ class Evaluation(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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
@@ -261,6 +454,15 @@ class Team(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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
|
||||
@@ -272,7 +474,26 @@ class TeamMember(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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
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')
|
||||
|
||||
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()]
|
||||
|
||||
|
||||
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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
@@ -317,7 +554,18 @@ class PlayerDisponibility(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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=False)
|
||||
@@ -330,7 +578,25 @@ class MatchParticipant(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'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
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])
|
||||
|
||||
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
|
||||
if user.id == self.player_id:
|
||||
return True
|
||||
@@ -378,6 +654,15 @@ class Contract(db.Model):
|
||||
return False
|
||||
|
||||
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
|
||||
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.
@@ -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_login import login_user, logout_user, login_required, current_user
|
||||
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.route('/login', methods=['GET', 'POST'])
|
||||
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:
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
@@ -28,8 +45,20 @@ def login():
|
||||
|
||||
return render_template('pages/login.html')
|
||||
|
||||
|
||||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||
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:
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
@@ -80,9 +109,17 @@ def register():
|
||||
|
||||
return render_template('pages/register.html', esport_games=ESPORT_GAMES)
|
||||
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
@login_required
|
||||
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()
|
||||
flash('You have been logged out.', 'info')
|
||||
return redirect(url_for('auth.login'))
|
||||
+77
-21
@@ -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 extensions import db
|
||||
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.route('')
|
||||
@login_required
|
||||
def list_evaluations():
|
||||
"""List all evaluations accessible to the current user.
|
||||
|
||||
President: All evaluations with player score summaries.
|
||||
Evaluators (coach/manager): Their given evaluations.
|
||||
Players: Their received evaluations.
|
||||
|
||||
Returns:
|
||||
Response: Rendered evaluations list template.
|
||||
"""
|
||||
user = current_user
|
||||
|
||||
if user.role == 'president':
|
||||
@@ -33,9 +48,22 @@ def list_evaluations():
|
||||
|
||||
return render_template('pages/evaluations.html', evaluations=evaluations, player_scores=player_scores)
|
||||
|
||||
|
||||
@evaluations_bp.route('/<int:tryout_id>/<int:player_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def evaluate_player(tryout_id, player_id):
|
||||
"""Evaluate a specific player in a tryout.
|
||||
|
||||
GET: Render the evaluation form with any existing evaluation.
|
||||
POST: Create or update the evaluation for the player.
|
||||
|
||||
Args:
|
||||
tryout_id: The ID of the tryout.
|
||||
player_id: The ID of the player to evaluate.
|
||||
|
||||
Returns:
|
||||
Response: Evaluation form or redirect to tryout view.
|
||||
"""
|
||||
if not current_user.can_evaluate():
|
||||
flash('You do not have permission to evaluate players.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
@@ -54,29 +82,41 @@ def evaluate_player(tryout_id, player_id):
|
||||
).first()
|
||||
|
||||
if request.method == 'POST':
|
||||
speed = request.form.get('speed_score')
|
||||
agility = request.form.get('agility_score')
|
||||
technique = request.form.get('technique_score')
|
||||
teamwork = request.form.get('teamwork_score')
|
||||
attitude = request.form.get('attitude_score')
|
||||
mecanics = request.form.get('mecanics_score')
|
||||
cohesion = request.form.get('cohesion_score')
|
||||
communication = request.form.get('communication_score')
|
||||
gamesense = request.form.get('gamesense_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')
|
||||
position = request.form.get('position_recommendation')
|
||||
|
||||
scores = []
|
||||
if speed: scores.append(int(speed))
|
||||
if agility: scores.append(int(agility))
|
||||
if technique: scores.append(int(technique))
|
||||
if teamwork: scores.append(int(teamwork))
|
||||
if attitude: scores.append(int(attitude))
|
||||
if mecanics: scores.append(int(mecanics))
|
||||
if cohesion: scores.append(int(cohesion))
|
||||
if communication: scores.append(int(communication))
|
||||
if gamesense: scores.append(int(gamesense))
|
||||
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
|
||||
|
||||
if existing_eval:
|
||||
existing_eval.speed_score = int(speed) if speed else None
|
||||
existing_eval.agility_score = int(agility) if agility else None
|
||||
existing_eval.technique_score = int(technique) if technique else None
|
||||
existing_eval.teamwork_score = int(teamwork) if teamwork else None
|
||||
existing_eval.attitude_score = int(attitude) if attitude else None
|
||||
existing_eval.mecanics_score = int(mecanics) if mecanics else None
|
||||
existing_eval.cohesion_score = int(cohesion) if cohesion else None
|
||||
existing_eval.communication_score = int(communication) if communication else None
|
||||
existing_eval.gamesense_score = int(gamesense) if gamesense 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.comments = comments
|
||||
existing_eval.position_recommendation = position
|
||||
@@ -86,11 +126,15 @@ def evaluate_player(tryout_id, player_id):
|
||||
tryout_id=tryout_id,
|
||||
player_id=player_id,
|
||||
evaluator_id=current_user.id,
|
||||
speed_score=int(speed) if speed else None,
|
||||
agility_score=int(agility) if agility else None,
|
||||
technique_score=int(technique) if technique else None,
|
||||
teamwork_score=int(teamwork) if teamwork else None,
|
||||
attitude_score=int(attitude) if attitude else None,
|
||||
mecanics_score=int(mecanics) if mecanics else None,
|
||||
cohesion_score=int(cohesion) if cohesion else None,
|
||||
communication_score=int(communication) if communication else None,
|
||||
gamesense_score=int(gamesense) if gamesense 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,
|
||||
comments=comments,
|
||||
position_recommendation=position
|
||||
@@ -112,9 +156,21 @@ def evaluate_player(tryout_id, player_id):
|
||||
existing_eval=existing_eval,
|
||||
evaluators=evaluators)
|
||||
|
||||
|
||||
@evaluations_bp.route('/<int:tryout_id>/players')
|
||||
@login_required
|
||||
def players_to_evaluate(tryout_id):
|
||||
"""List players that need evaluation in a specific tryout.
|
||||
|
||||
Shows all registered players and marks which ones have already been evaluated
|
||||
by the current user.
|
||||
|
||||
Args:
|
||||
tryout_id: The ID of the tryout.
|
||||
|
||||
Returns:
|
||||
Response: Rendered players-to-evaluate template.
|
||||
"""
|
||||
if not current_user.can_evaluate():
|
||||
flash('Permission denied.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
+30
-7
@@ -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_login import login_required, current_user
|
||||
from extensions import db
|
||||
@@ -7,13 +12,34 @@ from datetime import datetime, date
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
|
||||
@main_bp.route('/')
|
||||
def index():
|
||||
"""Redirect root URL to login page.
|
||||
|
||||
This is the entry point for the application when no specific route is provided.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to login page.
|
||||
"""
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
|
||||
@main_bp.route('/dashboard')
|
||||
@login_required
|
||||
def dashboard():
|
||||
"""Render the main dashboard with role-specific statistics.
|
||||
|
||||
Displays different statistics based on the user's role:
|
||||
- President: Overview of all users, tryouts, and evaluations
|
||||
- Manager: Their created tryouts and evaluations
|
||||
- Coach: Their evaluations and pending evaluations
|
||||
- Player: Their registrations, evaluations, and upcoming matches
|
||||
- Scout: Top-rated players across all tryouts
|
||||
|
||||
Returns:
|
||||
Response: Rendered dashboard template with user stats.
|
||||
"""
|
||||
user = current_user
|
||||
stats = {}
|
||||
|
||||
@@ -23,9 +49,9 @@ def dashboard():
|
||||
stats['total_tryouts'] = Tryout.query.count()
|
||||
stats['total_evaluations'] = Evaluation.query.count()
|
||||
stats['active_tryouts'] = Tryout.query.filter_by(status='in_progress').count()
|
||||
stats['upcoming_tryouts'] = Tryout.query.filter_by(status='upcoming').count()
|
||||
stats['recent_users'] = User.query.order_by(User.created_at.desc()).limit(5).all()
|
||||
stats['recent_tryouts'] = Tryout.query.order_by(Tryout.created_at.desc()).limit(5).all()
|
||||
stats['completed_tryouts'] = Tryout.query.filter_by(status='completed').count()
|
||||
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(10).all()
|
||||
|
||||
elif user.role == 'manager':
|
||||
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]
|
||||
evaluated_player_ids = [e.player_id for e in Evaluation.query.filter_by(evaluator_id=user.id).all()]
|
||||
stats['pending_evaluations'] = len(set(registered_player_ids) - set(evaluated_player_ids))
|
||||
stats['my_recent_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).order_by(Evaluation.created_at.desc()).limit(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':
|
||||
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_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
|
||||
today = date.today()
|
||||
|
||||
+87
-12
@@ -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_login import login_required, current_user
|
||||
from extensions import db
|
||||
@@ -8,21 +13,35 @@ matches_bp = Blueprint('matches', __name__, url_prefix='/matches')
|
||||
|
||||
|
||||
def can_schedule_match():
|
||||
"""Check if user can schedule matches (coaches and above)."""
|
||||
"""Check if user can schedule matches (coaches and above).
|
||||
|
||||
Returns:
|
||||
bool: True if user is president, manager, coach, or scout.
|
||||
"""
|
||||
return current_user.role in ['president', 'manager', 'coach', 'scout']
|
||||
|
||||
|
||||
@matches_bp.route('/calendar')
|
||||
@login_required
|
||||
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')
|
||||
|
||||
|
||||
@matches_bp.route('/api/events')
|
||||
@login_required
|
||||
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 = []
|
||||
|
||||
# Get tryouts based on user permissions
|
||||
@@ -96,7 +115,14 @@ def api_events():
|
||||
@matches_bp.route('/api/events/<int:tryout_id>')
|
||||
@login_required
|
||||
def api_events_for_tryout(tryout_id):
|
||||
"""API endpoint returning calendar events for a specific tryout."""
|
||||
"""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)
|
||||
|
||||
# Check if user can view this tryout
|
||||
@@ -201,7 +227,18 @@ def api_events_for_tryout(tryout_id):
|
||||
|
||||
|
||||
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':
|
||||
return Tryout.query.order_by(Tryout.date).all()
|
||||
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'])
|
||||
@login_required
|
||||
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)
|
||||
|
||||
# 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'])
|
||||
@login_required
|
||||
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)
|
||||
tryout = match.tryout
|
||||
|
||||
@@ -421,7 +478,14 @@ def edit_match(match_id):
|
||||
@matches_bp.route('/<int:match_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
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)
|
||||
tryout = match.tryout
|
||||
|
||||
@@ -438,12 +502,15 @@ def delete_match(match_id):
|
||||
def get_players_available_at_time(date_str, time_str):
|
||||
"""Get list of player IDs available at a specific date and time.
|
||||
|
||||
Checks player disponibility records to find who is available
|
||||
during the specified time block.
|
||||
|
||||
Args:
|
||||
date_str: Date in YYYY-MM-DD format
|
||||
time_str: Time in HH:MM format
|
||||
date_str: Date in YYYY-MM-DD format.
|
||||
time_str: Time in HH:MM format.
|
||||
|
||||
Returns:
|
||||
List of player IDs who are available at that time
|
||||
list: List of available player IDs.
|
||||
"""
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
@@ -487,7 +554,15 @@ def get_players_available_at_time(date_str, time_str):
|
||||
@matches_bp.route('/api/available_players/<date>/<time>')
|
||||
@login_required
|
||||
def api_available_players(date, time):
|
||||
"""API endpoint to get players available at a specific date/time slot."""
|
||||
"""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():
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
|
||||
@@ -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_login import login_required, current_user
|
||||
from extensions import db
|
||||
@@ -5,9 +10,18 @@ from models import OrgTeam, User
|
||||
|
||||
teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
|
||||
|
||||
|
||||
@teams_bp.route('')
|
||||
@login_required
|
||||
def list_teams():
|
||||
"""List all organization teams visible to the current user.
|
||||
|
||||
Coaches 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()
|
||||
|
||||
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()
|
||||
return render_template('pages/teams.html', teams=teams, coaches=coaches, all_players=all_players, can_manage=can_manage)
|
||||
|
||||
|
||||
@teams_bp.route('/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_team():
|
||||
"""Create a new organization team.
|
||||
|
||||
Args:
|
||||
name: Team name from form.
|
||||
coach_id: Optional coach assignment from form.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to teams list with status message.
|
||||
"""
|
||||
if not current_user.can_manage_teams():
|
||||
flash('You do not have permission to create teams.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
@@ -53,9 +77,20 @@ def create_team():
|
||||
flash(f'Team "{name}" created successfully!', 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
|
||||
@teams_bp.route('/<int:team_id>/edit', methods=['POST'])
|
||||
@login_required
|
||||
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)
|
||||
if not current_user.can_manage_this_org_team(team):
|
||||
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')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
|
||||
@teams_bp.route('/<int:team_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
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():
|
||||
flash('You do not have permission to delete teams.', 'danger')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
@@ -102,9 +149,21 @@ def delete_team(team_id):
|
||||
flash(f'Team "{name}" deleted successfully.', 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
|
||||
@teams_bp.route('/<int:team_id>/add_player', methods=['POST'])
|
||||
@login_required
|
||||
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)
|
||||
if not current_user.can_manage_this_org_team(team):
|
||||
flash('Permission denied.', 'danger')
|
||||
@@ -134,9 +193,19 @@ def add_player(team_id):
|
||||
flash(f'{player.full_name} added to {team.name}!', 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
|
||||
@teams_bp.route('/<int:team_id>/remove_player/<int:player_id>', methods=['POST'])
|
||||
@login_required
|
||||
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)
|
||||
if not current_user.can_manage_this_org_team(team):
|
||||
flash('Permission denied.', 'danger')
|
||||
|
||||
+132
-4
@@ -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_login import login_required, current_user
|
||||
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
|
||||
|
||||
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
|
||||
|
||||
|
||||
def can_manage():
|
||||
"""Check if current user can manage tryouts.
|
||||
|
||||
Returns:
|
||||
bool: True if user is president or manager.
|
||||
"""
|
||||
return current_user.role in ['president', 'manager']
|
||||
|
||||
|
||||
@tryouts_bp.route('')
|
||||
@login_required
|
||||
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':
|
||||
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
|
||||
elif current_user.role == 'manager':
|
||||
@@ -29,9 +52,20 @@ def list_tryouts():
|
||||
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
|
||||
return render_template('pages/tryouts.html', tryouts=tryouts, now=datetime.utcnow())
|
||||
|
||||
|
||||
@tryouts_bp.route('/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create_tryout():
|
||||
"""Create a new tryout event.
|
||||
|
||||
GET: Render the tryout creation form.
|
||||
POST: Create a tryout with the submitted details.
|
||||
|
||||
Requires president or manager role.
|
||||
|
||||
Returns:
|
||||
Response: Create form or redirect to the new tryout.
|
||||
"""
|
||||
if not can_manage():
|
||||
flash('You do not have permission to create tryouts.', 'danger')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
@@ -41,6 +75,7 @@ def create_tryout():
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title')
|
||||
description = request.form.get('description')
|
||||
game = request.form.get('game')
|
||||
date_str = request.form.get('date')
|
||||
location = request.form.get('location')
|
||||
max_players = request.form.get('max_players')
|
||||
@@ -55,6 +90,7 @@ def create_tryout():
|
||||
tryout = Tryout(
|
||||
title=title,
|
||||
description=description,
|
||||
game=game,
|
||||
date=date_obj,
|
||||
location=location,
|
||||
max_players=int(max_players) if max_players else None,
|
||||
@@ -67,11 +103,25 @@ def create_tryout():
|
||||
flash('Tryout created successfully!', 'success')
|
||||
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'])
|
||||
@login_required
|
||||
def edit_tryout(tryout_id):
|
||||
"""Edit an existing tryout event.
|
||||
|
||||
GET: Render the tryout edit form with current data.
|
||||
POST: Update the tryout with submitted changes.
|
||||
|
||||
Permission based on can_manage_this_tryout check.
|
||||
|
||||
Args:
|
||||
tryout_id: The ID of the tryout to edit.
|
||||
|
||||
Returns:
|
||||
Response: Edit form or redirect to tryout view.
|
||||
"""
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
|
||||
# Permission: president, manager (own tryouts), or coach (targets their team)
|
||||
@@ -84,6 +134,7 @@ def edit_tryout(tryout_id):
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title')
|
||||
description = request.form.get('description')
|
||||
game = request.form.get('game')
|
||||
date_str = request.form.get('date')
|
||||
location = request.form.get('location')
|
||||
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()
|
||||
except (ValueError, TypeError):
|
||||
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.description = description
|
||||
tryout.game = game
|
||||
tryout.date = date_obj
|
||||
tryout.location = location
|
||||
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')
|
||||
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>')
|
||||
@login_required
|
||||
def view_tryout(tryout_id):
|
||||
"""View a specific tryout with all details.
|
||||
|
||||
Displays tryout information, registered players, evaluations, teams,
|
||||
matches, and evaluation status information.
|
||||
|
||||
Args:
|
||||
tryout_id: The ID of the tryout to view.
|
||||
|
||||
Returns:
|
||||
Response: Rendered tryout detail template.
|
||||
"""
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
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]
|
||||
@@ -200,11 +264,24 @@ def view_tryout(tryout_id):
|
||||
all_players=all_players,
|
||||
matches=matches,
|
||||
match_data=match_data,
|
||||
game_positions=GAME_POSITIONS,
|
||||
now=datetime.utcnow())
|
||||
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/register', methods=['POST'])
|
||||
@login_required
|
||||
def register_for_tryout(tryout_id):
|
||||
"""Register a player for a tryout.
|
||||
|
||||
Allows players to register for tryouts. Validates that the tryout
|
||||
is accepting registrations and not at capacity.
|
||||
|
||||
Args:
|
||||
tryout_id: The ID of the tryout to register for.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to tryout view with status message.
|
||||
"""
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
if current_user.role != 'player':
|
||||
flash('Only players can register for tryouts.', 'danger')
|
||||
@@ -231,9 +308,20 @@ def register_for_tryout(tryout_id):
|
||||
flash('Successfully registered for tryout!', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/status', methods=['POST'])
|
||||
@login_required
|
||||
def update_status(tryout_id):
|
||||
"""Update the status of a tryout.
|
||||
|
||||
Changes tryout status between upcoming, in_progress, and completed.
|
||||
|
||||
Args:
|
||||
tryout_id: The ID of the tryout to update.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to tryout view.
|
||||
"""
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
if not current_user.can_manage_this_tryout(tryout):
|
||||
flash('Permission denied.', 'danger')
|
||||
@@ -245,9 +333,19 @@ def update_status(tryout_id):
|
||||
flash(f'Tryout status updated to {new_status}.', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/registration/<int:player_id>/status', methods=['POST'])
|
||||
@login_required
|
||||
def update_registration_status(tryout_id, player_id):
|
||||
"""Update the attendance status of a tryout registration.
|
||||
|
||||
Args:
|
||||
tryout_id: The ID of the tryout.
|
||||
player_id: The ID of the player whose status to update.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to tryout view.
|
||||
"""
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
if not current_user.can_manage_this_tryout(tryout):
|
||||
flash('Permission denied.', 'danger')
|
||||
@@ -261,9 +359,20 @@ def update_registration_status(tryout_id, player_id):
|
||||
flash('Registration status updated.', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/register_player', methods=['POST'])
|
||||
@login_required
|
||||
def register_player(tryout_id):
|
||||
"""Manually register a player for a tryout (by managers/coaches).
|
||||
|
||||
Allows authorized users to register players on their behalf.
|
||||
|
||||
Args:
|
||||
tryout_id: The ID of the tryout.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to tryout view with status message.
|
||||
"""
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
if not current_user.can_manage_this_tryout(tryout):
|
||||
flash('Permission denied.', 'danger')
|
||||
@@ -296,9 +405,18 @@ def register_player(tryout_id):
|
||||
flash(f'{player.full_name} registered for tryout!', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/team/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_team(tryout_id):
|
||||
"""Create a tryout-specific team.
|
||||
|
||||
Args:
|
||||
tryout_id: The ID of the tryout to create the team for.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to tryout view with status message.
|
||||
"""
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
if not current_user.can_manage_this_tryout(tryout):
|
||||
flash('Permission denied.', 'danger')
|
||||
@@ -312,9 +430,19 @@ def create_team(tryout_id):
|
||||
flash(f'Team "{team_name}" created!', 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/team/<int:team_id>/add', methods=['POST'])
|
||||
@login_required
|
||||
def add_to_team(tryout_id, team_id):
|
||||
"""Add a player to a tryout team.
|
||||
|
||||
Args:
|
||||
tryout_id: The ID of the tryout.
|
||||
team_id: The ID of the team to add the player to.
|
||||
|
||||
Returns:
|
||||
Response: Redirect to tryout view with status message.
|
||||
"""
|
||||
team = Team.query.get_or_404(team_id)
|
||||
tryout = Tryout.query.get_or_404(tryout_id)
|
||||
if not current_user.can_manage_this_tryout(tryout):
|
||||
|
||||
+201
-61
@@ -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
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file
|
||||
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 datetime import datetime, timedelta
|
||||
|
||||
|
||||
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('')
|
||||
@login_required
|
||||
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':
|
||||
flash('Only the president can manage users.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
@@ -18,9 +67,21 @@ def list_users():
|
||||
users = User.query.order_by(User.role, User.full_name).all()
|
||||
return render_template('pages/users.html', users=users, roles=ROLES)
|
||||
|
||||
|
||||
@users_bp.route('/<int:user_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
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':
|
||||
flash('Only the president can edit users.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
@@ -51,29 +112,8 @@ def edit_user(user_id):
|
||||
user.discord_username = discord_username or None
|
||||
user.league_os_profile = league_os_profile or None
|
||||
|
||||
# Handle gamertags - save or delete based on form input
|
||||
# First, 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])
|
||||
# Update gamertags using shared function
|
||||
update_user_gamertags(user, selected_games)
|
||||
|
||||
password = request.form.get('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}
|
||||
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'])
|
||||
@login_required
|
||||
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':
|
||||
flash('Only the president can delete users.', 'danger')
|
||||
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')
|
||||
return redirect(url_for('users.list_users'))
|
||||
|
||||
|
||||
@users_bp.route('/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
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':
|
||||
flash('Only the president can create users.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
@@ -146,18 +204,35 @@ def create_user():
|
||||
|
||||
return render_template('pages/create_user.html', roles=ROLES)
|
||||
|
||||
|
||||
@users_bp.route('/profile')
|
||||
@login_required
|
||||
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
|
||||
contracts = None
|
||||
if current_user.role == 'player':
|
||||
contracts = Contract.query.filter_by(player_id=current_user.id).order_by(Contract.uploaded_at.desc()).all()
|
||||
return render_template('pages/profile.html', user=current_user, contracts=contracts)
|
||||
|
||||
|
||||
@users_bp.route('/profile/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_profile():
|
||||
"""Edit the current user's profile.
|
||||
|
||||
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':
|
||||
full_name = request.form.get('full_name')
|
||||
email = request.form.get('email')
|
||||
@@ -178,29 +253,8 @@ def edit_profile():
|
||||
current_user.discord_username = discord_username or None
|
||||
current_user.league_os_profile = league_os_profile or None
|
||||
|
||||
# Handle gamertags - save or delete based on form input
|
||||
# First, get all existing gamertags for this user
|
||||
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])
|
||||
# Update gamertags using shared function
|
||||
update_user_gamertags(current_user, selected_games)
|
||||
|
||||
password = request.form.get('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)
|
||||
|
||||
|
||||
# Day names for disponibility
|
||||
# Day names for disponibility display
|
||||
DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@users_bp.route('/disponibilities')
|
||||
@login_required
|
||||
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
|
||||
if not current_user.can_manage_teams() and not current_user.can_schedule_matches():
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
@@ -256,7 +323,11 @@ def get_disponibilities():
|
||||
@users_bp.route('/disponibilities/my')
|
||||
@login_required
|
||||
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()
|
||||
|
||||
# Group by day for easier display
|
||||
@@ -279,7 +350,11 @@ def get_my_disponibilities():
|
||||
@users_bp.route('/disponibilities/add', methods=['POST'])
|
||||
@login_required
|
||||
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)
|
||||
start_time_str = request.form.get('start_time')
|
||||
|
||||
@@ -315,7 +390,14 @@ def add_disponibility():
|
||||
@users_bp.route('/disponibilities/add_bulk', methods=['POST'])
|
||||
@login_required
|
||||
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()
|
||||
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'])
|
||||
@login_required
|
||||
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()
|
||||
db.session.commit()
|
||||
return jsonify({'success': True})
|
||||
@@ -374,7 +460,14 @@ def clear_disponibilities():
|
||||
@users_bp.route('/disponibilities/<int:disponibility_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
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)
|
||||
|
||||
# Only the owner can delete their disponibility
|
||||
@@ -387,10 +480,22 @@ def delete_disponibility(disponibility_id):
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Contract Dropbox Functions
|
||||
# Contract Management Functions
|
||||
|
||||
|
||||
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
|
||||
if user.role == 'president':
|
||||
return True
|
||||
@@ -412,7 +517,14 @@ def can_manage_player_contract(user, player_id):
|
||||
@users_bp.route('/contracts')
|
||||
@login_required
|
||||
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
|
||||
|
||||
if current_user.role == 'player':
|
||||
@@ -438,7 +550,14 @@ def list_contracts():
|
||||
@users_bp.route('/contracts/upload', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
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']:
|
||||
flash('Only presidents, managers, and coaches can upload contracts.', 'danger')
|
||||
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'])
|
||||
@login_required
|
||||
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)
|
||||
|
||||
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')
|
||||
@login_required
|
||||
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)
|
||||
|
||||
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')
|
||||
@login_required
|
||||
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)
|
||||
|
||||
if not contract.can_view(current_user):
|
||||
|
||||
@@ -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 extensions import db, hash_password
|
||||
from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, PlayerDisponibility, UserGamertag
|
||||
@@ -6,6 +12,20 @@ import random
|
||||
|
||||
|
||||
def seed_database():
|
||||
"""Seed the database with sample data for development and testing.
|
||||
|
||||
Creates sample users with different roles (president, manager, coach, player, scout),
|
||||
organization teams, tryouts, registrations, evaluations, and player disponibilities.
|
||||
All existing data is cleared before seeding.
|
||||
|
||||
The function creates:
|
||||
- 7 admin users (president, 2 managers, 3 coaches, 1 scout)
|
||||
- 10 player users with E-Sports profiles
|
||||
- 4 organization teams
|
||||
- 4 tryouts
|
||||
- Registrations and evaluations
|
||||
- Player disponibilities for match scheduling
|
||||
"""
|
||||
# Clear existing data
|
||||
db.session.execute(text('DELETE FROM player_disponibilities'))
|
||||
db.session.execute(text('DELETE FROM match_participants'))
|
||||
@@ -33,9 +53,9 @@ def seed_database():
|
||||
|
||||
# Create players with E-Sports profile data (gamertags per game)
|
||||
player_data = [
|
||||
{'username': 'jplayer1', 'full_name': 'James Wilson', 'email': 'james@email.com', 'games': 'Valorant,Counter-Strike 2',
|
||||
'gamertags': {'Valorant': 'jameswilson_val', 'Counter-Strike 2': 'jameswilson_cs'},
|
||||
'discord': 'JamesW#7291', 'league_os': 'https://leagueos.gg/player/jameswilson'},
|
||||
{'username': 'jplayer1', 'full_name': 'nordjan', 'email': 'nordjan27@gmail.com', 'games': 'Valorant,Counter-Strike 2, Rainbow Six Siege, Rocket League, Overwatch 2',
|
||||
'gamertags': {'Valorant': 'nordjan#bad', 'Counter-Strike 2': 'nordjan', 'Rainbow Six Siege': 'n0rd-vpn', 'Rocket League': 'nordjiano'},
|
||||
'discord': 'nordjan', 'league_os': 'https://leagueos.gg/player/nordjan'},
|
||||
{'username': 'jplayer2', 'full_name': 'Emma Garcia', 'email': '[email protected]', 'games': 'League of Legends,Valorant',
|
||||
'gamertags': {'League of Legends': 'emmagarcia_lol', 'Valorant': 'emmagarcia_val'},
|
||||
'discord': 'EmmaG#4452', 'league_os': 'https://leagueos.gg/player/emmagarcia'},
|
||||
@@ -139,10 +159,10 @@ def seed_database():
|
||||
|
||||
# Create Organization Teams (OrgTeams)
|
||||
org_teams_data = [
|
||||
{'name': 'Varsity', 'coach': coaches[0], 'creator': president},
|
||||
{'name': 'Junior Varsity', 'coach': coaches[1], 'creator': president},
|
||||
{'name': 'U14 Development', 'coach': coaches[2], 'creator': president},
|
||||
{'name': 'Select Team', 'coach': None, 'creator': president},
|
||||
{'name': 'Rocket League main', 'coach': coaches[0], 'creator': president},
|
||||
{'name': 'CS2', 'coach': coaches[1], 'creator': president},
|
||||
{'name': 'Valorant', 'coach': coaches[2], 'creator': president},
|
||||
{'name': 'Rocket League acad', 'coach': None, 'creator': manager1},
|
||||
]
|
||||
|
||||
org_teams = []
|
||||
@@ -159,16 +179,16 @@ def seed_database():
|
||||
|
||||
# Create tryouts
|
||||
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': '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': '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': '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]},
|
||||
{'title': 'Rocket Leauge Tryouts', 'game': 'Rocket League', 'date': datetime.utcnow(), 'location': 'En ligne', 'description': 'Tryouts for the spring competitive season. All positions welcome.', 'status': 'in_progress', 'creator': manager1, 'target_team': org_teams[0]},
|
||||
{'title': 'CS2 Tryouts', 'game': 'Counter-Strike 2', 'date': datetime.utcnow() + timedelta(days=4), 'location': 'En ligne', 'description': 'Trials for the fall select team. High skill level required.', 'status': 'upcoming', 'creator': manager1, 'target_team': org_teams[3]},
|
||||
{'title': 'Valorant Tryouts', 'game': 'Valorant', 'date': datetime.utcnow() - timedelta(days=2), 'location': 'En ligne', 'description': 'Séléction pour l\'équipe de Valorant', 'status': 'in_progress', 'creator': manager2, 'target_team': org_teams[2]},
|
||||
]
|
||||
|
||||
tryouts = []
|
||||
for data in tryouts_data:
|
||||
t = Tryout(
|
||||
title=data['title'],
|
||||
game=data['game'],
|
||||
date=data['date'].date(),
|
||||
location=data['location'],
|
||||
description=data['description'],
|
||||
@@ -184,10 +204,9 @@ def seed_database():
|
||||
|
||||
# Register players for tryouts
|
||||
registrations_data = [
|
||||
(tryouts[0], players[:8]),
|
||||
(tryouts[1], players),
|
||||
(tryouts[2], players[:6]),
|
||||
(tryouts[3], players[2:9]),
|
||||
(tryouts[0], players[:3]),
|
||||
(tryouts[1], players[3:5]),
|
||||
(tryouts[2], players)
|
||||
]
|
||||
|
||||
regs = []
|
||||
@@ -196,7 +215,7 @@ def seed_database():
|
||||
reg = TryoutRegistration(
|
||||
tryout_id=tryout.id,
|
||||
player_id=player.id,
|
||||
status=random.choice(['registered', 'attended', 'attended'])
|
||||
status=random.choice(['registered', 'attended', 'attended', 'no_show'])
|
||||
)
|
||||
db.session.add(reg)
|
||||
regs.append(reg)
|
||||
@@ -205,55 +224,71 @@ def seed_database():
|
||||
|
||||
# Create evaluations (for in_progress and completed tryouts)
|
||||
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 coach in coaches:
|
||||
if random.random() > 0.3:
|
||||
speed = random.randint(4, 10)
|
||||
agility = random.randint(4, 10)
|
||||
technique = random.randint(3, 10)
|
||||
teamwork = random.randint(5, 10)
|
||||
attitude = random.randint(5, 10)
|
||||
overall = round((speed + agility + technique + teamwork + attitude) / 5, 1)
|
||||
positions = ['Forward', 'Midfield', 'Defense', 'Goalie', 'Wing', 'Center']
|
||||
mecanics = random.randint(4, 10)
|
||||
cohesion = random.randint(4, 10)
|
||||
communication = random.randint(3, 10)
|
||||
gamesense = random.randint(5, 10)
|
||||
versatility = random.randint(5, 10)
|
||||
discipline = random.randint(4, 10)
|
||||
analysis = random.randint(4, 10)
|
||||
sport_ethics = random.randint(5, 10)
|
||||
mental = random.randint(5, 10)
|
||||
overall = round((mecanics + cohesion + communication + gamesense + versatility + discipline + analysis + sport_ethics + mental) / 9, 1)
|
||||
eval_entry = Evaluation(
|
||||
tryout_id=tryouts[0].id,
|
||||
player_id=player.id,
|
||||
evaluator_id=coach.id,
|
||||
speed_score=speed,
|
||||
agility_score=agility,
|
||||
technique_score=technique,
|
||||
teamwork_score=teamwork,
|
||||
attitude_score=attitude,
|
||||
mecanics_score=mecanics,
|
||||
cohesion_score=cohesion,
|
||||
communication_score=communication,
|
||||
gamesense_score=gamesense,
|
||||
versatility_score=versatility,
|
||||
discipline_score=discipline,
|
||||
analysis_score=analysis,
|
||||
sport_ethics_score=sport_ethics,
|
||||
mental_score=mental,
|
||||
overall_score=overall,
|
||||
comments=f"{'Great' if overall > 7 else 'Good'} performance. {'Shows promise.' if overall > 6 else 'Needs improvement in some areas.'}",
|
||||
position_recommendation=random.choice(positions)
|
||||
position_recommendation=random.choice(rl_positions)
|
||||
)
|
||||
db.session.add(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 coach in coaches[:2]:
|
||||
speed = random.randint(3, 10)
|
||||
agility = random.randint(3, 10)
|
||||
technique = random.randint(3, 10)
|
||||
teamwork = random.randint(4, 10)
|
||||
attitude = random.randint(4, 10)
|
||||
overall = round((speed + agility + technique + teamwork + attitude) / 5, 1)
|
||||
positions = ['Forward', 'Midfield', 'Defense', 'Goalie', 'Wing', 'Center']
|
||||
mecanics = random.randint(3, 10)
|
||||
cohesion = random.randint(3, 10)
|
||||
communication = random.randint(3, 10)
|
||||
gamesense = random.randint(4, 10)
|
||||
versatility = random.randint(4, 10)
|
||||
discipline = random.randint(3, 10)
|
||||
analysis = random.randint(3, 10)
|
||||
sport_ethics = random.randint(4, 10)
|
||||
mental = random.randint(4, 10)
|
||||
overall = round((mecanics + cohesion + communication + gamesense + versatility + discipline + analysis + sport_ethics + mental) / 9, 1)
|
||||
eval_entry = Evaluation(
|
||||
tryout_id=tryouts[2].id,
|
||||
player_id=player.id,
|
||||
evaluator_id=coach.id,
|
||||
speed_score=speed,
|
||||
agility_score=agility,
|
||||
technique_score=technique,
|
||||
teamwork_score=teamwork,
|
||||
attitude_score=attitude,
|
||||
mecanics_score=mecanics,
|
||||
cohesion_score=cohesion,
|
||||
communication_score=communication,
|
||||
gamesense_score=gamesense,
|
||||
versatility_score=versatility,
|
||||
discipline_score=discipline,
|
||||
analysis_score=analysis,
|
||||
sport_ethics_score=sport_ethics,
|
||||
mental_score=mental,
|
||||
overall_score=overall,
|
||||
comments=f"{'Excellent' if overall > 8 else 'Solid'} display of skills during the camp.",
|
||||
position_recommendation=random.choice(positions)
|
||||
position_recommendation=random.choice(val_positions)
|
||||
)
|
||||
db.session.add(eval_entry)
|
||||
eval_data.append(eval_entry)
|
||||
@@ -268,14 +303,15 @@ def seed_database():
|
||||
db.session.add(team2)
|
||||
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 = [
|
||||
(team1.id, players[0].id, 'Forward'),
|
||||
(team1.id, players[1].id, 'Midfield'),
|
||||
(team1.id, players[2].id, 'Defense'),
|
||||
(team1.id, players[3].id, 'Goalie'),
|
||||
(team2.id, players[4].id, 'Midfield'),
|
||||
(team2.id, players[5].id, 'Forward'),
|
||||
(team1.id, players[0].id, random.choice(val_positions)),
|
||||
(team1.id, players[1].id, random.choice(val_positions)),
|
||||
(team1.id, players[2].id, random.choice(val_positions)),
|
||||
(team1.id, players[3].id, random.choice(val_positions)),
|
||||
(team2.id, players[4].id, random.choice(val_positions)),
|
||||
(team2.id, players[5].id, random.choice(val_positions)),
|
||||
]
|
||||
for team_id, player_id, position in team_members_data:
|
||||
tm = TeamMember(team_id=team_id, player_id=player_id, position=position)
|
||||
@@ -340,6 +376,7 @@ def seed_database():
|
||||
print("Player: username='jplayer1', password='password'")
|
||||
print("Scout: username='scout1', password='password'")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from app import create_app
|
||||
app = create_app()
|
||||
|
||||
+91
-8
@@ -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() {
|
||||
const body = document.documentElement;
|
||||
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() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
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() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
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) {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
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) {
|
||||
el.addEventListener('click', function(e) {
|
||||
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() {
|
||||
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) {
|
||||
let draggedElement = null;
|
||||
let placeholder = null;
|
||||
@@ -147,6 +191,11 @@ function makeHeaderDraggable(header, card, grid) {
|
||||
ghost.style.top = (e.clientY - offsetY) + '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) {
|
||||
// Position ghost directly at cursor position
|
||||
ghost.style.top = (clientY - offsetY) + 'px';
|
||||
@@ -158,7 +207,11 @@ function makeHeaderDraggable(header, card, grid) {
|
||||
const gridStyle = window.getComputedStyle(grid);
|
||||
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) {
|
||||
// Calculate which column the x position falls into
|
||||
const relativeX = x - gridRect.left;
|
||||
@@ -166,6 +219,10 @@ function makeHeaderDraggable(header, card, grid) {
|
||||
return Math.floor(relativeX / (colWidth + gridGap));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse movement during drag.
|
||||
* @param {MouseEvent} e - Mouse event
|
||||
*/
|
||||
function onMouseMove(e) {
|
||||
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) {
|
||||
if (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) {
|
||||
const pageKey = getPageKey();
|
||||
const cards = grid.querySelectorAll('.card');
|
||||
@@ -298,6 +364,11 @@ function saveLayout(grid) {
|
||||
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) {
|
||||
const pageKey = getPageKey();
|
||||
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() {
|
||||
const path = window.location.pathname;
|
||||
if (path.includes('/tryouts/')) {
|
||||
@@ -331,7 +407,11 @@ function getPageKey() {
|
||||
return 'dashboard';
|
||||
}
|
||||
|
||||
// Initialize draggable blocks on DOM ready
|
||||
/**
|
||||
* Initialize on DOM ready.
|
||||
*
|
||||
* Loads theme preference and initializes draggable blocks.
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load theme preference
|
||||
loadTheme();
|
||||
@@ -339,6 +419,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize draggable blocks
|
||||
initDraggableBlocks();
|
||||
|
||||
/**
|
||||
* Auto-dismiss flash alerts after 5 seconds.
|
||||
*/
|
||||
const alerts = document.querySelectorAll('.alert-dismissible');
|
||||
alerts.forEach(function(alert) {
|
||||
setTimeout(function() {
|
||||
|
||||
@@ -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 }}')">×</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 %}
|
||||
@@ -16,19 +16,30 @@
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="date">Date</label>
|
||||
<input type="date" id="date" name="date" required>
|
||||
<label for="game">Game</label>
|
||||
<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 class="form-group col-6">
|
||||
<label for="location">Location</label>
|
||||
<input type="text" id="location" name="location" placeholder="e.g., Main Field">
|
||||
<label for="date">Date</label>
|
||||
<input type="date" id="date" name="date" required>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<label for="max_players">Max Players</label>
|
||||
<input type="number" id="max_players" name="max_players" placeholder="Leave blank for unlimited" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="target_org_team_id">Target Team</label>
|
||||
<select id="target_org_team_id" name="target_org_team_id" class="form-select">
|
||||
|
||||
@@ -16,19 +16,30 @@
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="date">Date</label>
|
||||
<input type="date" id="date" name="date" value="{{ tryout.date.strftime('%Y-%m-%d') }}" required>
|
||||
<label for="game">Game</label>
|
||||
<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 class="form-group col-6">
|
||||
<label for="location">Location</label>
|
||||
<input type="text" id="location" name="location" value="{{ tryout.location or '' }}" placeholder="e.g., Main Field">
|
||||
<label for="date">Date</label>
|
||||
<input type="date" id="date" name="date" value="{{ tryout.date.strftime('%Y-%m-%d') }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-6">
|
||||
<label for="target_org_team_id">Target Team</label>
|
||||
<select id="target_org_team_id" name="target_org_team_id" class="form-select">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<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 class="card-body">
|
||||
<div class="eval-player-info mb-4">
|
||||
@@ -30,46 +30,90 @@
|
||||
|
||||
<div class="form-row">
|
||||
<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">
|
||||
<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">
|
||||
<span class="range-value">{{ existing_eval.speed_score or 5 }}</span>
|
||||
<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.mecanics_score or 5 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<span class="range-value">{{ existing_eval.agility_score or 5 }}</span>
|
||||
<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.cohesion_score or 5 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<span class="range-value">{{ existing_eval.technique_score or 5 }}</span>
|
||||
<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.communication_score or 5 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<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">
|
||||
<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">
|
||||
<span class="range-value">{{ existing_eval.teamwork_score or 5 }}</span>
|
||||
<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.gamesense_score or 5 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<span class="range-value">{{ existing_eval.attitude_score or 5 }}</span>
|
||||
<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.versatility_score or 5 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -96,17 +140,34 @@
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<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>
|
||||
<tbody>
|
||||
{% for entry in evaluators %}
|
||||
<tr>
|
||||
<td>{{ entry.evaluator.full_name }}</td>
|
||||
<td>{{ entry.eval.speed_score or '-' }}</td>
|
||||
<td>{{ entry.eval.agility_score or '-' }}</td>
|
||||
<td>{{ entry.eval.technique_score or '-' }}</td>
|
||||
<td>{{ entry.eval.teamwork_score or '-' }}</td>
|
||||
<td>{{ entry.eval.attitude_score or '-' }}</td>
|
||||
<td>{{ entry.eval.mecanics_score or '-' }}</td>
|
||||
<td>{{ entry.eval.cohesion_score or '-' }}</td>
|
||||
<td>{{ entry.eval.communication_score or '-' }}</td>
|
||||
<td>{{ entry.eval.gamesense_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>{{ entry.eval.position_recommendation or '-' }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -38,11 +38,15 @@
|
||||
<th>Tryout</th>
|
||||
<th>Player</th>
|
||||
<th>Evaluator</th>
|
||||
<th>Speed</th>
|
||||
<th>Agility</th>
|
||||
<th>Technique</th>
|
||||
<th>Teamwork</th>
|
||||
<th>Attitude</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>
|
||||
<th>Date</th>
|
||||
@@ -54,18 +58,22 @@
|
||||
<td>{{ eval.tryout.title }}</td>
|
||||
<td>{{ eval.player.full_name }}</td>
|
||||
<td>{{ eval.evaluator.full_name }}</td>
|
||||
<td>{{ eval.speed_score or '-' }}</td>
|
||||
<td>{{ eval.agility_score or '-' }}</td>
|
||||
<td>{{ eval.technique_score or '-' }}</td>
|
||||
<td>{{ eval.teamwork_score or '-' }}</td>
|
||||
<td>{{ eval.attitude_score or '-' }}</td>
|
||||
<td>{{ eval.mecanics_score or '-' }}</td>
|
||||
<td>{{ eval.cohesion_score or '-' }}</td>
|
||||
<td>{{ eval.communication_score or '-' }}</td>
|
||||
<td>{{ eval.gamesense_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="badge badge-info">{{ eval.position_recommendation or 'N/A' }}</span></td>
|
||||
<td>{{ eval.created_at.strftime('%m/%d/%Y') }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="11" class="text-center">
|
||||
<td colspan="15" class="text-center">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-clipboard"></i>
|
||||
<h3>No evaluations yet</h3>
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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">
|
||||
<span class="detail-label">Date</span>
|
||||
<span class="detail-value">{{ tryout.date.strftime('%A, %B %d, %Y') }}</span>
|
||||
@@ -209,6 +213,7 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% 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">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<select name="player_id" class="form-select" required>
|
||||
@@ -219,7 +224,16 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</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">
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-sm btn-primary">Add</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
@@ -378,11 +392,15 @@
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
<th>Evaluator</th>
|
||||
<th>Speed</th>
|
||||
<th>Agility</th>
|
||||
<th>Technique</th>
|
||||
<th>Teamwork</th>
|
||||
<th>Attitude</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>Recommendation</th>
|
||||
</tr>
|
||||
@@ -392,17 +410,21 @@
|
||||
<tr>
|
||||
<td>{{ eval.player.full_name }}</td>
|
||||
<td>{{ eval.evaluator.full_name }}</td>
|
||||
<td>{{ eval.speed_score or '-' }}</td>
|
||||
<td>{{ eval.agility_score or '-' }}</td>
|
||||
<td>{{ eval.technique_score or '-' }}</td>
|
||||
<td>{{ eval.teamwork_score or '-' }}</td>
|
||||
<td>{{ eval.attitude_score or '-' }}</td>
|
||||
<td>{{ eval.mecanics_score or '-' }}</td>
|
||||
<td>{{ eval.cohesion_score or '-' }}</td>
|
||||
<td>{{ eval.communication_score or '-' }}</td>
|
||||
<td>{{ eval.gamesense_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>{{ eval.position_recommendation or '-' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center">No evaluations yet.</td>
|
||||
<td colspan="13" class="text-center">No evaluations yet.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user