403 lines
17 KiB
Python
403 lines
17 KiB
Python
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
|
|
from datetime import datetime, time
|
|
|
|
matches_bp = Blueprint('matches', __name__, url_prefix='/matches')
|
|
|
|
|
|
def can_schedule_match():
|
|
"""Check if user can schedule matches (coaches and above)."""
|
|
return current_user.role in ['president', 'manager', 'coach', 'scout']
|
|
|
|
|
|
@matches_bp.route('/calendar')
|
|
@login_required
|
|
def calendar():
|
|
"""Calendar view showing tryouts and matches."""
|
|
return render_template('pages/calendar.html')
|
|
|
|
|
|
@matches_bp.route('/api/events')
|
|
@login_required
|
|
def api_events():
|
|
"""API endpoint returning calendar events."""
|
|
events = []
|
|
|
|
# Get tryouts based on user permissions
|
|
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 = [p.player.full_name for p in match.participants.all()]
|
|
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
|
|
|
|
events.append({
|
|
'id': f'match_{match.id}',
|
|
'title': match.title + ' (' + participants_str + ')',
|
|
'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
|
|
}
|
|
})
|
|
|
|
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."""
|
|
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 = [p.player.full_name for p in match.participants.filter_by(team_side=1).all()]
|
|
team2_players = [p.player.full_name for p in match.participants.filter_by(team_side=2).all()]
|
|
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 = [p.player.full_name for p in match.participants.all()]
|
|
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 + ' (' + participants_str + ')',
|
|
'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."""
|
|
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."""
|
|
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()
|
|
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
|
|
|
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')
|
|
|
|
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/create_match.html', tryout=tryout, teams=teams, all_players=all_players)
|
|
|
|
start_time = None
|
|
end_time = None
|
|
try:
|
|
if start_time_str:
|
|
start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
|
if end_time_str:
|
|
end_time = datetime.strptime(end_time_str, '%H:%M').time()
|
|
except ValueError:
|
|
flash('Invalid time format.', 'danger')
|
|
return render_template('pages/create_match.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
|
|
|
|
# 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
|
|
|
|
# Handle player vs player matches
|
|
elif match_type == 'player_vs_player':
|
|
team1_player_ids = request.form.getlist('team1_player_ids')
|
|
team2_player_ids = request.form.getlist('team2_player_ids')
|
|
for pid in team1_player_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=1)
|
|
db.session.add(participant)
|
|
for pid in team2_player_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=2)
|
|
db.session.add(participant)
|
|
|
|
# Handle player scrim matches
|
|
elif match_type == 'player_scrim':
|
|
player_ids = request.form.getlist('player_ids')
|
|
for pid in player_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=int(pid))
|
|
db.session.add(participant)
|
|
|
|
db.session.commit()
|
|
flash('Match scheduled successfully!', 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
return render_template('pages/create_match.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."""
|
|
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()
|
|
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
|
|
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/edit_match.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids)
|
|
|
|
try:
|
|
if start_time_str:
|
|
match.start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
|
if end_time_str:
|
|
match.end_time = datetime.strptime(end_time_str, '%H:%M').time()
|
|
except ValueError:
|
|
pass
|
|
|
|
match.location = location
|
|
if status in ['scheduled', 'completed', 'cancelled']:
|
|
match.status = status
|
|
|
|
# Handle team vs team matches
|
|
if match.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
|
|
|
|
# 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')
|
|
for pid in team1_player_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=1)
|
|
db.session.add(participant)
|
|
for pid in team2_player_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=int(pid), team_side=2)
|
|
db.session.add(participant)
|
|
|
|
# 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')
|
|
for pid in player_ids:
|
|
participant = MatchParticipant(match_id=match.id, player_id=int(pid))
|
|
db.session.add(participant)
|
|
|
|
db.session.commit()
|
|
flash('Match updated successfully!', 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
|
|
|
return render_template('pages/edit_match.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)
|
|
|
|
|
|
@matches_bp.route('/<int:match_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def delete_match(match_id):
|
|
"""Delete a match."""
|
|
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)) |