Ajout du statut remplaçant pour les joueurs Possibilité d'ajouter/enlever coach et manager des équipes les joueurs peuvent être associés à plusieurs équipes au lieu d'une seule
1537 lines
57 KiB
Python
1537 lines
57 KiB
Python
"""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
|
|
from extensions import db, hash_password
|
|
from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam, CoachAvailability, TeamNote, PersonalNote, OneOnOneRequest, Match, Team, TeamMember, MatchParticipant, Tryout, TryoutRegistration, TeamPlayer
|
|
from werkzeug.utils import secure_filename
|
|
from datetime import datetime, timedelta, date as date_type
|
|
import requests
|
|
|
|
|
|
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'))
|
|
|
|
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'))
|
|
|
|
user = User.query.get_or_404(user_id)
|
|
|
|
if request.method == 'POST':
|
|
full_name = request.form.get('full_name')
|
|
email = request.form.get('email')
|
|
phone = request.form.get('phone')
|
|
role = request.form.get('role')
|
|
is_active = request.form.get('is_active_account') == 'on'
|
|
|
|
if role not in ROLES:
|
|
flash('Invalid role selected.', 'danger')
|
|
return render_template('pages/edit_user.html', user=user, roles=ROLES, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS)
|
|
|
|
selected_games = request.form.getlist('games')
|
|
discord_username = request.form.get('discord_username', '').strip()
|
|
discord_user_id = request.form.get('discord_user_id', '').strip()
|
|
league_os_profile = request.form.get('league_os_profile', '').strip()
|
|
|
|
user.full_name = full_name
|
|
user.email = email
|
|
user.phone = phone
|
|
user.role = role
|
|
user.is_active_account = is_active
|
|
user.games = ','.join(selected_games) if selected_games else None
|
|
user.discord_username = discord_username or None
|
|
user.discord_user_id = discord_user_id or None
|
|
user.league_os_profile = league_os_profile or None
|
|
|
|
# Update gamertags using shared function
|
|
update_user_gamertags(user, selected_games)
|
|
|
|
password = request.form.get('password')
|
|
if password:
|
|
user.password_hash = hash_password(password)
|
|
|
|
db.session.commit()
|
|
flash(f'User {user.full_name} updated successfully!', 'success')
|
|
return redirect(url_for('users.list_users'))
|
|
|
|
user_gamertags = {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in user.gamertags}
|
|
return render_template('pages/edit_user.html', user=user, roles=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'))
|
|
|
|
if current_user.id == user_id:
|
|
flash('You cannot delete your own account.', 'danger')
|
|
return redirect(url_for('users.list_users'))
|
|
|
|
user = User.query.get_or_404(user_id)
|
|
db.session.delete(user)
|
|
db.session.commit()
|
|
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'))
|
|
|
|
if request.method == 'POST':
|
|
username = request.form.get('username')
|
|
email = request.form.get('email')
|
|
password = request.form.get('password')
|
|
full_name = request.form.get('full_name')
|
|
phone = request.form.get('phone')
|
|
role = request.form.get('role')
|
|
|
|
if role not in ROLES:
|
|
flash('Invalid role selected.', 'danger')
|
|
return render_template('pages/create_user.html', roles=ROLES)
|
|
|
|
if User.query.filter_by(username=username).first():
|
|
flash('Username already exists.', 'danger')
|
|
return render_template('pages/create_user.html', roles=ROLES)
|
|
|
|
if User.query.filter_by(email=email).first():
|
|
flash('Email already registered.', 'danger')
|
|
return render_template('pages/create_user.html', roles=ROLES)
|
|
|
|
hashed_password = hash_password(password)
|
|
user = User(
|
|
username=username,
|
|
password_hash=hashed_password,
|
|
role=role,
|
|
full_name=full_name,
|
|
email=email,
|
|
phone=phone
|
|
)
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
flash(f'User {full_name} created as {role}!', 'success')
|
|
return redirect(url_for('users.list_users'))
|
|
|
|
return render_template('pages/create_user.html', roles=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')
|
|
phone = request.form.get('phone')
|
|
|
|
selected_games = request.form.getlist('games')
|
|
discord_username = request.form.get('discord_username', '').strip()
|
|
discord_user_id = request.form.get('discord_user_id', '').strip()
|
|
league_os_profile = request.form.get('league_os_profile', '').strip()
|
|
|
|
if email != current_user.email and User.query.filter_by(email=email).first():
|
|
flash('Email already in use.', 'danger')
|
|
return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS)
|
|
|
|
current_user.full_name = full_name
|
|
current_user.email = email
|
|
current_user.phone = phone
|
|
current_user.games = ','.join(selected_games) if selected_games else None
|
|
current_user.discord_username = discord_username or None
|
|
current_user.discord_user_id = discord_user_id or None
|
|
current_user.league_os_profile = league_os_profile or None
|
|
|
|
# Update gamertags using shared function
|
|
update_user_gamertags(current_user, selected_games)
|
|
|
|
password = request.form.get('password')
|
|
if password:
|
|
current_user.password_hash = hash_password(password)
|
|
|
|
db.session.commit()
|
|
flash('Profile updated successfully!', 'success')
|
|
return redirect(url_for('users.profile'))
|
|
|
|
user_gamertags = {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in current_user.gamertags}
|
|
return render_template('pages/edit_profile.html', user=current_user, esport_games=ESPORT_GAMES, game_platforms=GAME_PLATFORMS, user_gamertags=user_gamertags)
|
|
|
|
|
|
# Day names for disponibility display
|
|
DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
|
|
|
|
|
def add_30_minutes(t):
|
|
"""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.
|
|
|
|
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
|
|
|
|
players = User.query.filter_by(role='player', is_active_account=True).order_by(User.full_name).all()
|
|
result = {}
|
|
|
|
for player in players:
|
|
disponibilities = list(player.disponibilities)
|
|
result[player.id] = {
|
|
'full_name': player.full_name,
|
|
'disponibilities': [
|
|
{
|
|
'id': d.id,
|
|
'day_of_week': d.day_of_week,
|
|
'day_name': DAY_NAMES[d.day_of_week],
|
|
'start_time': d.start_time.strftime('%H:%M'),
|
|
'end_time': d.end_time.strftime('%H:%M')
|
|
}
|
|
for d in disponibilities
|
|
]
|
|
}
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
@users_bp.route('/disponibilities/my')
|
|
@login_required
|
|
def get_my_disponibilities():
|
|
"""API endpoint for players to get their own disponibilities.
|
|
|
|
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
|
|
result = {}
|
|
for d in disponibilities:
|
|
day = d.day_of_week
|
|
if day not in result:
|
|
result[day] = []
|
|
result[day].append({
|
|
'id': d.id,
|
|
'day_of_week': d.day_of_week,
|
|
'day_name': DAY_NAMES[d.day_of_week],
|
|
'start_time': d.start_time.strftime('%H:%M'),
|
|
'end_time': d.end_time.strftime('%H:%M')
|
|
})
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
@users_bp.route('/disponibilities/add', methods=['POST'])
|
|
@login_required
|
|
def add_disponibility():
|
|
"""Add a disponibility block for the current player.
|
|
|
|
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')
|
|
|
|
if day_of_week is None or day_of_week < 0 or day_of_week > 6:
|
|
return jsonify({'error': 'Invalid day of week'}), 400
|
|
|
|
try:
|
|
start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
|
except (ValueError, TypeError):
|
|
return jsonify({'error': 'Invalid time format'}), 400
|
|
|
|
end_time = add_30_minutes(start_time)
|
|
|
|
disponibility = PlayerDisponibility(
|
|
player_id=current_user.id,
|
|
day_of_week=day_of_week,
|
|
start_time=start_time,
|
|
end_time=end_time
|
|
)
|
|
|
|
db.session.add(disponibility)
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'id': disponibility.id,
|
|
'day_of_week': disponibility.day_of_week,
|
|
'day_name': DAY_NAMES[disponibility.day_of_week],
|
|
'start_time': disponibility.start_time.strftime('%H:%M'),
|
|
'end_time': disponibility.end_time.strftime('%H:%M')
|
|
})
|
|
|
|
|
|
@users_bp.route('/disponibilities/add_bulk', methods=['POST'])
|
|
@login_required
|
|
def add_disponibilities_bulk():
|
|
"""Add multiple disponibility blocks at once (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}
|
|
|
|
created = []
|
|
for slot in slots:
|
|
day_of_week = slot.get('day_of_week')
|
|
start_time_str = slot.get('start_time')
|
|
|
|
if day_of_week is None or day_of_week < 0 or day_of_week > 6:
|
|
continue
|
|
|
|
try:
|
|
start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
|
except (ValueError, TypeError):
|
|
continue
|
|
|
|
end_time = add_30_minutes(start_time)
|
|
|
|
# Check if this slot already exists for this player
|
|
existing = PlayerDisponibility.query.filter_by(
|
|
player_id=current_user.id,
|
|
day_of_week=day_of_week,
|
|
start_time=start_time
|
|
).first()
|
|
|
|
if not existing:
|
|
disponibility = PlayerDisponibility(
|
|
player_id=current_user.id,
|
|
day_of_week=day_of_week,
|
|
start_time=start_time,
|
|
end_time=end_time
|
|
)
|
|
db.session.add(disponibility)
|
|
db.session.flush()
|
|
created.append({
|
|
'id': disponibility.id,
|
|
'day_of_week': disponibility.day_of_week,
|
|
'day_name': DAY_NAMES[disponibility.day_of_week],
|
|
'start_time': disponibility.start_time.strftime('%H:%M'),
|
|
'end_time': disponibility.start_time.strftime('%H:%M')
|
|
})
|
|
|
|
db.session.commit()
|
|
return jsonify({'success': True, 'created': created})
|
|
|
|
|
|
@users_bp.route('/disponibilities/clear', methods=['POST'])
|
|
@login_required
|
|
def clear_disponibilities():
|
|
"""Clear all disponibilities for the current player (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})
|
|
|
|
|
|
@users_bp.route('/disponibilities/<int:disponibility_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def delete_disponibility(disponibility_id):
|
|
"""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
|
|
if disponibility.player_id != current_user.id:
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
db.session.delete(disponibility)
|
|
db.session.commit()
|
|
|
|
return jsonify({'success': True})
|
|
|
|
|
|
# Contract Management Functions
|
|
|
|
|
|
def can_manage_player_contract(user, player_id):
|
|
"""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
|
|
|
|
# Manager can upload contracts for any player
|
|
if user.role == 'manager':
|
|
return True
|
|
|
|
# Coach can upload contracts only for players on their team
|
|
if user.role == 'coach':
|
|
org_team = OrgTeam.query.filter_by(coach_id=user.id).first()
|
|
if org_team:
|
|
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=org_team.id).first()
|
|
if tp:
|
|
return True
|
|
return False
|
|
|
|
|
|
@users_bp.route('/contracts')
|
|
@login_required
|
|
def list_contracts():
|
|
"""View contracts for the current user (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':
|
|
# Players see their own contracts
|
|
contracts = Contract.query.filter_by(player_id=current_user.id).order_by(Contract.uploaded_at.desc()).all()
|
|
elif current_user.role in ['president', 'manager', 'coach']:
|
|
# Superiors see contracts for players on their teams
|
|
players = []
|
|
if current_user.role == 'coach':
|
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
|
if org_team:
|
|
player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
|
|
players = User.query.filter(User.id.in_(player_ids)).all() if player_ids else []
|
|
else: # president or manager
|
|
players = User.query.filter_by(role='player').all()
|
|
|
|
if players:
|
|
player_ids = [p.id for p in players]
|
|
contracts = Contract.query.filter(Contract.player_id.in_(player_ids)).order_by(Contract.uploaded_at.desc()).all()
|
|
|
|
return render_template('pages/contracts.html', contracts=contracts, players=players if current_user.role in ['president', 'manager', 'coach'] else None)
|
|
|
|
|
|
@users_bp.route('/contracts/upload', methods=['GET', 'POST'])
|
|
@login_required
|
|
def upload_contract():
|
|
"""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'))
|
|
|
|
# Get players this user can manage
|
|
if current_user.role == 'coach':
|
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
|
if org_team:
|
|
player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
|
|
players = User.query.filter(User.id.in_(player_ids)).all() if player_ids else []
|
|
else:
|
|
players = []
|
|
else:
|
|
players = User.query.filter_by(role='player').all()
|
|
|
|
if request.method == 'POST':
|
|
player_id = request.form.get('player_id', type=int)
|
|
notes = request.form.get('notes', '').strip()
|
|
|
|
if not can_manage_player_contract(current_user, player_id):
|
|
flash('You do not have permission to upload a contract for this player.', 'danger')
|
|
return redirect(url_for('users.upload_contract'))
|
|
|
|
if 'contract_file' not in request.files:
|
|
flash('No file selected.', 'danger')
|
|
return redirect(url_for('users.upload_contract'))
|
|
|
|
file = request.files['contract_file']
|
|
if file.filename == '':
|
|
flash('No file selected.', 'danger')
|
|
return redirect(url_for('users.upload_contract'))
|
|
|
|
# Create upload directory
|
|
upload_dir = os.path.join(os.getcwd(), 'documents', 'contrats signés')
|
|
os.makedirs(upload_dir, exist_ok=True)
|
|
|
|
# Get player info for folder structure
|
|
player = User.query.get_or_404(player_id)
|
|
player_teams = player.get_org_teams()
|
|
team = player_teams[0] if player_teams else None
|
|
|
|
# Create team folder if team exists
|
|
if team:
|
|
team_folder = os.path.join(upload_dir, secure_filename(team.name))
|
|
os.makedirs(team_folder, exist_ok=True)
|
|
final_dir = team_folder
|
|
else:
|
|
final_dir = upload_dir
|
|
|
|
# Generate unique filename
|
|
original_filename = secure_filename(file.filename)
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
stored_filename = f"{secure_filename(player.full_name)}_{timestamp}_{original_filename}"
|
|
stored_filename = stored_filename.replace(' ', '_')
|
|
|
|
# Save file
|
|
file_path = os.path.join(final_dir, stored_filename)
|
|
file.save(file_path)
|
|
|
|
# Create database record
|
|
contract = Contract(
|
|
player_id=player_id,
|
|
team_id=team.id if team else None,
|
|
uploaded_by_id=current_user.id,
|
|
original_filename=original_filename,
|
|
stored_filename=stored_filename,
|
|
file_path=file_path,
|
|
notes=notes if notes else None
|
|
)
|
|
db.session.add(contract)
|
|
db.session.commit()
|
|
|
|
flash(f'Contract uploaded successfully for {player.full_name}!', 'success')
|
|
return redirect(url_for('users.list_contracts'))
|
|
|
|
return render_template('pages/upload_contract.html', players=players)
|
|
|
|
|
|
@users_bp.route('/contracts/<int:contract_id>/upload_signed', methods=['POST'])
|
|
@login_required
|
|
def upload_signed_contract(contract_id):
|
|
"""Upload a signed contract (player only).
|
|
|
|
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):
|
|
flash('Only the player can upload their signed contract.', 'danger')
|
|
return redirect(url_for('users.list_contracts'))
|
|
|
|
if 'signed_file' not in request.files:
|
|
flash('No file selected.', 'danger')
|
|
return redirect(url_for('users.list_contracts'))
|
|
|
|
file = request.files['signed_file']
|
|
if file.filename == '':
|
|
flash('No file selected.', 'danger')
|
|
return redirect(url_for('users.list_contracts'))
|
|
|
|
# Generate unique filename for signed contract
|
|
original_filename = secure_filename(file.filename)
|
|
signed_filename = f"signed_{contract.stored_filename}"
|
|
|
|
# Save file
|
|
file.save(contract.file_path.replace(contract.stored_filename, signed_filename))
|
|
|
|
# Update contract record
|
|
contract.signed_filename = signed_filename
|
|
contract.signed_file_path = contract.file_path.replace(contract.stored_filename, signed_filename)
|
|
contract.status = 'signed'
|
|
contract.signed_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
flash('Signed contract uploaded successfully!', 'success')
|
|
return redirect(url_for('users.list_contracts'))
|
|
|
|
|
|
@users_bp.route('/contracts/<int:contract_id>/download')
|
|
@login_required
|
|
def download_contract(contract_id):
|
|
"""Download a contract file.
|
|
|
|
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):
|
|
flash('You do not have permission to download this contract.', 'danger')
|
|
return redirect(url_for('users.list_contracts'))
|
|
|
|
return send_file(contract.file_path, as_attachment=True, download_name=contract.original_filename)
|
|
|
|
|
|
@users_bp.route('/contracts/<int:contract_id>/download_signed')
|
|
@login_required
|
|
def download_signed_contract(contract_id):
|
|
"""Download a signed contract file.
|
|
|
|
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):
|
|
flash('You do not have permission to download this contract.', 'danger')
|
|
return redirect(url_for('users.list_contracts'))
|
|
|
|
if not contract.signed_file_path:
|
|
flash('No signed contract available.', 'danger')
|
|
return redirect(url_for('users.list_contracts'))
|
|
|
|
return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename)
|
|
|
|
|
|
# One on One Functions
|
|
|
|
|
|
DISCORD_WEBHOOK_URL = os.environ.get('DISCORD_WEBHOOK_URL', '')
|
|
|
|
|
|
def send_discord_notification(player_name, points, date_str, start_time_str, end_time_str, team_name, coach_name, coach_discord, coach_discord_id, request_id=None):
|
|
"""Send a Discord notification for a One on One request.
|
|
|
|
Sends a DM to the coach via Discord bot if their Discord User ID is configured,
|
|
otherwise falls back to webhook notification.
|
|
|
|
Args:
|
|
player_name: Name of the player making the request.
|
|
points: Discussion points from the player.
|
|
date_str: Date of the requested session.
|
|
start_time_str: Start time of the requested session.
|
|
end_time_str: End time of the requested session.
|
|
team_name: Name of the player's team.
|
|
coach_name: Name of the coach.
|
|
coach_discord: Coach's Discord username.
|
|
coach_discord_id: Coach's Discord User ID (for DMs).
|
|
"""
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Try to send DM via Discord bot first (requires coach's Discord User ID)
|
|
if coach_discord_id:
|
|
try:
|
|
from discord_bot import send_one_on_one_dm
|
|
send_one_on_one_dm(
|
|
coach_name=coach_name,
|
|
coach_discord_id=coach_discord_id,
|
|
player_name=player_name,
|
|
team_name=team_name,
|
|
date_str=date_str,
|
|
start_time=start_time_str,
|
|
end_time=end_time_str,
|
|
points=points,
|
|
request_id=request_id
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to send Discord DM: {e}")
|
|
|
|
# Also send to webhook as backup/fallback
|
|
if DISCORD_WEBHOOK_URL:
|
|
try:
|
|
from discord_bot import send_one_on_one_dm
|
|
|
|
# Try to send DM to webhook URL if it's a user ID
|
|
if DISCORD_WEBHOOK_URL.isdigit() and not coach_discord_id:
|
|
send_one_on_one_dm(
|
|
coach_name=coach_name,
|
|
coach_discord_id=DISCORD_WEBHOOK_URL,
|
|
player_name=player_name,
|
|
team_name=team_name,
|
|
date_str=date_str,
|
|
start_time=start_time_str,
|
|
end_time=end_time_str,
|
|
points=points
|
|
)
|
|
elif not DISCORD_WEBHOOK_URL.isdigit():
|
|
# Legacy webhook URL - send traditional webhook embed
|
|
embed = {
|
|
"embeds": [{
|
|
"title": "One on One Request",
|
|
"color": 3447003,
|
|
"fields": [
|
|
{"name": "Player", "value": player_name, "inline": True},
|
|
{"name": "Team", "value": team_name or "Unknown Team", "inline": True},
|
|
{"name": "Date", "value": date_str, "inline": True},
|
|
{"name": "Time", "value": f"{start_time_str} - {end_time_str}", "inline": True},
|
|
{"name": "Discussion Points", "value": points or "No specific points provided", "inline": False}
|
|
],
|
|
"footer": {
|
|
"text": f"Coach: {coach_name}" + (f" (Discord: {coach_discord})" if coach_discord else "")
|
|
}
|
|
}]
|
|
}
|
|
requests.post(DISCORD_WEBHOOK_URL, json=embed, timeout=5)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to send Discord notification: {e}")
|
|
|
|
@users_bp.route('/one-on-one', methods=['GET', 'POST'])
|
|
@login_required
|
|
def one_on_one():
|
|
"""One on One request page for players.
|
|
|
|
Players can view team notes, personal notes, and request One on One sessions.
|
|
|
|
Returns:
|
|
Response: Rendered One on One page.
|
|
"""
|
|
if current_user.role != 'player':
|
|
flash('Only players can request One on One sessions.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
# Get player's teams and coach (use the first team the player is on)
|
|
org_teams = current_user.get_org_teams()
|
|
org_team = org_teams[0] if org_teams else None
|
|
coach = User.query.get(org_team.coach_id) if org_team and org_team.coach_id else None
|
|
|
|
if not coach:
|
|
flash('You do not have a coach assigned to your team.', 'info')
|
|
|
|
# Get team notes for this player's team
|
|
team_notes = []
|
|
if org_team:
|
|
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
|
|
|
|
# Get personal notes for this player
|
|
personal_notes = PersonalNote.query.filter_by(player_id=current_user.id).order_by(PersonalNote.created_at.desc()).all()
|
|
|
|
# Get coach's availability for the next 7 days
|
|
coach_availability = []
|
|
if coach:
|
|
coach_availability = CoachAvailability.query.filter_by(coach_id=coach.id).all()
|
|
|
|
if request.method == 'POST':
|
|
# Handle One on One request submission
|
|
date_str = request.form.get('date')
|
|
start_time_str = request.form.get('start_time')
|
|
end_time_str = request.form.get('end_time')
|
|
points = request.form.get('points', '').strip()
|
|
|
|
if not coach:
|
|
flash('Cannot request One on One - no coach assigned.', 'danger')
|
|
return redirect(url_for('users.one_on_one'))
|
|
|
|
# Validate date and time
|
|
try:
|
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
|
start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
|
end_time = datetime.strptime(end_time_str, '%H:%M').time()
|
|
except (ValueError, TypeError):
|
|
flash('Invalid date or time format.', 'danger')
|
|
return redirect(url_for('users.one_on_one'))
|
|
|
|
# Check if the requested slot is within coach's availability
|
|
date_parts = date_str.split('-')
|
|
check_date = datetime(int(date_parts[0]), int(date_parts[1]), int(date_parts[2]))
|
|
# Python weekday: Monday=0, Sunday=6
|
|
day_of_week = check_date.weekday()
|
|
|
|
is_available = any(
|
|
av.day_of_week == day_of_week and
|
|
av.start_time <= start_time and
|
|
av.end_time >= end_time
|
|
for av in coach_availability
|
|
)
|
|
|
|
if not is_available:
|
|
flash('The requested time is not within the coach\'s availability.', 'danger')
|
|
return redirect(url_for('users.one_on_one'))
|
|
|
|
# Create the request
|
|
request_obj = OneOnOneRequest(
|
|
player_id=current_user.id,
|
|
coach_id=coach.id,
|
|
org_team_id=org_team.id if org_team else None,
|
|
date=date_obj,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
points=points if points else None
|
|
)
|
|
db.session.add(request_obj)
|
|
db.session.commit()
|
|
|
|
# Send Discord notification
|
|
send_discord_notification(
|
|
player_name=current_user.full_name,
|
|
points=points,
|
|
date_str=date_str,
|
|
start_time_str=start_time_str,
|
|
end_time_str=end_time_str,
|
|
team_name=org_team.name if org_team else None,
|
|
coach_name=coach.full_name,
|
|
coach_discord=coach.discord_username,
|
|
coach_discord_id=coach.discord_user_id,
|
|
request_id=request_obj.id
|
|
)
|
|
|
|
flash('One on One request sent to your coach!', 'success')
|
|
return redirect(url_for('users.one_on_one'))
|
|
|
|
# Calculate dates for the next week (starting from today)
|
|
dates = []
|
|
for i in range(7):
|
|
d = date_type.today() + timedelta(days=i)
|
|
dates.append({
|
|
'value': d.strftime('%Y-%m-%d'),
|
|
'display': d.strftime('%A, %b %d'),
|
|
'day_of_week': d.weekday()
|
|
})
|
|
|
|
# Serialize coach availability for JavaScript
|
|
coach_availability_serialized = [
|
|
{
|
|
'id': av.id,
|
|
'day_of_week': av.day_of_week,
|
|
'start_time': av.start_time.strftime('%H:%M'),
|
|
'end_time': av.end_time.strftime('%H:%M')
|
|
}
|
|
for av in coach_availability
|
|
]
|
|
|
|
return render_template('pages/one_on_one.html',
|
|
org_team=org_team,
|
|
coach=coach,
|
|
team_notes=team_notes,
|
|
personal_notes=personal_notes,
|
|
coach_availability=coach_availability_serialized,
|
|
dates=dates)
|
|
|
|
|
|
@users_bp.route('/coach-availability', methods=['GET', 'POST'])
|
|
@login_required
|
|
def manage_coach_availability():
|
|
"""Manage coach availability (for coaches).
|
|
|
|
GET: Render the availability management page.
|
|
POST: Add availability slots via bulk.
|
|
|
|
Returns:
|
|
Response: Rendered management page or redirect.
|
|
"""
|
|
if current_user.role != 'coach':
|
|
flash('Only coaches can manage availability.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
if request.method == 'POST':
|
|
data = request.get_json()
|
|
slots = data.get('slots', [])
|
|
|
|
created = []
|
|
for slot in slots:
|
|
day_of_week = slot.get('day_of_week')
|
|
start_time_str = slot.get('start_time')
|
|
|
|
if day_of_week is None or day_of_week < 0 or day_of_week > 6:
|
|
continue
|
|
|
|
try:
|
|
start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
|
except (ValueError, TypeError):
|
|
continue
|
|
|
|
end_time = add_30_minutes(start_time)
|
|
|
|
# Check if this slot already exists for this coach
|
|
existing = CoachAvailability.query.filter_by(
|
|
coach_id=current_user.id,
|
|
day_of_week=day_of_week,
|
|
start_time=start_time
|
|
).first()
|
|
|
|
if not existing:
|
|
availability = CoachAvailability(
|
|
coach_id=current_user.id,
|
|
day_of_week=day_of_week,
|
|
start_time=start_time,
|
|
end_time=end_time
|
|
)
|
|
db.session.add(availability)
|
|
db.session.flush()
|
|
created.append({
|
|
'id': availability.id,
|
|
'day_of_week': availability.day_of_week,
|
|
'day_name': DAY_NAMES[availability.day_of_week],
|
|
'start_time': availability.start_time.strftime('%H:%M'),
|
|
'end_time': availability.end_time.strftime('%H:%M')
|
|
})
|
|
|
|
db.session.commit()
|
|
return jsonify({'success': True, 'created': created})
|
|
|
|
# Get existing availability
|
|
existing_availability = CoachAvailability.query.filter_by(coach_id=current_user.id).all()
|
|
|
|
return render_template('pages/coach_availability.html',
|
|
existing_availability=existing_availability)
|
|
|
|
|
|
@users_bp.route('/coach-availability/clear', methods=['POST'])
|
|
@login_required
|
|
def clear_coach_availability():
|
|
"""Clear all coach availability slots.
|
|
|
|
Returns:
|
|
Response: JSON with success status.
|
|
"""
|
|
if current_user.role != 'coach':
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
CoachAvailability.query.filter_by(coach_id=current_user.id).delete()
|
|
db.session.commit()
|
|
return jsonify({'success': True})
|
|
|
|
|
|
@users_bp.route('/coach-availability/<int:availability_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def delete_coach_availability(availability_id):
|
|
"""Delete a coach availability slot.
|
|
|
|
Args:
|
|
availability_id: The ID of the availability slot to delete.
|
|
|
|
Returns:
|
|
Response: JSON with success status or error.
|
|
"""
|
|
if current_user.role != 'coach':
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
availability = CoachAvailability.query.get_or_404(availability_id)
|
|
|
|
if availability.coach_id != current_user.id:
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
db.session.delete(availability)
|
|
db.session.commit()
|
|
return jsonify({'success': True})
|
|
|
|
|
|
@users_bp.route('/api/coach-availability/<int:coach_id>')
|
|
@login_required
|
|
def api_get_coach_availability(coach_id):
|
|
"""API endpoint to get coach availability.
|
|
|
|
Args:
|
|
coach_id: The ID of the coach.
|
|
|
|
Returns:
|
|
Response: JSON with availability data.
|
|
"""
|
|
if current_user.role != 'player':
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
# Players can only view their own coach's availability
|
|
player_teams = current_user.get_org_teams()
|
|
org_team = player_teams[0] if player_teams else None
|
|
if org_team and org_team.coach_id != coach_id:
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
availability = CoachAvailability.query.filter_by(coach_id=coach_id).all()
|
|
|
|
result = {}
|
|
for av in availability:
|
|
if av.day_of_week not in result:
|
|
result[av.day_of_week] = []
|
|
result[av.day_of_week].append({
|
|
'id': av.id,
|
|
'start_time': av.start_time.strftime('%H:%M'),
|
|
'end_time': av.end_time.strftime('%H:%M')
|
|
})
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
@users_bp.route('/team-notes', methods=['GET', 'POST'])
|
|
@login_required
|
|
def manage_team_notes():
|
|
"""Manage team improvement notes (for coaches).
|
|
|
|
GET: Render the team notes management page.
|
|
POST: Create or update team notes.
|
|
|
|
Returns:
|
|
Response: Rendered management page or redirect.
|
|
"""
|
|
if current_user.role != 'coach':
|
|
flash('Only coaches can manage team notes.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
# Get the coach's team
|
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
|
if not org_team:
|
|
flash('You are not assigned to coach any team.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
# Get existing team notes
|
|
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
|
|
|
|
if request.method == 'POST':
|
|
content = request.form.get('content', '').strip()
|
|
|
|
if content:
|
|
# Create new team note entry (keeps history)
|
|
note = TeamNote(
|
|
org_team_id=org_team.id,
|
|
coach_id=current_user.id,
|
|
content=content
|
|
)
|
|
db.session.add(note)
|
|
db.session.commit()
|
|
flash('Team notes added successfully!', 'success')
|
|
return redirect(url_for('users.manage_team_notes'))
|
|
|
|
return render_template('pages/team_notes.html',
|
|
org_team=org_team,
|
|
team_notes=team_notes)
|
|
|
|
|
|
@users_bp.route('/personal-notes', methods=['GET', 'POST'])
|
|
@login_required
|
|
def manage_personal_notes():
|
|
"""Manage personal notes for players (for coaches).
|
|
|
|
GET: Render the personal notes management page.
|
|
POST: Create a personal note for a player.
|
|
|
|
Returns:
|
|
Response: Rendered management page or redirect.
|
|
"""
|
|
if current_user.role != 'coach':
|
|
flash('Only coaches can manage personal notes.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
# Get players on the coach's team
|
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
|
players = []
|
|
if org_team:
|
|
player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
|
|
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all() if player_ids else []
|
|
|
|
if request.method == 'POST':
|
|
player_id = request.form.get('player_id', type=int)
|
|
content = request.form.get('content', '').strip()
|
|
|
|
if not player_id or not content:
|
|
flash('Please select a player and enter note content.', 'danger')
|
|
return redirect(url_for('users.manage_personal_notes'))
|
|
|
|
# Verify player is on coach's team
|
|
if org_team and player_id not in [p.id for p in players]:
|
|
flash('You can only add notes for players on your team.', 'danger')
|
|
return redirect(url_for('users.manage_personal_notes'))
|
|
|
|
note = PersonalNote(
|
|
player_id=player_id,
|
|
coach_id=current_user.id,
|
|
content=content
|
|
)
|
|
db.session.add(note)
|
|
db.session.commit()
|
|
flash('Personal note added successfully!', 'success')
|
|
return redirect(url_for('users.manage_personal_notes'))
|
|
|
|
# Get all personal notes for players on this team
|
|
personal_notes = []
|
|
if org_team:
|
|
personal_notes = PersonalNote.query.filter(
|
|
PersonalNote.player_id.in_([p.id for p in players])
|
|
).order_by(PersonalNote.created_at.desc()).all()
|
|
|
|
return render_template('pages/personal_notes.html',
|
|
players=players,
|
|
personal_notes=personal_notes,
|
|
org_team=org_team)
|
|
|
|
|
|
@users_bp.route('/notes', methods=['GET'])
|
|
@login_required
|
|
def notes_dashboard():
|
|
"""Unified notes dashboard for coaches.
|
|
|
|
GET: Render the combined notes management page with both team and personal notes forms.
|
|
|
|
Returns:
|
|
Response: Rendered notes dashboard template.
|
|
"""
|
|
if current_user.role != 'coach':
|
|
flash('Only coaches can manage notes.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
# Get the coach's team
|
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
|
players = []
|
|
if org_team:
|
|
player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
|
|
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all() if player_ids else []
|
|
|
|
# Get existing team notes
|
|
team_notes = []
|
|
latest_team_note = None
|
|
if org_team:
|
|
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
|
|
latest_team_note = team_notes[0] if team_notes else None
|
|
|
|
# Get all personal notes for players on this team
|
|
personal_notes = []
|
|
if org_team:
|
|
personal_notes = PersonalNote.query.filter(
|
|
PersonalNote.player_id.in_([p.id for p in players])
|
|
).order_by(PersonalNote.created_at.desc()).all()
|
|
|
|
# Get available matches and tryouts for context
|
|
matches = []
|
|
tryouts = []
|
|
teams = []
|
|
if current_user.role == 'coach' and org_team:
|
|
tryouts = Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date.desc()).all()
|
|
matches = Match.query.join(Tryout).filter(Tryout.target_org_team_id == org_team.id).order_by(Match.date.desc()).all()
|
|
teams = Team.query.order_by(Team.name).all()
|
|
|
|
return render_template('pages/notes.html',
|
|
org_team=org_team,
|
|
players=players,
|
|
team_notes=team_notes,
|
|
latest_team_note=latest_team_note,
|
|
personal_notes=personal_notes,
|
|
matches=matches,
|
|
tryouts=tryouts,
|
|
teams=teams)
|
|
|
|
|
|
# Note source types for filtering
|
|
NOTE_SOURCE_MATCH = 'match'
|
|
NOTE_SOURCE_TRYOUT = 'tryout'
|
|
NOTE_SOURCE_TEAM = 'team'
|
|
|
|
|
|
@users_bp.route('/my-notes')
|
|
@login_required
|
|
def my_notes():
|
|
"""View all notes for the current player.
|
|
|
|
Shows both personal notes and team notes that the player has received.
|
|
Personal notes are grouped by source (match, tryout, team).
|
|
|
|
Returns:
|
|
Response: Rendered my notes template.
|
|
"""
|
|
if current_user.role != 'player':
|
|
flash('Only players can view their notes.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
# Get player's teams and coach (use the first team the player is on)
|
|
player_teams = current_user.get_org_teams()
|
|
org_team = player_teams[0] if player_teams else None
|
|
|
|
# Get all personal notes for this player with eager loading for relationships
|
|
personal_notes = PersonalNote.query.filter_by(player_id=current_user.id).order_by(PersonalNote.created_at.desc()).all()
|
|
|
|
# Get team notes for this player's team
|
|
team_notes = []
|
|
if org_team:
|
|
team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all()
|
|
|
|
return render_template('pages/player_personal_notes.html',
|
|
org_team=org_team,
|
|
personal_notes=personal_notes,
|
|
team_notes=team_notes)
|
|
|
|
|
|
@users_bp.route('/notes/add', methods=['GET', 'POST'])
|
|
@login_required
|
|
def add_personal_note():
|
|
"""Add a personal note with optional context (for coaches and managers).
|
|
|
|
GET: Render the note creation form.
|
|
POST: Create a personal note, optionally linked to match/tryout/team.
|
|
|
|
Returns:
|
|
Response: Create form or redirect to notes view.
|
|
"""
|
|
if current_user.role not in ['coach', 'manager', 'president']:
|
|
flash('Only coaches and managers can add notes.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
# Get players this user can manage
|
|
players = []
|
|
org_team = None
|
|
if current_user.role == 'coach':
|
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
|
if org_team:
|
|
player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
|
|
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all() if player_ids else []
|
|
elif current_user.role in ['president', 'manager']:
|
|
players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
|
|
|
# Get available matches and tryouts for context
|
|
matches = []
|
|
tryouts = []
|
|
teams = []
|
|
if current_user.role == 'coach' and org_team:
|
|
tryouts = Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date.desc()).all()
|
|
matches = Match.query.join(Tryout).filter(Tryout.target_org_team_id == org_team.id).order_by(Match.date.desc()).all()
|
|
elif current_user.role in ['president', 'manager']:
|
|
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
|
|
matches = Match.query.order_by(Match.date.desc()).all()
|
|
teams = Team.query.order_by(Team.name).all()
|
|
|
|
if request.method == 'POST':
|
|
player_id = request.form.get('player_id', type=int)
|
|
content = request.form.get('content', '').strip()
|
|
match_id = request.form.get('match_id', type=int)
|
|
tryout_id = request.form.get('tryout_id', type=int)
|
|
team_id = request.form.get('team_id', type=int)
|
|
|
|
if not player_id or not content:
|
|
flash('Please select a player and enter note content.', 'danger')
|
|
return redirect(url_for('users.add_personal_note'))
|
|
|
|
# Verify player is on coach's team (if coach)
|
|
if current_user.role == 'coach' and org_team and player_id not in [p.id for p in players]:
|
|
flash('You can only add notes for players on your team.', 'danger')
|
|
return redirect(url_for('users.add_personal_note'))
|
|
|
|
# Validate context - ensure coach can access the match/tryout/team
|
|
if match_id:
|
|
match = Match.query.get(match_id)
|
|
if match:
|
|
match_tryout = Tryout.query.get(match.tryout_id)
|
|
# For coaches, verify the match is in their team's tryout
|
|
if current_user.role == 'coach' and org_team:
|
|
if match_tryout and match_tryout.target_org_team_id and match_tryout.target_org_team_id != org_team.id:
|
|
flash('You can only add notes for matches in your team\'s tryouts.', 'danger')
|
|
return redirect(url_for('users.add_personal_note'))
|
|
if tryout_id:
|
|
tryout = Tryout.query.get(tryout_id)
|
|
# For coaches, verify the tryout targets their team
|
|
if current_user.role == 'coach' and org_team:
|
|
if tryout and tryout.target_org_team_id and tryout.target_org_team_id != org_team.id:
|
|
flash('You can only add notes for your team\'s tryouts.', 'danger')
|
|
return redirect(url_for('users.add_personal_note'))
|
|
if team_id:
|
|
team = Team.query.get(team_id)
|
|
if team:
|
|
team_tryout = Tryout.query.get(team.tryout_id)
|
|
# For coaches, verify the team is in their tryout
|
|
if current_user.role == 'coach' and org_team:
|
|
if team_tryout and team_tryout.target_org_team_id and team_tryout.target_org_team_id != org_team.id:
|
|
flash('You can only add notes for your team\'s tryouts.', 'danger')
|
|
return redirect(url_for('users.add_personal_note'))
|
|
|
|
note = PersonalNote(
|
|
player_id=player_id,
|
|
coach_id=current_user.id,
|
|
content=content,
|
|
match_id=match_id if match_id else None,
|
|
team_id=team_id if team_id else None,
|
|
tryout_id=tryout_id if tryout_id else None
|
|
)
|
|
db.session.add(note)
|
|
db.session.commit()
|
|
flash('Personal note added successfully!', 'success')
|
|
return redirect(url_for('users.notes_dashboard'))
|
|
|
|
return render_template('pages/add_personal_note.html',
|
|
players=players,
|
|
matches=matches,
|
|
tryouts=tryouts,
|
|
teams=teams,
|
|
org_team=org_team)
|
|
|
|
|
|
@users_bp.route('/match/<int:match_id>/add-note', methods=['GET', 'POST'])
|
|
@login_required
|
|
def add_note_from_match(match_id):
|
|
"""Add a personal note from a match view context (for coaches).
|
|
|
|
GET: Render the note creation form pre-filled with match info.
|
|
POST: Create a personal note linked to the match.
|
|
|
|
Args:
|
|
match_id: The ID of the match to create note for.
|
|
|
|
Returns:
|
|
Response: Create form or redirect.
|
|
"""
|
|
if current_user.role not in ['coach', 'manager', 'president']:
|
|
flash('Only coaches can add notes from matches.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
match = Match.query.get_or_404(match_id)
|
|
tryout = Tryout.query.get(match.tryout_id)
|
|
|
|
# Check permissions
|
|
if current_user.role == 'coach':
|
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
|
if not org_team or (tryout.target_org_team_id and tryout.target_org_team_id != org_team.id):
|
|
flash('You do not have permission for this match.', 'danger')
|
|
return redirect(url_for('matches.calendar'))
|
|
|
|
# Get all participants for this match
|
|
participant_ids = []
|
|
if match.match_type == 'team_vs_team':
|
|
if match.team1_id:
|
|
team_members = TeamMember.query.filter_by(team_id=match.team1_id).all()
|
|
participant_ids.extend([m.player_id for m in team_members])
|
|
if match.team2_id:
|
|
team_members = TeamMember.query.filter_by(team_id=match.team2_id).all()
|
|
participant_ids.extend([m.player_id for m in team_members])
|
|
else:
|
|
participants = MatchParticipant.query.filter_by(match_id=match_id).all()
|
|
participant_ids = [p.player_id for p in participants]
|
|
|
|
players = User.query.filter(User.id.in_(participant_ids)).order_by(User.full_name).all() if participant_ids else []
|
|
|
|
# Get team notes for context
|
|
team_notes = []
|
|
if tryout and tryout.target_org_team_id:
|
|
team_notes = TeamNote.query.filter_by(org_team_id=tryout.target_org_team_id).order_by(TeamNote.created_at.desc()).all()
|
|
|
|
if request.method == 'POST':
|
|
player_id = request.form.get('player_id', type=int)
|
|
content = request.form.get('content', '').strip()
|
|
|
|
if not player_id or not content:
|
|
flash('Please select a player and enter note content.', 'danger')
|
|
elif player_id not in participant_ids:
|
|
flash('Selected player is not in this match.', 'danger')
|
|
else:
|
|
note = PersonalNote(
|
|
player_id=player_id,
|
|
coach_id=current_user.id,
|
|
content=content,
|
|
match_id=match_id
|
|
)
|
|
db.session.add(note)
|
|
db.session.commit()
|
|
flash('Personal note added successfully!', 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
|
|
|
return render_template('pages/add_note_from_match.html',
|
|
match=match,
|
|
tryout=tryout,
|
|
players=players,
|
|
team_notes=team_notes)
|
|
|
|
|
|
@users_bp.route('/tryout/<int:tryout_id>/add-note', methods=['GET', 'POST'])
|
|
@login_required
|
|
def add_note_from_tryout(tryout_id):
|
|
"""Add a personal note from a tryout view context (for coaches).
|
|
|
|
GET: Render the note creation form pre-filled with tryout info.
|
|
POST: Create a personal note linked to the tryout.
|
|
|
|
Args:
|
|
tryout_id: The ID of the tryout to create note for.
|
|
|
|
Returns:
|
|
Response: Create form or redirect.
|
|
"""
|
|
if current_user.role not in ['coach', 'manager', 'president']:
|
|
flash('Only coaches can add notes from tryouts.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
tryout = Tryout.query.get_or_404(tryout_id)
|
|
|
|
# Check permissions
|
|
org_team = None
|
|
if current_user.role == 'coach':
|
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
|
if not org_team or (tryout.target_org_team_id and tryout.target_org_team_id != org_team.id):
|
|
flash('You do not have permission for this tryout.', 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
|
|
# Get all registered players in this tryout
|
|
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
|
|
player_ids = [r.player_id for r in registrations]
|
|
|
|
# If coach, filter to only their team players; otherwise include all
|
|
if org_team:
|
|
team_player_ids = [tp.player_id for tp in TeamPlayer.query.filter_by(org_team_id=org_team.id).all()]
|
|
players = User.query.filter(User.id.in_(player_ids), User.id.in_(team_player_ids)).order_by(User.full_name).all()
|
|
else:
|
|
players = User.query.filter(User.id.in_(player_ids)).order_by(User.full_name).all()
|
|
|
|
# Get team notes for context
|
|
team_notes = []
|
|
if tryout.target_org_team_id:
|
|
team_notes = TeamNote.query.filter_by(org_team_id=tryout.target_org_team_id).order_by(TeamNote.created_at.desc()).all()
|
|
|
|
if request.method == 'POST':
|
|
player_id = request.form.get('player_id', type=int)
|
|
content = request.form.get('content', '').strip()
|
|
|
|
if not player_id or not content:
|
|
flash('Please select a player and enter note content.', 'danger')
|
|
elif player_id not in [p.id for p in players]:
|
|
flash('Selected player is not registered for this tryout.', 'danger')
|
|
else:
|
|
note = PersonalNote(
|
|
player_id=player_id,
|
|
coach_id=current_user.id,
|
|
content=content,
|
|
tryout_id=tryout_id
|
|
)
|
|
db.session.add(note)
|
|
db.session.commit()
|
|
flash('Personal note added successfully!', 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
# Allow pre-selecting a player via query parameter
|
|
preselected_player_id = request.args.get('player_id', type=int)
|
|
|
|
return render_template('pages/add_note_from_tryout.html',
|
|
tryout=tryout,
|
|
players=players,
|
|
team_notes=team_notes,
|
|
preselected_player_id=preselected_player_id)
|