From 19c6740edb3fca9ee7ff832de9d91e2d97dd45c7 Mon Sep 17 00:00:00 2001 From: cedrick2711 Date: Wed, 29 Jul 2026 20:24:10 -0400 Subject: [PATCH] =?UTF-8?q?r=C3=A9gler=20probl=C3=A8me=20avec=20le=20bot?= =?UTF-8?q?=20discord=20et=20ajouter=20un=20panneau=20pour=20g=C3=A9rer=20?= =?UTF-8?q?les=20one=20on=20one=20(accepter=20,refuser,=20confirmer)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app.py | 2 +- app/discord_bot.py | 240 +++++++++++++++++++++------- app/routes/matches.py | 32 ++++ app/routes/users.py | 104 +++++++++++- app/templates/pages/notes.html | 104 ++++++++++++ app/templates/pages/one_on_one.html | 59 +++++++ 6 files changed, 483 insertions(+), 58 deletions(-) diff --git a/app/app.py b/app/app.py index c462a97..9e7febd 100644 --- a/app/app.py +++ b/app/app.py @@ -358,7 +358,7 @@ def create_app(): # Start the Discord bot for notifications try: from app.discord_bot import start_bot - start_bot() + start_bot(flask_app=app) except Exception as e: app.logger.warning('Could not start Discord bot: %s', e) diff --git a/app/discord_bot.py b/app/discord_bot.py index 964960a..e497b38 100644 --- a/app/discord_bot.py +++ b/app/discord_bot.py @@ -10,6 +10,7 @@ import os import logging import asyncio import threading +import traceback from datetime import datetime, timedelta from zoneinfo import ZoneInfo from queue import Queue, Empty @@ -37,15 +38,17 @@ class TeamTryoutsBot(commands.Bot): Handles One on One requests, schedule additions, and daily reminders. """ - def __init__(self): + def __init__(self, flask_app=None): intents = Intents.default() intents.message_content = True intents.dm_messages = True intents.dm_reactions = True intents.reactions = True intents.guilds = True + intents.members = True super().__init__(command_prefix='!', intents=intents) + self.flask_app = flask_app self.pending_requests = {} # Maps message_id to {type, id} for reaction handling self.message_queue = Queue() # Thread-safe queue for messages from Flask self.scheduler = AsyncIOScheduler() @@ -57,7 +60,11 @@ class TeamTryoutsBot(commands.Bot): async def on_ready(self): """Log when the bot is ready and start background tasks.""" - logger.info(f'TeamTryoutsBot is ready! Logged in as {self.user}') + try: + guilds = [g.name for g in self.guilds] + logger.info(f'TeamTryoutsBot is ready! Logged in as {self.user} | Guilds: {guilds}') + except Exception: + logger.info(f'TeamTryoutsBot is ready! Logged in as {self.user}') # Start the queue processing task self.loop.create_task(self.process_queue()) @@ -92,10 +99,16 @@ class TeamTryoutsBot(commands.Bot): if item.get('type') == 'one_on_one_request': await self._send_one_on_one_dm(**item['data']) elif item.get('type') == 'schedule_addition': - await self._send_schedule_notification(**item['data']) + if self.flask_app: + with self.flask_app.app_context(): + await self._send_schedule_notification(**item['data']) + else: + await self._send_schedule_notification(**item['data']) + elif item.get('type') == 'one_on_one_response': + await self._send_one_on_one_response_dm(**item['data']) except Exception as e: - logger.error(f"Error processing queue: {e}") + logger.error(f"Error processing queue: {e}\n{traceback.format_exc()}") await asyncio.sleep(0.1) def is_dm_channel(self, channel) -> bool: @@ -123,14 +136,30 @@ class TeamTryoutsBot(commands.Bot): if emoji_str == CHECK_EMOJI: if handler_type == 'one_on_one': - await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message) + if self.flask_app: + with self.flask_app.app_context(): + await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message) + else: + await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message) elif handler_type == 'schedule_addition': - await self.handle_attendance_confirm(user, message_id, request_id, reaction.message) + if self.flask_app: + with self.flask_app.app_context(): + await self.handle_attendance_confirm(user, message_id, request_id, reaction.message) + else: + await self.handle_attendance_confirm(user, message_id, request_id, reaction.message) elif emoji_str == CROSS_EMOJI: if handler_type == 'one_on_one': - await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message) + if self.flask_app: + with self.flask_app.app_context(): + await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message) + else: + await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message) elif handler_type == 'schedule_addition': - await self.handle_attendance_decline(user, message_id, request_id, reaction.message) + if self.flask_app: + with self.flask_app.app_context(): + await self.handle_attendance_decline(user, message_id, request_id, reaction.message) + else: + await self.handle_attendance_decline(user, message_id, request_id, reaction.message) async def _send_one_on_one_dm(self, coach_name: str, coach_discord_id: str, player_name: str, team_name: str, date_str: str, start_time: str, end_time: str, @@ -188,7 +217,7 @@ class TeamTryoutsBot(commands.Bot): """ try: # Look up the DB user to get their Discord user ID - from app.models.models import User as DBUser + from app.models import User as DBUser db_user = DBUser.query.get(user_id) if not db_user: logger.warning(f"DB user {user_id} not found for schedule notification") @@ -234,13 +263,10 @@ class TeamTryoutsBot(commands.Bot): async def handle_one_on_one_approve(self, coach, message_id, request_id, original_message): """Handle coach approving a One on One request.""" try: - from app.models.models import OneOnOneRequest, db - from sqlalchemy.orm import joinedload + from app.models import OneOnOneRequest + from app.extensions import db - request = OneOnOneRequest.query.options( - joinedload(OneOnOneRequest.player), - joinedload(OneOnOneRequest.coach) - ).get(request_id) + request = OneOnOneRequest.query.get(request_id) if not request: return @@ -248,11 +274,13 @@ class TeamTryoutsBot(commands.Bot): await original_message.channel.send("⚠️ You are not the intended recipient.") return - # Capture data before commit (to avoid expired session issues) - player = request.player + # Re-attach to current session (object may be detached across app contexts) + request = db.session.merge(request) + + # Capture data before commit + player_full_name = request.player.full_name if request.player else 'Unknown' + player_discord_id = request.player.discord_user_id if request.player else None coach_obj = request.coach - player_full_name = player.full_name - player_discord_id = player.discord_user_id request.status = 'approved' request.responded_at = datetime.utcnow() @@ -262,29 +290,27 @@ class TeamTryoutsBot(commands.Bot): f"✅ You have **approved** the One on One session with {player_full_name}." ) - # Pass the pre-fetched data to avoid session expiration issues - await self.notify_player_about_one_on_one_direct( - player_discord_id=player_discord_id, - player_full_name=player_full_name, - coach_full_name=coach_obj.full_name, - request=request, - approved=True - ) + # Notify player via Discord + if player_discord_id: + await self.notify_player_about_one_on_one_direct( + player_discord_id=player_discord_id, + player_full_name=player_full_name, + coach_full_name=coach_obj.full_name if coach_obj else 'Coach', + request=request, + approved=True + ) del self.pending_requests[message_id] except Exception as e: - logger.error(f"Error handling approval: {e}") + logger.error(f"Error handling approval: {e}\n{traceback.format_exc()}") async def handle_one_on_one_reject(self, coach, message_id, request_id, original_message): """Handle coach rejecting a One on One request.""" try: - from app.models.models import OneOnOneRequest, db - from sqlalchemy.orm import joinedload + from app.models import OneOnOneRequest + from app.extensions import db - request = OneOnOneRequest.query.options( - joinedload(OneOnOneRequest.player), - joinedload(OneOnOneRequest.coach) - ).get(request_id) + request = OneOnOneRequest.query.get(request_id) if not request: return @@ -292,11 +318,12 @@ class TeamTryoutsBot(commands.Bot): await original_message.channel.send("⚠️ You are not the intended recipient.") return - # Capture data before commit (to avoid expired session issues) - player = request.player + # Re-attach to current session (object may be detached across app contexts) + request = db.session.merge(request) + + player_full_name = request.player.full_name if request.player else 'Unknown' + player_discord_id = request.player.discord_user_id if request.player else None coach_obj = request.coach - player_full_name = player.full_name - player_discord_id = player.discord_user_id refusal_note = None try: @@ -320,23 +347,25 @@ class TeamTryoutsBot(commands.Bot): rejection_msg += "\n\nℹ️ The player has been notified that you are not available." await original_message.channel.send(rejection_msg) - await self.notify_player_about_one_on_one_direct( - player_discord_id=player_discord_id, - player_full_name=player_full_name, - coach_full_name=coach_obj.full_name, - request=request, - approved=False, - refusal_note=refusal_note - ) + if player_discord_id: + await self.notify_player_about_one_on_one_direct( + player_discord_id=player_discord_id, + player_full_name=player_full_name, + coach_full_name=coach_obj.full_name if coach_obj else 'Coach', + request=request, + approved=False, + refusal_note=refusal_note + ) del self.pending_requests[message_id] except Exception as e: - logger.error(f"Error handling rejection: {e}") + logger.error(f"Error handling rejection: {e}\n{traceback.format_exc()}") async def handle_attendance_confirm(self, player, message_id, reference_id, original_message): """Handle player confirming attendance for a match/tryout.""" try: - from app.models.models import MatchParticipant, TryoutRegistration, Match, Tryout, db + from app.models import MatchParticipant, TryoutRegistration, Match, Tryout + from app.extensions import db request_info = self.pending_requests[message_id] event_type = request_info.get('event_type') @@ -344,10 +373,12 @@ class TeamTryoutsBot(commands.Bot): if event_type == 'match': participant = MatchParticipant.query.get(reference_id) if participant: + participant = db.session.merge(participant) participant.attendance_confirmed = True elif event_type == 'tryout': registration = TryoutRegistration.query.get(reference_id) if registration: + registration = db.session.merge(registration) registration.attendance_confirmed = True db.session.commit() @@ -356,12 +387,13 @@ class TeamTryoutsBot(commands.Bot): del self.pending_requests[message_id] except Exception as e: - logger.error(f"Error handling attendance confirmation: {e}") + logger.error(f"Error handling attendance confirmation: {e}\n{traceback.format_exc()}") async def handle_attendance_decline(self, player, message_id, reference_id, original_message): """Handle player declining attendance for a match/tryout.""" try: - from app.models.models import MatchParticipant, TryoutRegistration, Match, Tryout, db + from app.models import MatchParticipant, TryoutRegistration, Match, Tryout + from app.extensions import db request_info = self.pending_requests[message_id] event_type = request_info.get('event_type') @@ -369,10 +401,12 @@ class TeamTryoutsBot(commands.Bot): if event_type == 'match': participant = MatchParticipant.query.get(reference_id) if participant: + participant = db.session.merge(participant) db.session.delete(participant) elif event_type == 'tryout': registration = TryoutRegistration.query.get(reference_id) if registration: + registration = db.session.merge(registration) registration.status = 'no_show' db.session.commit() @@ -381,7 +415,7 @@ class TeamTryoutsBot(commands.Bot): del self.pending_requests[message_id] except Exception as e: - logger.error(f"Error handling attendance decline: {e}") + logger.error(f"Error handling attendance decline: {e}\n{traceback.format_exc()}") async def notify_player_about_one_on_one(self, request, approved=True, refusal_note=None): """Send confirmation to player about One on One response. @@ -471,7 +505,19 @@ class TeamTryoutsBot(commands.Bot): async def send_daily_reminders(self): """Send daily reminders at 18:00 EDT for events in 24-48 hours.""" try: - from app.models.models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest, db + if self.flask_app: + with self.flask_app.app_context(): + await self._send_daily_reminders_impl() + else: + await self._send_daily_reminders_impl() + except Exception as e: + logger.error(f"Error sending daily reminders: {e}\n{traceback.format_exc()}") + + async def _send_daily_reminders_impl(self): + """Internal implementation of daily reminders with proper app context.""" + try: + from app.models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest + from app.extensions import db from sqlalchemy.orm import joinedload now = datetime.now(self.timezone) @@ -540,6 +586,56 @@ class TeamTryoutsBot(commands.Bot): except Exception as e: logger.error(f"Error sending tryout reminder: {e}") + async def _send_one_on_one_response_dm(self, player_discord_id: str, player_full_name: str, + coach_full_name: str, date_str: str, start_time: str, + end_time: str, points: str, approved: bool, + refusal_note: str = None) -> bool: + """Send a DM to a player notifying them of their One on One request response. + + Called from the message queue when a coach accepts/rejects via the web app. + """ + try: + if not player_discord_id: + logger.warning("Cannot send response DM: no player_discord_id") + return False + + player_user = await self.fetch_user(int(player_discord_id)) + if not player_user: + logger.warning(f"Could not fetch Discord user {player_discord_id}") + return False + + if approved: + message = ( + "🎉 **One on One Session Confirmed!**\n\n" + f"Your coach **{coach_full_name}** has approved your request:\n" + f"**Date:** {date_str}\n" + f"**Time:** {start_time} - {end_time}\n" + f"**Discussion Points:** {points or 'No specific points provided'}\n\n" + "Please prepare for your session!" + ) + else: + if refusal_note: + message = ( + "😞 **One on One Session Rejected**\n\n" + f"Your coach **{coach_full_name}** has declined:\n" + f"**Reason:** {refusal_note}\n\n" + "Please try selecting a different time slot." + ) + else: + message = ( + "😞 **One on One Session Unavailable**\n\n" + f"Your coach **{coach_full_name}** is not available.\n\n" + "Please try selecting a different time slot." + ) + + await player_user.send(message) + logger.info(f"Sent One on One response DM to player {player_full_name} (approved={approved})") + return True + + except Exception as e: + logger.error(f"Error sending One on One response DM: {e}") + return False + async def send_one_on_one_reminder(self, player, session): """Send One on One reminder to player.""" try: @@ -562,11 +658,13 @@ bot_instance = None bot_thread = None -def get_bot(): +def get_bot(flask_app=None): """Get or create the bot instance.""" global bot_instance if bot_instance is None: - bot_instance = TeamTryoutsBot() + bot_instance = TeamTryoutsBot(flask_app=flask_app) + elif flask_app is not None and bot_instance.flask_app is None: + bot_instance.flask_app = flask_app return bot_instance @@ -618,11 +716,41 @@ def send_schedule_notification(user_id: int, event_type: str, event_title: str, return False -def start_bot(): +def send_one_on_one_response(player_discord_id: str, player_full_name: str, + coach_full_name: str, date_str: str, start_time: str, + end_time: str, points: str, approved: bool, + refusal_note: str = None) -> bool: + """Queue a One on One response DM to be sent to the player by the bot. + + Called from Flask routes when a coach accepts/rejects via the web app. + """ + bot = get_bot() + try: + bot.message_queue.put({ + 'type': 'one_on_one_response', + 'data': { + 'player_discord_id': player_discord_id, + 'player_full_name': player_full_name, + 'coach_full_name': coach_full_name, + 'date_str': date_str, + 'start_time': start_time, + 'end_time': end_time, + 'points': points, + 'approved': approved, + 'refusal_note': refusal_note, + } + }) + return True + except Exception as e: + logger.error(f"Error queuing One on One response DM: {e}") + return False + + +def start_bot(flask_app=None): """Start the Discord bot in the background.""" global bot_thread - bot = get_bot() + bot = get_bot(flask_app=flask_app) if DISCORD_BOT_TOKEN and bot_thread is None: def run_bot(): try: diff --git a/app/routes/matches.py b/app/routes/matches.py index 2f8f81e..649183f 100644 --- a/app/routes/matches.py +++ b/app/routes/matches.py @@ -10,6 +10,7 @@ from app.models import ( Admin, Manager, Coach, Player, Scout, User, Tryout, Match, MatchParticipant, Team, TeamMember, OrgTeam, TryoutRegistration, PlayerDisponibility, + OneOnOneRequest, ) from datetime import datetime, time, timedelta from app.discord_bot import send_schedule_notification @@ -101,6 +102,37 @@ def api_events(): }, }) + # Add approved One on One sessions for the current user (player or coach) + if isinstance(current_user, Player): + one_on_ones = OneOnOneRequest.query.filter_by( + player_id=current_user.id, + status='approved' + ).all() + elif isinstance(current_user, Coach): + one_on_ones = OneOnOneRequest.query.filter_by( + coach_id=current_user.id, + status='approved' + ).all() + else: + one_on_ones = [] + + for ooo in one_on_ones: + events.append({ + 'id': f'one_on_one_{ooo.id}', + 'title': f'1:1 - {ooo.player.full_name} & {ooo.coach.full_name}', + 'date': ooo.date.strftime('%Y-%m-%d'), + 'type': 'one_on_one', + 'color': '#8b5cf6', + 'extendedProps': { + 'location': 'Discord / Voice Chat', + 'status': 'approved', + 'description': ooo.points or 'One on One session', + 'start_time': ooo.start_time.strftime('%H:%M') if ooo.start_time else None, + 'end_time': ooo.end_time.strftime('%H:%M') if ooo.end_time else None, + 'participants': f"{ooo.player.full_name} with {ooo.coach.full_name}", + }, + }) + return jsonify(events) diff --git a/app/routes/users.py b/app/routes/users.py index d94e29c..9a18a3e 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -788,11 +788,104 @@ def one_on_one(): '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) + 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 = 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(f'One on One request from {player.username if player else "Unknown"} has been approved!', '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 = 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(f'One on One request from {player.username if player else "Unknown"} has been rejected.', 'info') + return redirect(url_for('users.notes_dashboard')) # --------------------------------------------------------------------------- @@ -920,6 +1013,14 @@ def notes_dashboard(): coach_id=current_user.id, ).order_by(PersonalNote.created_at.desc()).all() + # One on One requests from team players + one_on_one_requests = [] + if org_team and players: + player_ids_list = [p.id for p in players] + one_on_one_requests = OneOnOneRequest.query.filter( + OneOnOneRequest.player_id.in_(player_ids_list) + ).order_by(OneOnOneRequest.created_at.desc()).all() + # For context selectors in the form from app.models import Team as MatchTeam matches = Match.query.filter( @@ -936,6 +1037,7 @@ def notes_dashboard(): team_notes=team_notes, latest_team_note=latest_team_note, personal_notes=personal_notes, + one_on_one_requests=one_on_one_requests, matches=matches, tryouts=tryouts, teams=teams) diff --git a/app/templates/pages/notes.html b/app/templates/pages/notes.html index 4237bff..75146de 100644 --- a/app/templates/pages/notes.html +++ b/app/templates/pages/notes.html @@ -105,6 +105,94 @@ + +
+
+

