Ajout des match qui arrivent pour les coach, president et manager Légèrement alléger la quantité de pages (merge certaines ensembles)
1594 lines
60 KiB
Python
1594 lines
60 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, Evaluation, 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)
|
|
|
|
# Clean up related records before deleting the user
|
|
# 1. Delete evaluations involving this user
|
|
Evaluation.query.filter(
|
|
db.or_(Evaluation.evaluator_id == user_id, Evaluation.player_id == user_id)
|
|
).delete(synchronize_session=False)
|
|
|
|
# 2. Delete availability/disponibility records
|
|
PlayerDisponibility.query.filter_by(player_id=user_id).delete()
|
|
CoachAvailability.query.filter_by(coach_id=user_id).delete()
|
|
|
|
# 3. Delete personal notes involving this user
|
|
PersonalNote.query.filter(
|
|
db.or_(PersonalNote.player_id == user_id, PersonalNote.coach_id == user_id)
|
|
).delete(synchronize_session=False)
|
|
|
|
# 4. Delete team notes authored by this user
|
|
TeamNote.query.filter_by(coach_id=user_id).delete()
|
|
|
|
# 5. Delete one-on-one requests involving this user
|
|
OneOnOneRequest.query.filter(
|
|
db.or_(OneOnOneRequest.player_id == user_id, OneOnOneRequest.coach_id == user_id)
|
|
).delete(synchronize_session=False)
|
|
|
|
# 6. Delete gamertags
|
|
UserGamertag.query.filter_by(user_id=user_id).delete()
|
|
|
|
# 7. Delete contracts for this player (and any they uploaded as manager/coach)
|
|
Contract.query.filter_by(player_id=user_id).delete()
|
|
|
|
# 8. Delete tryout registrations
|
|
TryoutRegistration.query.filter_by(player_id=user_id).delete()
|
|
|
|
# 9. Delete team placements
|
|
TeamPlayer.query.filter_by(player_id=user_id).delete()
|
|
|
|
# 10. Delete team memberships in tryout teams
|
|
TeamMember.query.filter_by(player_id=user_id).delete()
|
|
|
|
# 11. Delete match participants
|
|
MatchParticipant.query.filter_by(player_id=user_id).delete()
|
|
|
|
# 12. Nullify org team references so the user can be deleted
|
|
OrgTeam.query.filter_by(coach_id=user_id).update({'coach_id': None})
|
|
OrgTeam.query.filter_by(manager_id=user_id).update({'manager_id': None})
|
|
|
|
# 13. Handle records this user created (tryouts, matches, teams, org teams, contracts)
|
|
# Reassign created_by to the current president (deleting user) to avoid FK violations
|
|
Tryout.query.filter_by(created_by=user_id).update({'created_by': current_user.id})
|
|
Match.query.filter_by(created_by=user_id).update({'created_by': current_user.id})
|
|
Team.query.filter_by(created_by=user_id).update({'created_by': current_user.id})
|
|
OrgTeam.query.filter_by(created_by=user_id).update({'created_by': current_user.id})
|
|
Contract.query.filter_by(uploaded_by_id=user_id).update({'uploaded_by_id': current_user.id})
|
|
|
|
db.session.delete(user)
|
|
db.session.commit()
|
|
flash(f'User {user.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_note.html',
|
|
players=players,
|
|
matches=matches,
|
|
tryouts=tryouts,
|
|
teams=teams,
|
|
org_team=org_team,
|
|
context_type='general')
|
|
|
|
|
|
@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.html',
|
|
match=match,
|
|
tryout=tryout,
|
|
players=players,
|
|
team_notes=team_notes,
|
|
context_type='match')
|
|
|
|
|
|
@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.html',
|
|
tryout=tryout,
|
|
players=players,
|
|
team_notes=team_notes,
|
|
preselected_player_id=preselected_player_id,
|
|
context_type='tryout')
|