"""One-on-one sessions between a player and their coach.""" 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 marshmallow import ValidationError from app.extensions import db from app.forms import flash_validation_errors, form_payload 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 from app.time_utils import utc_now_naive from app.validators import OneOnOneRejectionSchema, OneOnOneRequestSchema @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() ) # Kept as model objects for the availability check below, and serialised # separately for the page. They used to be the same list of strings, # which is what made the check compare '9:00' with '10:00' as text. availabilities = CoachAvailability.query.filter_by(coach_id=coach.id).all() if coach else [] 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': if not coach: flash(_('Cannot request One on One - no coach assigned.'), 'danger') return redirect(url_for('users.one_on_one')) try: data = OneOnOneRequestSchema().load(form_payload()) except ValidationError as err: flash_validation_errors(err) return redirect(url_for('users.one_on_one')) date_obj = data['date'] start_time = data['start_time'] end_time = data['end_time'] points = data['points'] or '' # Compared as times, not as strings. The old code parsed the three # form fields into objects and then compared the *original strings* # against the serialised availability — which worked only because # both sides happened to be zero-padded HH:MM. is_available = any( av.day_of_week == date_obj.weekday() and av.start_time <= start_time and av.end_time >= end_time for av in availabilities ) 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_obj.strftime('%Y-%m-%d'), start_time_str=start_time.strftime('%H:%M'), end_time_str=end_time.strftime('%H:%M'), 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//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 = db.get_or_404(OneOnOneRequest, 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 = utc_now_naive() 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//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 = db.get_or_404(OneOnOneRequest, 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')) try: data = OneOnOneRejectionSchema().load(form_payload(list_fields=())) except ValidationError as err: flash_validation_errors(err) return redirect(url_for('users.notes_dashboard')) rejection_reason = data['rejection_reason'] player = request_obj.player request_obj.status = 'rejected' request_obj.responded_at = utc_now_naive() 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'))