Ajout d'un profil public cliquable pour les utilisateurs déplacement du profil
806 lines
34 KiB
Python
806 lines
34 KiB
Python
"""Match scheduling routes for managing scrimmages and matches within tryouts.
|
|
|
|
This module handles calendar views, match creation, and player availability.
|
|
"""
|
|
|
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
|
from flask_login import login_required, current_user
|
|
from extensions import db
|
|
from models import User, Tryout, Match, MatchParticipant, Team, TeamMember, OrgTeam, TryoutRegistration, PlayerDisponibility
|
|
from datetime import datetime, time, timedelta
|
|
from discord_bot import send_schedule_notification
|
|
|
|
matches_bp = Blueprint('matches', __name__, url_prefix='/matches')
|
|
|
|
|
|
def can_schedule_match():
|
|
"""Check if user can schedule matches (coaches and above).
|
|
|
|
Returns:
|
|
bool: True if user is president, manager, coach, or scout.
|
|
"""
|
|
return current_user.role in ['president', 'manager', 'coach', 'scout']
|
|
|
|
|
|
@matches_bp.route('/calendar')
|
|
@login_required
|
|
def calendar():
|
|
"""Render the calendar view showing all tryouts and matches.
|
|
|
|
Returns:
|
|
Response: Rendered calendar template.
|
|
"""
|
|
return render_template('pages/calendar.html')
|
|
|
|
|
|
@matches_bp.route('/api/events')
|
|
@login_required
|
|
def api_events():
|
|
"""API endpoint returning calendar events for FullCalendar.
|
|
|
|
Returns tryout events and match events with participant information.
|
|
|
|
Returns:
|
|
Response: JSON array of calendar events.
|
|
"""
|
|
events = []
|
|
|
|
# Get tryouts based on user permissions (this already filters by user's role)
|
|
tryouts = get_visible_tryouts_for_user()
|
|
|
|
for tryout in tryouts:
|
|
events.append({
|
|
'id': f'tryout_{tryout.id}',
|
|
'title': tryout.title,
|
|
'date': tryout.date.strftime('%Y-%m-%d'),
|
|
'type': 'tryout',
|
|
'color': '#3b82f6', # Blue for tryouts
|
|
'extendedProps': {
|
|
'location': tryout.location or 'TBD',
|
|
'status': tryout.status,
|
|
'description': tryout.description or '',
|
|
'tryout_id': tryout.id
|
|
}
|
|
})
|
|
|
|
# Add matches for this tryout
|
|
for match in tryout.matches:
|
|
match_color = '#10b981' if match.match_type == 'team_vs_team' else '#f59e0b'
|
|
|
|
# Build match description with participants
|
|
match_desc = match.description or ''
|
|
participants_str = ''
|
|
if match.match_type == 'team_vs_team':
|
|
teams = []
|
|
if match.team1:
|
|
teams.append(match.team1.name)
|
|
if match.team2:
|
|
teams.append(match.team2.name)
|
|
participants_str = f"{' vs '.join(teams)}"
|
|
match_desc = participants_str + (f"<br>{match.description}" if match.description else '')
|
|
else:
|
|
# Player scrim - show all participants
|
|
player_names = []
|
|
for p in match.participants.all():
|
|
player_name = p.player.username if p.player else 'Unknown Player'
|
|
player_names.append(player_name)
|
|
participants_str = ', '.join(player_names) if player_names else 'No players'
|
|
match_desc = participants_str + (f"<br>{match.description}" if match.description else '')
|
|
|
|
# Include time for calendar display
|
|
start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None
|
|
end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None
|
|
|
|
# Find current user's participant record for presence toggle
|
|
user_participant = MatchParticipant.query.filter_by(
|
|
match_id=match.id,
|
|
player_id=current_user.id
|
|
).first()
|
|
|
|
events.append({
|
|
'id': f'match_{match.id}',
|
|
'title': match.title,
|
|
'date': match.date.strftime('%Y-%m-%d'),
|
|
'type': 'match',
|
|
'color': match_color,
|
|
'extendedProps': {
|
|
'location': match.location or tryout.location or 'TBD',
|
|
'status': match.status,
|
|
'description': match_desc,
|
|
'match_type': match.match_type,
|
|
'tryout_id': tryout.id,
|
|
'match_id': match.id,
|
|
'start_time': start_time_str,
|
|
'end_time': end_time_str,
|
|
'participants': participants_str,
|
|
'user_participant_id': user_participant.id if user_participant else None,
|
|
'user_attendance_confirmed': user_participant.attendance_confirmed if user_participant else False
|
|
}
|
|
})
|
|
|
|
return jsonify(events)
|
|
|
|
|
|
@matches_bp.route('/api/events/<int:tryout_id>')
|
|
@login_required
|
|
def api_events_for_tryout(tryout_id):
|
|
"""API endpoint returning calendar events for a specific tryout.
|
|
|
|
Args:
|
|
tryout_id: The ID of the tryout to get events for.
|
|
|
|
Returns:
|
|
Response: JSON array of calendar events for the tryout.
|
|
"""
|
|
tryout = Tryout.query.get_or_404(tryout_id)
|
|
|
|
# Check if user can view this tryout
|
|
can_view = current_user.can_manage_this_tryout(tryout)
|
|
|
|
# For players, check if they're registered or participating in a match
|
|
is_registered = False
|
|
player_in_match = False
|
|
if current_user.role == 'player':
|
|
is_registered = TryoutRegistration.query.filter_by(
|
|
tryout_id=tryout_id, player_id=current_user.id
|
|
).first() is not None
|
|
|
|
# Check if player is participating in any matches for this tryout
|
|
player_matches = Match.query.join(MatchParticipant).filter(
|
|
MatchParticipant.player_id == current_user.id,
|
|
Match.tryout_id == tryout_id
|
|
).all()
|
|
player_in_match = len(player_matches) > 0
|
|
|
|
# Non-participating players cannot see the calendar
|
|
if not can_view and not is_registered and not player_in_match:
|
|
return jsonify([])
|
|
|
|
events = []
|
|
|
|
# Add tryout date as an event (read-only, for context)
|
|
events.append({
|
|
'id': f'tryout_{tryout.id}',
|
|
'title': f'Tryout: {tryout.title}',
|
|
'date': tryout.date.strftime('%Y-%m-%d'),
|
|
'type': 'tryout',
|
|
'color': '#3b82f6', # Blue for tryouts
|
|
'extendedProps': {
|
|
'location': tryout.location or 'TBD',
|
|
'status': tryout.status,
|
|
'description': tryout.description or '',
|
|
'tryout_id': tryout.id
|
|
}
|
|
})
|
|
|
|
for match in tryout.matches:
|
|
# Determine color based on match type
|
|
if match.match_type == 'team_vs_team' or match.match_type == 'player_vs_player':
|
|
match_color = '#10b981' # Green for team matches
|
|
else:
|
|
match_color = '#f59e0b' # Orange for scrims
|
|
|
|
# Build participant string with proper grouping
|
|
participants_str = ''
|
|
if match.match_type == 'team_vs_team':
|
|
teams = []
|
|
if match.team1:
|
|
teams.append(match.team1.name)
|
|
if match.team2:
|
|
teams.append(match.team2.name)
|
|
participants_str = f"{' vs '.join(teams)}"
|
|
elif match.match_type == 'player_vs_player':
|
|
# Get players grouped by team side
|
|
team1_players = []
|
|
for p in match.participants.filter_by(team_side=1).all():
|
|
if p.player:
|
|
team1_players.append(p.player.username)
|
|
team2_players = []
|
|
for p in match.participants.filter_by(team_side=2).all():
|
|
if p.player:
|
|
team2_players.append(p.player.username)
|
|
if team1_players and team2_players:
|
|
participants_str = f"{', '.join(team1_players)} vs {', '.join(team2_players)}"
|
|
else:
|
|
participants_str = 'TBD vs TBD'
|
|
else:
|
|
player_names = []
|
|
for p in match.participants.all():
|
|
player_name = p.player.username if p.player else 'Unknown Player'
|
|
player_names.append(player_name)
|
|
participants_str = ', '.join(player_names) if player_names else 'No players'
|
|
|
|
# Include time for calendar display
|
|
start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None
|
|
end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None
|
|
|
|
events.append({
|
|
'id': f'match_{match.id}',
|
|
'title': match.title,
|
|
'date': match.date.strftime('%Y-%m-%d'),
|
|
'type': 'match',
|
|
'color': match_color,
|
|
'extendedProps': {
|
|
'location': match.location or tryout.location or 'TBD',
|
|
'status': match.status,
|
|
'match_type': match.match_type,
|
|
'tryout_id': tryout.id,
|
|
'match_id': match.id,
|
|
'participants': participants_str,
|
|
'start_time': start_time_str,
|
|
'end_time': end_time_str
|
|
}
|
|
})
|
|
|
|
return jsonify(events)
|
|
|
|
|
|
def get_visible_tryouts_for_user():
|
|
"""Get tryouts that the current user can see based on their role.
|
|
|
|
Permission hierarchy:
|
|
- President: All tryouts
|
|
- Manager: Only their created tryouts
|
|
- Coach: Tryouts targeting their org team
|
|
- Player: Tryouts they're registered for or matches they're in
|
|
- Scout: All tryouts
|
|
|
|
Returns:
|
|
list: Query result of Tryout objects.
|
|
"""
|
|
if current_user.role == 'president':
|
|
return Tryout.query.order_by(Tryout.date).all()
|
|
elif current_user.role == 'manager':
|
|
return Tryout.query.filter_by(created_by=current_user.id).order_by(Tryout.date).all()
|
|
elif current_user.role == 'coach':
|
|
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
|
|
if org_team:
|
|
return Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date).all()
|
|
return []
|
|
elif current_user.role == 'player':
|
|
# Get tryouts player is registered for
|
|
player_tryout_ids = [r.tryout_id for r in current_user.tryout_registrations.all()]
|
|
tryouts = Tryout.query.filter(Tryout.id.in_(player_tryout_ids)).order_by(Tryout.date).all() if player_tryout_ids else []
|
|
|
|
# Also include matches where player is participating
|
|
player_matches = Match.query.join(MatchParticipant).filter(
|
|
MatchParticipant.player_id == current_user.id
|
|
).all()
|
|
|
|
player_match_tryout_ids = list(set(m.tryout_id for m in player_matches))
|
|
additional_tryouts = Tryout.query.filter(
|
|
Tryout.id.in_(player_match_tryout_ids)
|
|
).order_by(Tryout.date).all() if player_match_tryout_ids else []
|
|
|
|
# Combine and deduplicate
|
|
all_tryouts = tryouts + [t for t in additional_tryouts if t.id not in player_tryout_ids]
|
|
return all_tryouts
|
|
else: # scout
|
|
return Tryout.query.order_by(Tryout.date).all()
|
|
|
|
|
|
@matches_bp.route('/create/<int:tryout_id>', methods=['GET', 'POST'])
|
|
@login_required
|
|
def create_match(tryout_id):
|
|
"""Create a new match/scrimmage within a tryout.
|
|
|
|
GET: Render the match creation form.
|
|
POST: Create a match with submitted details.
|
|
|
|
Args:
|
|
tryout_id: The ID of the tryout to create the match for.
|
|
|
|
Returns:
|
|
Response: Create form or redirect to tryout view.
|
|
"""
|
|
tryout = Tryout.query.get_or_404(tryout_id)
|
|
|
|
# Check if user can manage this tryout (president, manager, or coach)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash('You do not have permission to schedule matches for this tryout.', 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
teams = Team.query.filter_by(tryout_id=tryout_id).all()
|
|
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
|
|
all_players = [User.query.get(r.player_id) for r in registrations if User.query.get(r.player_id)]
|
|
all_players = sorted([p for p in all_players if p], key=lambda x: x.username)
|
|
|
|
# Allow pre-filling the date from query param (e.g., from calendar click)
|
|
prefill_date = request.args.get('date', '')
|
|
|
|
if request.method == 'POST':
|
|
title = request.form.get('title')
|
|
description = request.form.get('description')
|
|
date_str = request.form.get('date')
|
|
start_time_str = request.form.get('start_time')
|
|
end_time_str = request.form.get('end_time')
|
|
location = request.form.get('location')
|
|
match_type = request.form.get('match_type')
|
|
|
|
# Start time is now mandatory
|
|
if not start_time_str:
|
|
flash('Start time is required. Please select a time slot.', 'danger')
|
|
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players, prefill_date=prefill_date)
|
|
|
|
try:
|
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else tryout.date
|
|
except (ValueError, TypeError):
|
|
flash('Invalid date format.', 'danger')
|
|
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players, prefill_date=prefill_date)
|
|
|
|
start_time = None
|
|
end_time = None
|
|
try:
|
|
start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
|
# Auto-calculate end time if not provided (start + 30 minutes)
|
|
if end_time_str:
|
|
end_time = datetime.strptime(end_time_str, '%H:%M').time()
|
|
else:
|
|
# Auto-calculate end time as start + 30 minutes
|
|
start_dt = datetime.combine(date_obj, start_time)
|
|
end_dt = start_dt + timedelta(minutes=30)
|
|
end_time = end_dt.time()
|
|
except ValueError:
|
|
flash('Invalid time format.', 'danger')
|
|
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players)
|
|
|
|
match = Match(
|
|
tryout_id=tryout_id,
|
|
title=title,
|
|
description=description,
|
|
date=date_obj,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
location=location,
|
|
match_type=match_type,
|
|
created_by=current_user.id
|
|
)
|
|
db.session.add(match)
|
|
db.session.flush() # Get match.id before commit
|
|
|
|
# Collect player IDs for Discord notifications
|
|
notified_player_ids = []
|
|
|
|
# Handle team vs team matches
|
|
if match_type == 'team_vs_team':
|
|
team1_id = request.form.get('team1_id')
|
|
team2_id = request.form.get('team2_id')
|
|
match.team1_id = int(team1_id) if team1_id else None
|
|
match.team2_id = int(team2_id) if team2_id else None
|
|
# Create MatchParticipant records for all team members AND get notified player IDs
|
|
notified_participant_ids = []
|
|
if match.team1_id:
|
|
for m in TeamMember.query.filter_by(team_id=match.team1_id).all():
|
|
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=1)
|
|
db.session.add(participant)
|
|
db.session.flush()
|
|
notified_participant_ids.append(participant.id)
|
|
notified_player_ids.append(m.player_id)
|
|
if match.team2_id:
|
|
for m in TeamMember.query.filter_by(team_id=match.team2_id).all():
|
|
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=2)
|
|
db.session.add(participant)
|
|
db.session.flush()
|
|
notified_participant_ids.append(participant.id)
|
|
notified_player_ids.append(m.player_id)
|
|
|
|
# Handle player vs player matches
|
|
elif match_type == 'player_vs_player':
|
|
team1_player_ids = request.form.get('team1_player_ids', '')
|
|
team2_player_ids = request.form.get('team2_player_ids', '')
|
|
team1_ids = [int(p) for p in team1_player_ids.split(',') if p] if team1_player_ids else []
|
|
team2_ids = [int(p) for p in team2_player_ids.split(',') if p] if team2_player_ids else []
|
|
notified_participant_ids = []
|
|
for pid in team1_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=pid, team_side=1)
|
|
db.session.add(participant)
|
|
db.session.flush()
|
|
notified_participant_ids.append(participant.id)
|
|
for pid in team2_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=pid, team_side=2)
|
|
db.session.add(participant)
|
|
db.session.flush()
|
|
notified_participant_ids.append(participant.id)
|
|
notified_player_ids = team1_ids + team2_ids
|
|
|
|
# Handle player scrim matches
|
|
elif match_type == 'player_scrim':
|
|
player_ids = request.form.getlist('player_ids')
|
|
notified_participant_ids = []
|
|
for pid in player_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=int(pid))
|
|
db.session.add(participant)
|
|
db.session.flush()
|
|
notified_participant_ids.append(participant.id)
|
|
notified_player_ids = [int(p) for p in player_ids]
|
|
|
|
db.session.commit()
|
|
|
|
# Send Discord notifications to players (one per participant for proper attendance tracking)
|
|
event_date_str = date_obj.strftime('%Y-%m-%d')
|
|
event_time_str = f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}" if start_time and end_time else 'TBD'
|
|
|
|
# Send Discord notifications with proper participant reference IDs
|
|
if match_type == 'team_vs_team':
|
|
for i, player_id in enumerate(notified_player_ids):
|
|
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
|
|
send_schedule_notification(
|
|
user_id=player_id,
|
|
event_type='match',
|
|
event_title=match.title,
|
|
event_date=event_date_str,
|
|
event_time=event_time_str,
|
|
reference_id=reference_id
|
|
)
|
|
else:
|
|
for i, player_id in enumerate(notified_player_ids):
|
|
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
|
|
send_schedule_notification(
|
|
user_id=player_id,
|
|
event_type='match',
|
|
event_title=match.title,
|
|
event_date=event_date_str,
|
|
event_time=event_time_str,
|
|
reference_id=reference_id
|
|
)
|
|
|
|
flash('Match scheduled successfully!', 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players)
|
|
|
|
|
|
@matches_bp.route('/<int:match_id>/edit', methods=['GET', 'POST'])
|
|
@login_required
|
|
def edit_match(match_id):
|
|
"""Edit an existing match.
|
|
|
|
GET: Render the match edit form.
|
|
POST: Update match with submitted changes.
|
|
|
|
Args:
|
|
match_id: The ID of the match to edit.
|
|
|
|
Returns:
|
|
Response: Edit form or redirect to tryout view.
|
|
"""
|
|
match = Match.query.get_or_404(match_id)
|
|
tryout = match.tryout
|
|
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash('You do not have permission to edit this match.', 'danger')
|
|
return redirect(url_for('matches.calendar'))
|
|
|
|
teams = Team.query.filter_by(tryout_id=tryout.id).all()
|
|
# Only show players registered for this tryout
|
|
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout.id).all()
|
|
all_players = [User.query.get(r.player_id) for r in registrations if r.player_id]
|
|
all_players = sorted([p for p in all_players if p], key=lambda x: x.username)
|
|
current_player_ids = [p.player_id for p in match.participants.all()]
|
|
# Get players grouped by team side for player_vs_player matches
|
|
team1_player_ids = [p.player_id for p in match.participants.filter_by(team_side=1).all()]
|
|
team2_player_ids = [p.player_id for p in match.participants.filter_by(team_side=2).all()]
|
|
|
|
if request.method == 'POST':
|
|
match.title = request.form.get('title')
|
|
match.description = request.form.get('description')
|
|
date_str = request.form.get('date')
|
|
start_time_str = request.form.get('start_time')
|
|
end_time_str = request.form.get('end_time')
|
|
location = request.form.get('location')
|
|
status = request.form.get('status')
|
|
|
|
try:
|
|
match.date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
|
except (ValueError, TypeError):
|
|
flash('Invalid date format.', 'danger')
|
|
return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids)
|
|
|
|
# Start time is now mandatory
|
|
if not start_time_str:
|
|
flash('Start time is required. Please select a time slot.', 'danger')
|
|
return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids)
|
|
|
|
try:
|
|
match.start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
|
# Auto-calculate end time if not provided (start + 30 minutes)
|
|
if end_time_str:
|
|
match.end_time = datetime.strptime(end_time_str, '%H:%M').time()
|
|
else:
|
|
# Auto-calculate end time as start + 30 minutes
|
|
start_dt = datetime.combine(match.date, match.start_time)
|
|
end_dt = start_dt + timedelta(minutes=30)
|
|
match.end_time = end_dt.time()
|
|
except ValueError:
|
|
match.start_time = None
|
|
|
|
match.location = location
|
|
if status in ['scheduled', 'completed', 'cancelled']:
|
|
match.status = status
|
|
|
|
# Collect player IDs for Discord notifications
|
|
notified_player_ids = []
|
|
|
|
# Handle team vs team matches
|
|
notified_participant_ids = []
|
|
if match.match_type == 'team_vs_team':
|
|
team1_id = request.form.get('team1_id')
|
|
team2_id = request.form.get('team2_id')
|
|
new_team1_id = int(team1_id) if team1_id else None
|
|
new_team2_id = int(team2_id) if team2_id else None
|
|
|
|
# If teams changed, recreate MatchParticipant records
|
|
if new_team1_id != match.team1_id or new_team2_id != match.team2_id:
|
|
MatchParticipant.query.filter_by(match_id=match.id).delete()
|
|
match.team1_id = new_team1_id
|
|
match.team2_id = new_team2_id
|
|
if match.team1_id:
|
|
for m in TeamMember.query.filter_by(team_id=match.team1_id).all():
|
|
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=1)
|
|
db.session.add(participant)
|
|
db.session.flush()
|
|
notified_participant_ids.append(participant.id)
|
|
notified_player_ids.append(m.player_id)
|
|
if match.team2_id:
|
|
for m in TeamMember.query.filter_by(team_id=match.team2_id).all():
|
|
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=2)
|
|
db.session.add(participant)
|
|
db.session.flush()
|
|
notified_participant_ids.append(participant.id)
|
|
notified_player_ids.append(m.player_id)
|
|
else:
|
|
# Teams didn't change, still get notified player IDs
|
|
if match.team1_id:
|
|
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team1_id).all()])
|
|
if match.team2_id:
|
|
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team2_id).all()])
|
|
|
|
# Handle player vs player matches - update participants
|
|
elif match.match_type == 'player_vs_player':
|
|
MatchParticipant.query.filter_by(match_id=match.id).delete()
|
|
team1_player_ids = request.form.getlist('team1_player_ids')
|
|
team2_player_ids = request.form.getlist('team2_player_ids')
|
|
notified_participant_ids = []
|
|
for pid in team1_player_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=1)
|
|
db.session.add(participant)
|
|
db.session.flush()
|
|
notified_participant_ids.append(participant.id)
|
|
for pid in team2_player_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=2)
|
|
db.session.add(participant)
|
|
db.session.flush()
|
|
notified_participant_ids.append(participant.id)
|
|
notified_player_ids = [int(p) for p in team1_player_ids] + [int(p) for p in team2_player_ids]
|
|
|
|
# Handle player scrim matches - update participants
|
|
elif match.match_type == 'player_scrim':
|
|
MatchParticipant.query.filter_by(match_id=match.id).delete()
|
|
player_ids = request.form.getlist('player_ids')
|
|
notified_participant_ids = []
|
|
for pid in player_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=int(pid))
|
|
db.session.add(participant)
|
|
db.session.flush()
|
|
notified_participant_ids.append(participant.id)
|
|
notified_player_ids = [int(p) for p in player_ids]
|
|
|
|
db.session.commit()
|
|
|
|
# Send Discord notifications to players
|
|
if match.match_type in ['player_vs_player', 'player_scrim']:
|
|
end_time_val = match.end_time if match.end_time else match.start_time if match.start_time else None
|
|
if match.start_time and end_time_val:
|
|
event_time_str = f"{match.start_time.strftime('%I:%M %p')} - {end_time_val.strftime('%I:%M %p')}"
|
|
else:
|
|
event_time_str = 'TBD'
|
|
event_date_str = match.date.strftime('%Y-%m-%d')
|
|
for i, player_id in enumerate(notified_player_ids):
|
|
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
|
|
send_schedule_notification(
|
|
user_id=player_id,
|
|
event_type='match',
|
|
event_title=match.title,
|
|
event_date=event_date_str,
|
|
event_time=event_time_str,
|
|
reference_id=reference_id
|
|
)
|
|
elif match.match_type == 'team_vs_team':
|
|
event_date_str = match.date.strftime('%Y-%m-%d')
|
|
end_time_val = match.end_time if match.end_time else match.start_time if match.start_time else None
|
|
event_time_str = f"{match.start_time.strftime('%I:%M %p')} - {end_time_val.strftime('%I:%M %p')}" if match.start_time and end_time_val else 'TBD'
|
|
if notified_participant_ids:
|
|
for i, player_id in enumerate(notified_player_ids):
|
|
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
|
|
send_schedule_notification(
|
|
user_id=player_id,
|
|
event_type='match',
|
|
event_title=match.title,
|
|
event_date=event_date_str,
|
|
event_time=event_time_str,
|
|
reference_id=reference_id
|
|
)
|
|
else:
|
|
for player_id in notified_player_ids:
|
|
send_schedule_notification(
|
|
user_id=player_id,
|
|
event_type='match',
|
|
event_title=match.title,
|
|
event_date=event_date_str,
|
|
event_time=event_time_str,
|
|
reference_id=match.id
|
|
)
|
|
|
|
flash('Match updated successfully!', 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
|
|
|
# Build participant attendance map for the template
|
|
participants_map = {}
|
|
for p in match.participants.all():
|
|
participants_map[p.player_id] = {
|
|
'participant_id': p.id,
|
|
'attendance_confirmed': p.attendance_confirmed,
|
|
'team_side': p.team_side
|
|
}
|
|
|
|
return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams,
|
|
all_players=all_players, current_player_ids=current_player_ids,
|
|
team1_player_ids=team1_player_ids, team2_player_ids=team2_player_ids,
|
|
participants_map=participants_map)
|
|
|
|
|
|
@matches_bp.route('/api/manageable-tryouts')
|
|
@login_required
|
|
def api_manageable_tryouts():
|
|
"""API endpoint returning tryouts the current user can manage.
|
|
|
|
Used by the calendar's "Create Event" modal to populate the tryout dropdown.
|
|
|
|
Returns:
|
|
Response: JSON array of {id, title, date}.
|
|
"""
|
|
if not can_schedule_match():
|
|
return jsonify([])
|
|
|
|
tryouts = get_visible_tryouts_for_user()
|
|
manageable = []
|
|
for t in tryouts:
|
|
if current_user.can_manage_this_tryout(t):
|
|
manageable.append({
|
|
'id': t.id,
|
|
'title': t.title,
|
|
'date': t.date.strftime('%Y-%m-%d')
|
|
})
|
|
return jsonify(manageable)
|
|
|
|
|
|
@matches_bp.route('/<int:match_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def delete_match(match_id):
|
|
"""Delete a match.
|
|
|
|
Args:
|
|
match_id: The ID of the match to delete.
|
|
|
|
Returns:
|
|
Response: Redirect to tryout view with status message.
|
|
"""
|
|
match = Match.query.get_or_404(match_id)
|
|
tryout = match.tryout
|
|
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash('You do not have permission to delete this match.', 'danger')
|
|
return redirect(url_for('matches.calendar'))
|
|
|
|
db.session.delete(match)
|
|
db.session.commit()
|
|
flash('Match deleted successfully.', 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
|
|
|
|
|
def get_players_available_at_time(date_str, time_str):
|
|
"""Get list of player IDs available at a specific date and time.
|
|
|
|
Checks player disponibility records to find who is available
|
|
during the specified time block.
|
|
|
|
Args:
|
|
date_str: Date in YYYY-MM-DD format.
|
|
time_str: Time in HH:MM format.
|
|
|
|
Returns:
|
|
list: List of available player IDs.
|
|
"""
|
|
try:
|
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
|
time_obj = datetime.strptime(time_str, '%H:%M').time()
|
|
except (ValueError, TypeError):
|
|
return []
|
|
|
|
# Calculate day of week (Python: 0=Monday, 6=Sunday)
|
|
# JavaScript: 0=Sunday, 6=Saturday, so we convert
|
|
date_parts = date_str.split('-')
|
|
date_for_day = datetime(int(date_parts[0]), int(date_parts[1]), int(date_parts[2]))
|
|
js_day = date_for_day.weekday()
|
|
|
|
# Convert Python weekday (Mon=0) to our format (Mon=0)
|
|
day_of_week = js_day
|
|
|
|
# Get all active players
|
|
players = User.query.filter_by(role='player', is_active_account=True).all()
|
|
|
|
available_players = []
|
|
for player in players:
|
|
# Check if player has disponibility at this time
|
|
disponibilities = PlayerDisponibility.query.filter_by(
|
|
player_id=player.id,
|
|
day_of_week=day_of_week
|
|
).all()
|
|
|
|
for disp in disponibilities:
|
|
# Check if time falls within disponibility block
|
|
disp_start = disp.start_time.hour * 60 + disp.start_time.minute
|
|
disp_end = disp.end_time.hour * 60 + disp.end_time.minute
|
|
match_time = time_obj.hour * 60 + time_obj.minute
|
|
|
|
if disp_start <= match_time < disp_end:
|
|
available_players.append(player.id)
|
|
break
|
|
|
|
return available_players
|
|
|
|
|
|
@matches_bp.route('/api/available_players/<date>/<time>')
|
|
@login_required
|
|
def api_available_players(date, time):
|
|
"""API endpoint to get players available at a specific date/time slot.
|
|
|
|
Args:
|
|
date: Date in YYYY-MM-DD format.
|
|
time: Time in HH:MM format.
|
|
|
|
Returns:
|
|
Response: JSON with list of available player IDs.
|
|
"""
|
|
if not current_user.can_manage_teams() and not current_user.can_schedule_matches():
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
player_ids = get_players_available_at_time(date, time)
|
|
return jsonify({'available_player_ids': player_ids})
|
|
|
|
|
|
@matches_bp.route('/<int:match_id>/toggle-presence/<int:participant_id>', methods=['POST'])
|
|
@login_required
|
|
def toggle_presence(match_id, participant_id):
|
|
"""Toggle the attendance_confirmed status for a match participant.
|
|
|
|
Accessible to tryout managers AND the participant themselves.
|
|
|
|
Args:
|
|
match_id: The ID of the match.
|
|
participant_id: The ID of the MatchParticipant record.
|
|
|
|
Returns:
|
|
Response: JSON with new status.
|
|
"""
|
|
match = Match.query.get_or_404(match_id)
|
|
tryout = match.tryout
|
|
|
|
participant = MatchParticipant.query.get_or_404(participant_id)
|
|
if participant.match_id != match_id:
|
|
return jsonify({'error': 'Participant does not belong to this match'}), 400
|
|
|
|
# Allow the participant themselves OR a tryout manager
|
|
is_self = participant.player_id == current_user.id
|
|
if not is_self and not current_user.can_manage_this_tryout(tryout):
|
|
return jsonify({'error': 'Unauthorized'}), 403
|
|
|
|
participant.attendance_confirmed = not participant.attendance_confirmed
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'participant_id': participant.id,
|
|
'attendance_confirmed': participant.attendance_confirmed,
|
|
'player_name': participant.player.username if participant.player else 'Unknown'
|
|
})
|