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

This commit is contained in:
cedrick2711
2026-07-15 21:27:18 -04:00
parent a129f218b6
commit 195f3ce312
29 changed files with 1501 additions and 269 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+37
View File
@@ -1,3 +1,8 @@
"""Authentication routes for user login, logout, and registration.
This module handles user authentication including login, logout, and new user registration.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_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
View File
@@ -1,4 +1,9 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
"""Evaluation routes for assessing player performance during tryouts.
This module handles player evaluation creation, management, and viewing.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from 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
View File
@@ -1,3 +1,8 @@
"""Main dashboard routes for the Team Tryouts application.
This module provides the main dashboard view with role-specific statistics.
"""
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_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()
+88 -13
View File
@@ -1,3 +1,8 @@
"""Match scheduling routes for managing scrimmages and matches within tryouts.
This module handles calendar views, match creation, and player availability.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_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,9 +554,17 @@ 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
player_ids = get_players_available_at_time(date, time)
return jsonify({'available_player_ids': player_ids})
return jsonify({'available_player_ids': player_ids})
+70 -1
View File
@@ -1,3 +1,8 @@
"""Organization team management routes.
This module handles CRUD operations for organization teams and player assignments.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_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')
@@ -151,4 +220,4 @@ def remove_player(team_id, player_id):
player.team_id = None
db.session.commit()
flash(f'{player.full_name} removed from {team.name}.', 'success')
return redirect(url_for('teams.list_teams'))
return redirect(url_for('teams.list_teams'))
+133 -5
View File
@@ -1,17 +1,40 @@
"""Tryout management routes for creating, viewing, and managing tryout events.
This module handles CRUD operations for tryouts and player registrations.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_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]
@@ -156,7 +220,7 @@ def view_tryout(tryout_id):
).first() is not None
can_view_calendar = is_registered or player_in_match
# Get all players (for manager registration dropdown)
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
@@ -200,11 +264,24 @@ def view_tryout(tryout_id):
all_players=all_players,
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):
+202 -62
View File
@@ -1,3 +1,9 @@
"""User management routes for profiles, disponibilities, and contracts.
This module handles user CRUD operations, profile editing, player availability,
and contract management.
"""
import os
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):
@@ -576,4 +716,4 @@ def download_signed_contract(contract_id):
flash('No signed contract available.', 'danger')
return redirect(url_for('users.list_contracts'))
return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename)
return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename)