Active la regle isort (I) de ruff. 45 fichiers reordonnes, aucun changement de comportement : la suite passe avant comme apres. app/models/__init__.py en est exclu. Ses imports sont ranges en onze couches commentees qui decrivent le graphe de dependances ; trier par ordre alphabetique laisse chaque titre au-dessus d un import qu il ne decrit pas, et ce fichier n a qu un role, etre lu. Commit isole, comme le formatage : un diff de brassage ne doit pas servir de couverture a un changement de comportement.
261 lines
9.4 KiB
Python
261 lines
9.4 KiB
Python
"""One-on-one sessions between a player and their coach."""
|
|
|
|
from datetime import datetime
|
|
|
|
from flask import flash, redirect, render_template, request, url_for
|
|
from flask_babel import gettext as _
|
|
from flask_login import current_user, login_required
|
|
|
|
from app.extensions import db
|
|
from app.models import Coach, CoachAvailability, OneOnOneRequest, PersonalNote, Player, TeamNote
|
|
from app.routes.users.blueprint import users_bp
|
|
from app.services.notifications import send_discord_notification
|
|
|
|
|
|
@users_bp.route('/one-on-one', methods=['GET', 'POST'])
|
|
@login_required
|
|
def one_on_one():
|
|
"""One on One request page for players."""
|
|
if not isinstance(current_user, Player):
|
|
flash(_('Only players can request One on One sessions.'), 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
org_teams = current_user.get_org_teams()
|
|
org_team = org_teams[0] if org_teams else None
|
|
# Reading org_team.coach_id directly told every player whose team lists
|
|
# its coaches through the many-to-many relationship — the newer of the
|
|
# two ways — that they had no coach, and closed the page to them.
|
|
# get_coaches() falls back to the legacy column when the list is empty.
|
|
team_coaches = org_team.get_coaches() if org_team else []
|
|
coach = team_coaches[0] if team_coaches else None
|
|
|
|
if not coach:
|
|
flash(_('You do not have a coach assigned to your team.'), 'info')
|
|
|
|
team_notes = []
|
|
if org_team:
|
|
team_notes = (
|
|
TeamNote.query.filter_by(org_team_id=org_team.id)
|
|
.order_by(TeamNote.created_at.desc())
|
|
.all()
|
|
)
|
|
|
|
personal_notes = (
|
|
PersonalNote.query.filter_by(player_id=current_user.id)
|
|
.order_by(PersonalNote.created_at.desc())
|
|
.all()
|
|
)
|
|
|
|
coach_availability = []
|
|
if coach:
|
|
availabilities = CoachAvailability.query.filter_by(coach_id=coach.id).all()
|
|
coach_availability = [
|
|
{
|
|
'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 availabilities
|
|
]
|
|
|
|
if request.method == 'POST':
|
|
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'))
|
|
|
|
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_date = datetime.strptime(date_str, '%Y-%m-%d')
|
|
day_of_week = check_date.weekday()
|
|
|
|
is_available = any(
|
|
av['day_of_week'] == day_of_week
|
|
and av['start_time'] <= start_time_str
|
|
and av['end_time'] >= end_time_str
|
|
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'))
|
|
|
|
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(
|
|
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 'Unknown Team',
|
|
coach_name=coach.full_name,
|
|
coach_discord=coach.discord_username or '',
|
|
coach_discord_id=coach.discord_user_id or '',
|
|
request_id=request_obj.id,
|
|
)
|
|
|
|
flash(_('Your One on One request has been submitted!'), 'success')
|
|
return redirect(url_for('users.one_on_one'))
|
|
|
|
# Build list of upcoming dates that have coach availability
|
|
from datetime import date as date_cls
|
|
from datetime import timedelta as td
|
|
|
|
today = date_cls.today()
|
|
available_days = {av['day_of_week'] for av in coach_availability}
|
|
dates = []
|
|
for i in range(14): # Next 14 days
|
|
d = today + td(days=i)
|
|
if d.weekday() in available_days:
|
|
dates.append(
|
|
{
|
|
'value': d.strftime('%Y-%m-%d'),
|
|
'day_of_week': d.weekday(),
|
|
'display': d.strftime('%B %d, %Y (%A)'),
|
|
}
|
|
)
|
|
|
|
# Player's own One on One request history
|
|
my_requests = (
|
|
OneOnOneRequest.query.filter_by(player_id=current_user.id)
|
|
.order_by(OneOnOneRequest.created_at.desc())
|
|
.all()
|
|
)
|
|
|
|
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,
|
|
dates=dates,
|
|
my_requests=my_requests,
|
|
)
|
|
|
|
|
|
@users_bp.route('/one-on-one/<int:request_id>/accept', methods=['POST'])
|
|
@login_required
|
|
def accept_one_on_one(request_id):
|
|
"""Coach accepts a One on One request."""
|
|
if not isinstance(current_user, Coach):
|
|
flash(_('Only coaches can accept One on One requests.'), 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
request_obj = OneOnOneRequest.query.get_or_404(request_id)
|
|
|
|
if request_obj.coach_id != current_user.id:
|
|
flash(_('This request is not for you.'), 'danger')
|
|
return redirect(url_for('users.notes_dashboard'))
|
|
|
|
if request_obj.status != 'pending':
|
|
flash(_('This request has already been processed.'), 'info')
|
|
return redirect(url_for('users.notes_dashboard'))
|
|
|
|
player = request_obj.player
|
|
request_obj.status = 'approved'
|
|
request_obj.responded_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
# Notify player via Discord (same message as if approved through Discord reactions)
|
|
if player and player.discord_user_id:
|
|
from app.discord_bot import send_one_on_one_response
|
|
|
|
send_one_on_one_response(
|
|
player_discord_id=player.discord_user_id,
|
|
player_full_name=player.full_name,
|
|
coach_full_name=current_user.full_name,
|
|
date_str=request_obj.date.strftime('%A, %B %d, %Y'),
|
|
start_time=request_obj.start_time.strftime('%I:%M %p')
|
|
if request_obj.start_time
|
|
else 'TBD',
|
|
end_time=request_obj.end_time.strftime('%I:%M %p') if request_obj.end_time else 'TBD',
|
|
points=request_obj.points or 'No specific points provided',
|
|
approved=True,
|
|
)
|
|
|
|
flash(
|
|
_(
|
|
'One on One request from %(player)s has been approved!',
|
|
player=player.username if player else 'Unknown',
|
|
),
|
|
'success',
|
|
)
|
|
return redirect(url_for('users.notes_dashboard'))
|
|
|
|
|
|
@users_bp.route('/one-on-one/<int:request_id>/reject', methods=['POST'])
|
|
@login_required
|
|
def reject_one_on_one(request_id):
|
|
"""Coach rejects a One on One request."""
|
|
if not isinstance(current_user, Coach):
|
|
flash(_('Only coaches can reject One on One requests.'), 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
request_obj = OneOnOneRequest.query.get_or_404(request_id)
|
|
|
|
if request_obj.coach_id != current_user.id:
|
|
flash(_('This request is not for you.'), 'danger')
|
|
return redirect(url_for('users.notes_dashboard'))
|
|
|
|
if request_obj.status != 'pending':
|
|
flash(_('This request has already been processed.'), 'info')
|
|
return redirect(url_for('users.notes_dashboard'))
|
|
|
|
rejection_reason = request.form.get('rejection_reason', '').strip()
|
|
player = request_obj.player
|
|
|
|
request_obj.status = 'rejected'
|
|
request_obj.responded_at = datetime.utcnow()
|
|
if rejection_reason:
|
|
request_obj.coach_rejection_message = rejection_reason
|
|
db.session.commit()
|
|
|
|
# Notify player via Discord (same message as if rejected through Discord reactions)
|
|
if player and player.discord_user_id:
|
|
from app.discord_bot import send_one_on_one_response
|
|
|
|
send_one_on_one_response(
|
|
player_discord_id=player.discord_user_id,
|
|
player_full_name=player.full_name,
|
|
coach_full_name=current_user.full_name,
|
|
date_str=request_obj.date.strftime('%A, %B %d, %Y'),
|
|
start_time=request_obj.start_time.strftime('%I:%M %p')
|
|
if request_obj.start_time
|
|
else 'TBD',
|
|
end_time=request_obj.end_time.strftime('%I:%M %p') if request_obj.end_time else 'TBD',
|
|
points=request_obj.points or 'No specific points provided',
|
|
approved=False,
|
|
refusal_note=rejection_reason or None,
|
|
)
|
|
|
|
flash(
|
|
_(
|
|
'One on One request from %(player)s has been rejected.',
|
|
player=player.username if player else 'Unknown',
|
|
),
|
|
'info',
|
|
)
|
|
return redirect(url_for('users.notes_dashboard'))
|