One on One Requests

+ {% if org_team %} + {{ org_team.name }} + {% endif %} +
+
+ {% if one_on_one_requests %} +
+ + + + + + + + + + + + + {% for req in one_on_one_requests %} + + + + + + + + + {% endfor %} + +
PlayerDateTimeDiscussion PointsStatusActions
{{ req.player.username if req.player else 'Unknown' }}{{ req.date.strftime('%b %d, %Y') }}{{ req.start_time.strftime('%I:%M %p') }} - {{ req.end_time.strftime('%I:%M %p') }}{{ req.points or 'N/A' }} + {% if req.status == 'pending' %} + Pending + {% elif req.status == 'approved' %} + Approved + {% elif req.status == 'rejected' %} + Rejected + {% endif %} + + {% if req.status == 'pending' %} +
+ + +
+ + {% elif req.status == 'rejected' and req.coach_rejection_message %} + Reason: {{ req.coach_rejection_message[:50] }}{% if req.coach_rejection_message|length > 50 %}...{% endif %} + {% endif %} +
+
+ {% else %} +

No One on One requests from your players yet.

+ {% endif %} +
+
+ + + +
{% if org_team and team_notes %} @@ -169,4 +257,20 @@
{% endif %} +{% endblock %} + +{% block scripts %} + {% endblock %} \ No newline at end of file diff --git a/app/templates/pages/one_on_one.html b/app/templates/pages/one_on_one.html index 9fdff6e..177c3ba 100644 --- a/app/templates/pages/one_on_one.html +++ b/app/templates/pages/one_on_one.html @@ -54,7 +54,66 @@ {% endif %} + + +
+
+

My One on One Requests

+
+
+ {% if my_requests %} +
+ + + + + + + + + + + + {% for req in my_requests %} + + + + + + + + {% endfor %} + +
DateTimeDiscussion PointsStatusCoach Response
{{ req.date.strftime('%b %d, %Y') }}{{ req.start_time.strftime('%I:%M %p') }} - {{ req.end_time.strftime('%I:%M %p') }}{{ req.points or 'N/A' }} + {% if req.status == 'pending' %} + Pending + {% elif req.status == 'approved' %} + Approved + {% elif req.status == 'rejected' %} + Rejected + {% endif %} + + {% if req.status == 'approved' %} + Session confirmed! + {% elif req.status == 'rejected' %} + {% if req.coach_rejection_message %} + {{ req.coach_rejection_message }} + {% else %} + Coach is unavailable + {% endif %} + {% else %} + Awaiting coach response... + {% endif %} +
+
+ {% else %} +

You haven't made any One on One requests yet.

+ {% endif %} +
+
+ +