700 lines
26 KiB
Python
700 lines
26 KiB
Python
"""Tryout management routes for creating, viewing, and managing tryout events.
|
|
|
|
This module handles CRUD operations for tryouts and player registrations.
|
|
Uses polymorphic isinstance checks instead of role-string comparisons.
|
|
"""
|
|
|
|
from flask import Blueprint, abort, 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 sqlalchemy import select
|
|
|
|
from app.extensions import db
|
|
from app.forms import flash_validation_errors, form_payload
|
|
from app.models import (
|
|
ESPORT_GAMES,
|
|
GAME_POSITIONS,
|
|
Admin,
|
|
Coach,
|
|
Evaluation,
|
|
Manager,
|
|
Match,
|
|
MatchParticipant,
|
|
OrgTeam,
|
|
PersonalNote,
|
|
Player,
|
|
Scout,
|
|
Team,
|
|
TeamMember,
|
|
Tryout,
|
|
TryoutRegistration,
|
|
User,
|
|
)
|
|
from app.time_utils import utc_now_naive
|
|
from app.validators import (
|
|
PlayerSelectionSchema,
|
|
TryoutRegistrationStatusSchema,
|
|
TryoutSchema,
|
|
TryoutStatusSchema,
|
|
TryoutTeamMemberSchema,
|
|
TryoutTeamSchema,
|
|
)
|
|
|
|
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
|
|
|
|
|
|
def can_manage():
|
|
"""Check if current user can manage tryouts (Admin or Manager)."""
|
|
return isinstance(current_user, (Admin, Manager))
|
|
|
|
|
|
def tryout_form_payload():
|
|
"""The tryout form, shaped for marshmallow (ARCH-005)."""
|
|
return form_payload(list_fields=('coach_ids',), optional_blank=())
|
|
|
|
|
|
def coaches_from_ids(coach_ids):
|
|
"""The coach accounts behind these ids.
|
|
|
|
Filtered by role, which the previous `User.id.in_(...)` was not: the form
|
|
posts a list of ids and nothing stopped a hand-made submission from
|
|
naming a player, who then appeared as a coach of the tryout and inherited
|
|
every permission that comes with it.
|
|
"""
|
|
if not coach_ids:
|
|
return []
|
|
return User.query.filter(User.id.in_(coach_ids), User.role == 'coach').all()
|
|
|
|
|
|
def _users_by_id(user_ids):
|
|
"""Load these users in one query, keyed by id.
|
|
|
|
Replaces the `User.query.get()`-inside-a-loop that view_tryout used in
|
|
three separate places (PERF-001). Missing ids are simply absent from
|
|
the result, which is what a per-row get() returning None amounted to.
|
|
|
|
Args:
|
|
user_ids: Iterable of primary keys, may repeat and may be empty.
|
|
|
|
Returns:
|
|
dict[int, User]
|
|
"""
|
|
wanted = {user_id for user_id in user_ids if user_id}
|
|
if not wanted:
|
|
return {}
|
|
return {user.id: user for user in User.query.filter(User.id.in_(wanted)).all()}
|
|
|
|
|
|
def registration_lock_statement(tryout_id):
|
|
"""The PostgreSQL row lock used by both registration entry points."""
|
|
return select(Tryout).where(Tryout.id == tryout_id).with_for_update()
|
|
|
|
|
|
def locked_tryout_or_404(tryout_id):
|
|
"""Load and row-lock a tryout while a registration slot is decided.
|
|
|
|
PostgreSQL serializes concurrent registration attempts on this row. The
|
|
duplicate check, capacity count and insert that follow therefore form
|
|
one decision instead of three independently racing statements. SQLite
|
|
ignores ``FOR UPDATE`` in tests, but production does not.
|
|
"""
|
|
tryout = db.session.execute(registration_lock_statement(tryout_id)).scalar_one_or_none()
|
|
if tryout is None:
|
|
abort(404)
|
|
return tryout
|
|
|
|
|
|
@tryouts_bp.route('')
|
|
@login_required
|
|
def list_tryouts():
|
|
"""List all tryouts visible to the current user.
|
|
|
|
Delegates to the polymorphic User subclass's get_visible_tryouts() method.
|
|
"""
|
|
tryouts = current_user.get_visible_tryouts()
|
|
return render_template('pages/tryouts.html', tryouts=tryouts, now=utc_now_naive())
|
|
|
|
|
|
@tryouts_bp.route('/create', methods=['GET', 'POST'])
|
|
@login_required
|
|
def create_tryout():
|
|
"""Create a new tryout event. Requires Admin or Manager."""
|
|
if not can_manage():
|
|
flash(_('You do not have permission to create tryouts.'), 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
|
|
org_teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
|
managers = (
|
|
User.query.filter_by(role='manager', is_active_account=True).order_by(User.username).all()
|
|
)
|
|
coaches = (
|
|
User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all()
|
|
)
|
|
|
|
def rerender():
|
|
return render_template(
|
|
'pages/tryout_form.html',
|
|
tryout=None,
|
|
org_teams=org_teams,
|
|
managers=managers,
|
|
coaches=coaches,
|
|
esport_games=ESPORT_GAMES,
|
|
)
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
data = TryoutSchema().load(tryout_form_payload())
|
|
except ValidationError as err:
|
|
flash_validation_errors(err)
|
|
return rerender()
|
|
|
|
tryout = Tryout(
|
|
title=data['title'],
|
|
description=data['description'],
|
|
game=data['game'],
|
|
date=data['date'],
|
|
end_date=data['end_date'],
|
|
location=data['location'],
|
|
max_players=data['max_players'],
|
|
created_by=current_user.id,
|
|
status='upcoming',
|
|
target_org_team_id=data['target_org_team_id'],
|
|
manager_id=data['manager_id'],
|
|
)
|
|
db.session.add(tryout)
|
|
db.session.flush()
|
|
|
|
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
|
|
|
db.session.commit()
|
|
flash(_('Tryout created successfully!'), 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
|
|
|
return rerender()
|
|
|
|
|
|
@tryouts_bp.route('/<int:tryout_id>/edit', methods=['GET', 'POST'])
|
|
@login_required
|
|
def edit_tryout(tryout_id):
|
|
"""Edit an existing tryout event. Permission based on can_manage_this_tryout."""
|
|
tryout = db.get_or_404(Tryout, tryout_id)
|
|
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash(_('You do not have permission to edit this tryout.'), 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
|
|
if tryout.is_ended:
|
|
flash(_('This tryout has ended and can no longer be modified.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
|
|
|
org_teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
|
managers = (
|
|
User.query.filter_by(role='manager', is_active_account=True).order_by(User.full_name).all()
|
|
)
|
|
coaches = (
|
|
User.query.filter_by(role='coach', is_active_account=True).order_by(User.full_name).all()
|
|
)
|
|
|
|
def rerender():
|
|
return render_template(
|
|
'pages/tryout_form.html',
|
|
tryout=tryout,
|
|
org_teams=org_teams,
|
|
managers=managers,
|
|
coaches=coaches,
|
|
esport_games=ESPORT_GAMES,
|
|
)
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
data = TryoutSchema().load(tryout_form_payload())
|
|
except ValidationError as err:
|
|
flash_validation_errors(err)
|
|
return rerender()
|
|
|
|
tryout.title = data['title']
|
|
tryout.description = data['description']
|
|
tryout.game = data['game']
|
|
tryout.date = data['date']
|
|
tryout.end_date = data['end_date']
|
|
tryout.location = data['location']
|
|
tryout.max_players = data['max_players']
|
|
tryout.target_org_team_id = data['target_org_team_id']
|
|
tryout.manager_id = data['manager_id']
|
|
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
|
|
|
db.session.commit()
|
|
flash(_('Tryout updated successfully!'), 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
|
|
|
return rerender()
|
|
|
|
|
|
@tryouts_bp.route('/<int:tryout_id>')
|
|
@login_required
|
|
def view_tryout(tryout_id):
|
|
"""View a specific tryout with all details. Permission via polymorphic dispatch."""
|
|
tryout = db.get_or_404(Tryout, tryout_id)
|
|
|
|
can_view = False
|
|
if isinstance(current_user, Admin):
|
|
can_view = True
|
|
elif isinstance(current_user, Manager):
|
|
can_view = tryout.created_by == current_user.id or tryout.manager_id == current_user.id
|
|
elif isinstance(current_user, Coach):
|
|
can_view = current_user.can_manage_this_tryout(tryout)
|
|
elif isinstance(current_user, Player):
|
|
is_registered = (
|
|
TryoutRegistration.query.filter_by(
|
|
tryout_id=tryout_id, player_id=current_user.id
|
|
).first()
|
|
is not None
|
|
)
|
|
player_in_match = (
|
|
MatchParticipant.query.join(Match)
|
|
.filter(
|
|
MatchParticipant.player_id == current_user.id,
|
|
Match.tryout_id == tryout_id,
|
|
)
|
|
.first()
|
|
is not None
|
|
)
|
|
can_view = is_registered or player_in_match
|
|
elif isinstance(current_user, Scout):
|
|
can_view = True
|
|
|
|
if not can_view:
|
|
flash(_('You do not have permission to view this tryout.'), 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
|
|
# Everything below used to run one query per row (PERF-001): one
|
|
# User.query.get() per registration, one Evaluation lookup per player,
|
|
# one TeamMember query per team and one more User.query.get() per
|
|
# member. Thirty registrants and four teams put this page well past a
|
|
# hundred round trips, on unindexed columns.
|
|
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
|
|
registered_player_ids = [r.player_id for r in registrations if r.player_id]
|
|
players_by_id = _users_by_id(registered_player_ids)
|
|
registered_players = [
|
|
players_by_id[player_id]
|
|
for player_id in registered_player_ids
|
|
if player_id in players_by_id
|
|
]
|
|
evaluations = Evaluation.query.filter_by(tryout_id=tryout_id).all()
|
|
|
|
player_eval_status = {}
|
|
if current_user.can_evaluate():
|
|
evaluated_by_me = {
|
|
row.player_id
|
|
for row in evaluations
|
|
if row.evaluator_id == current_user.id and row.player_id
|
|
}
|
|
player_eval_status = {p.id: p.id in evaluated_by_me for p in registered_players}
|
|
|
|
is_registered = (
|
|
TryoutRegistration.query.filter_by(
|
|
tryout_id=tryout_id,
|
|
player_id=current_user.id,
|
|
).first()
|
|
is not None
|
|
)
|
|
|
|
teams = Team.query.filter_by(tryout_id=tryout_id).all()
|
|
team_ids = [team.id for team in teams]
|
|
members_by_team = {}
|
|
if team_ids:
|
|
member_rows = TeamMember.query.filter(TeamMember.team_id.in_(team_ids)).all()
|
|
member_players = _users_by_id([m.player_id for m in member_rows if m.player_id])
|
|
for row in member_rows:
|
|
members_by_team.setdefault(row.team_id, []).append(
|
|
{'player': member_players.get(row.player_id), 'position': row.position}
|
|
)
|
|
team_data = [{'team': team, 'members': members_by_team.get(team.id, [])} for team in teams]
|
|
|
|
can_edit = current_user.can_manage_this_tryout(tryout)
|
|
|
|
can_view_calendar = can_edit
|
|
if isinstance(current_user, Player):
|
|
player_in_match = (
|
|
MatchParticipant.query.join(Match)
|
|
.filter(
|
|
MatchParticipant.player_id == current_user.id,
|
|
Match.tryout_id == tryout_id,
|
|
)
|
|
.first()
|
|
is not None
|
|
)
|
|
can_view_calendar = is_registered or player_in_match
|
|
|
|
all_players = None
|
|
if can_edit:
|
|
# is_active_account, like the manager and coach queries in this same
|
|
# module. Offering a deactivated account in a roster select
|
|
# contradicts the one control that says the person has left.
|
|
all_players = (
|
|
User.query.filter_by(role='player', is_active_account=True)
|
|
.order_by(User.username)
|
|
.all()
|
|
)
|
|
|
|
matches = (
|
|
Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date, Match.start_time).all()
|
|
)
|
|
match_data = []
|
|
for match in matches:
|
|
all_participants = list(match.participants.all())
|
|
confirmed_count = sum(1 for p in all_participants if p.attendance_confirmed)
|
|
total_count = len(all_participants)
|
|
|
|
player_presence = []
|
|
for p in all_participants:
|
|
if p.player:
|
|
player_presence.append(
|
|
{
|
|
'participant_id': p.id,
|
|
'player_id': p.player_id,
|
|
'player_name': p.player.username,
|
|
'attendance_confirmed': p.attendance_confirmed,
|
|
}
|
|
)
|
|
|
|
if match.match_type == 'team_vs_team':
|
|
participants = {
|
|
'team1': match.team1.name if match.team1 else 'TBD',
|
|
'team2': match.team2.name if match.team2 else 'TBD',
|
|
'team1_players': [
|
|
{'name': m.player.username, 'position': m.position}
|
|
for m in match.team1.members.all()
|
|
]
|
|
if match.team1
|
|
else [],
|
|
'team2_players': [
|
|
{'name': m.player.username, 'position': m.position}
|
|
for m in match.team2.members.all()
|
|
]
|
|
if match.team2
|
|
else [],
|
|
}
|
|
elif match.match_type == 'player_vs_player':
|
|
# Filtered from the list already in hand. Asking the dynamic
|
|
# relationship again cost two more round trips per match for
|
|
# rows that were loaded a dozen lines above.
|
|
team1_players = [
|
|
{'name': p.player.username, 'position': p.position}
|
|
for p in all_participants
|
|
if p.team_side == 1 and p.player
|
|
]
|
|
team2_players = [
|
|
{'name': p.player.username, 'position': p.position}
|
|
for p in all_participants
|
|
if p.team_side == 2 and p.player
|
|
]
|
|
participants = {
|
|
'team1': 'Team 1',
|
|
'team2': 'Team 2',
|
|
'team1_players': team1_players,
|
|
'team2_players': team2_players,
|
|
}
|
|
else:
|
|
participants = [p.player.username for p in match.participants.all()]
|
|
|
|
match_data.append(
|
|
{
|
|
'match': match,
|
|
'participants': participants,
|
|
'confirmed_count': confirmed_count,
|
|
'total_count': total_count,
|
|
'player_presence': player_presence,
|
|
}
|
|
)
|
|
|
|
return render_template(
|
|
'pages/view_tryout.html',
|
|
tryout=tryout,
|
|
registered_players=registered_players,
|
|
evaluations=evaluations,
|
|
player_eval_status=player_eval_status,
|
|
is_registered=is_registered,
|
|
registrations=registrations,
|
|
team_data=team_data,
|
|
can_edit=can_edit,
|
|
can_view_calendar=can_view_calendar,
|
|
all_players=all_players,
|
|
matches=matches,
|
|
match_data=match_data,
|
|
game_positions=GAME_POSITIONS,
|
|
now=utc_now_naive(),
|
|
)
|
|
|
|
|
|
@tryouts_bp.route('/<int:tryout_id>/register', methods=['POST'])
|
|
@login_required
|
|
def register_for_tryout(tryout_id):
|
|
"""Register a player for a tryout. Only Players can self-register."""
|
|
if not isinstance(current_user, Player):
|
|
flash(_('Only players can register for tryouts.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
tryout = locked_tryout_or_404(tryout_id)
|
|
|
|
if tryout.status not in ['upcoming', 'in_progress']:
|
|
flash(_('This tryout is not accepting registrations.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
existing = TryoutRegistration.query.filter_by(
|
|
tryout_id=tryout_id, player_id=current_user.id
|
|
).first()
|
|
if existing:
|
|
flash(_('You are already registered for this tryout.'), 'info')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
if tryout.max_players:
|
|
count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count()
|
|
if count >= tryout.max_players:
|
|
flash(_('This tryout is full.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
registration = TryoutRegistration(tryout_id=tryout_id, player_id=current_user.id)
|
|
db.session.add(registration)
|
|
db.session.commit()
|
|
flash(_('Successfully registered for tryout!'), 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
|
|
@tryouts_bp.route('/<int:tryout_id>/status', methods=['POST'])
|
|
@login_required
|
|
def update_status(tryout_id):
|
|
"""Update the status of a tryout."""
|
|
tryout = db.get_or_404(Tryout, tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash(_('Permission denied.'), 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
try:
|
|
data = TryoutStatusSchema().load(form_payload(list_fields=()))
|
|
except ValidationError as err:
|
|
flash_validation_errors(err)
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
tryout.status = data['status']
|
|
db.session.commit()
|
|
flash(_('Tryout status updated to %(new_status)s.', new_status=data['status']), 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
|
|
@tryouts_bp.route('/<int:tryout_id>/registration/<int:player_id>/status', methods=['POST'])
|
|
@login_required
|
|
def update_registration_status(tryout_id, player_id):
|
|
"""Update a registration's attendance status."""
|
|
tryout = db.get_or_404(Tryout, tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash(_('Permission denied.'), 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
|
|
registration = TryoutRegistration.query.filter_by(
|
|
tryout_id=tryout_id, player_id=player_id
|
|
).first_or_404()
|
|
try:
|
|
data = TryoutRegistrationStatusSchema().load(form_payload(list_fields=()))
|
|
except ValidationError as err:
|
|
flash_validation_errors(err)
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
registration.status = data['status']
|
|
db.session.commit()
|
|
flash(_('Registration status updated.'), 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
|
|
@tryouts_bp.route('/<int:tryout_id>/register_player', methods=['POST'])
|
|
@login_required
|
|
def register_player(tryout_id):
|
|
"""Manually register a player for a tryout (by managers/coaches)."""
|
|
tryout = locked_tryout_or_404(tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash(_('Permission denied.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
try:
|
|
data = PlayerSelectionSchema().load(form_payload())
|
|
except ValidationError as err:
|
|
flash_validation_errors(err)
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
if not data['player_id']:
|
|
flash(_('Please select a player.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
# Same two checks as the team roster (SEC-16): the right role, and an
|
|
# account that has not been deactivated. The select this comes from now
|
|
# filters both, but the select is not the control.
|
|
player = db.session.get(User, data['player_id'])
|
|
if not player or not isinstance(player, Player) or not player.is_active_account:
|
|
flash(_('Can only register players.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
existing = TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player.id).first()
|
|
if existing:
|
|
flash(
|
|
_('%(username)s is already registered for this tryout.', username=player.username),
|
|
'info',
|
|
)
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
if tryout.max_players:
|
|
count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count()
|
|
if count >= tryout.max_players:
|
|
flash(_('This tryout is full.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
registration = TryoutRegistration(tryout_id=tryout_id, player_id=player.id)
|
|
db.session.add(registration)
|
|
db.session.commit()
|
|
flash(_('%(username)s registered for tryout!', username=player.username), 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
|
|
@tryouts_bp.route('/<int:tryout_id>/remove_player/<int:player_id>', methods=['POST'])
|
|
@login_required
|
|
def remove_player(tryout_id, player_id):
|
|
"""Remove a registered player from a tryout (cascades to teams/matches)."""
|
|
tryout = db.get_or_404(Tryout, tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash(_('Permission denied.'), 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
|
|
player = db.get_or_404(User, player_id)
|
|
|
|
registration = TryoutRegistration.query.filter_by(
|
|
tryout_id=tryout_id, player_id=player_id
|
|
).first()
|
|
if registration:
|
|
db.session.delete(registration)
|
|
|
|
team_ids = [t.id for t in Team.query.filter_by(tryout_id=tryout_id).all()]
|
|
if team_ids:
|
|
TeamMember.query.filter(
|
|
TeamMember.team_id.in_(team_ids),
|
|
TeamMember.player_id == player_id,
|
|
).delete(synchronize_session=False)
|
|
|
|
match_ids = [m.id for m in Match.query.filter_by(tryout_id=tryout_id).all()]
|
|
if match_ids:
|
|
MatchParticipant.query.filter(
|
|
MatchParticipant.match_id.in_(match_ids),
|
|
MatchParticipant.player_id == player_id,
|
|
).delete(synchronize_session=False)
|
|
|
|
db.session.commit()
|
|
flash(_('%(username)s removed from tryout.', username=player.username), 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
|
|
@tryouts_bp.route('/<int:tryout_id>/team/create', methods=['POST'])
|
|
@login_required
|
|
def create_team(tryout_id):
|
|
"""Create a tryout-specific team."""
|
|
tryout = db.get_or_404(Tryout, tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash(_('Permission denied.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
try:
|
|
data = TryoutTeamSchema().load(form_payload(list_fields=()))
|
|
except ValidationError as err:
|
|
flash_validation_errors(err)
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
team = Team(tryout_id=tryout_id, name=data['team_name'], created_by=current_user.id)
|
|
db.session.add(team)
|
|
db.session.commit()
|
|
flash(_('Team "%(team_name)s" created!', team_name=data['team_name']), 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
|
|
@tryouts_bp.route('/<int:tryout_id>/team/<int:team_id>/add', methods=['POST'])
|
|
@login_required
|
|
def add_to_team(tryout_id, team_id):
|
|
"""Add a player to a tryout team."""
|
|
team = db.get_or_404(Team, team_id)
|
|
tryout = db.get_or_404(Tryout, tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash(_('Permission denied.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
# The two ids arrive independently in the URL. Without this check, being
|
|
# allowed to manage tryout A was enough to modify a team belonging to
|
|
# tryout B, since only the tryout was authorised.
|
|
if team.tryout_id != tryout_id:
|
|
abort(404)
|
|
|
|
try:
|
|
data = TryoutTeamMemberSchema().load(form_payload(list_fields=()))
|
|
except ValidationError as err:
|
|
flash_validation_errors(err)
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
player_id = data['player_id']
|
|
|
|
# Only players registered for this tryout may be placed on its teams.
|
|
is_registered = (
|
|
TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player_id).first()
|
|
is not None
|
|
)
|
|
if not is_registered:
|
|
flash(_('That player is not registered for this tryout.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
existing = TeamMember.query.filter_by(team_id=team_id, player_id=player_id).first()
|
|
if existing:
|
|
flash(_('Player is already on this team.'), 'info')
|
|
else:
|
|
member = TeamMember(team_id=team_id, player_id=player_id, position=data['position'])
|
|
db.session.add(member)
|
|
db.session.commit()
|
|
flash(_('Player added to team!'), 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
|
|
@tryouts_bp.route('/<int:tryout_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def delete_tryout(tryout_id):
|
|
"""Delete a tryout and all associated data (matches, teams, registrations, evaluations)."""
|
|
tryout = db.get_or_404(Tryout, tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash(_('You do not have permission to delete this tryout.'), 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
|
|
match_ids = [m.id for m in Match.query.filter_by(tryout_id=tryout_id).all()]
|
|
team_ids = [t.id for t in Team.query.filter_by(tryout_id=tryout_id).all()]
|
|
|
|
# Personal notes outlive the tryout: they are a coach's observations
|
|
# about a player, not tryout data. Only their context links are cleared.
|
|
# Missing this step made the deletion fail on the foreign keys below.
|
|
PersonalNote.query.filter_by(tryout_id=tryout_id).update(
|
|
{'tryout_id': None}, synchronize_session=False
|
|
)
|
|
if match_ids:
|
|
PersonalNote.query.filter(PersonalNote.match_id.in_(match_ids)).update(
|
|
{'match_id': None}, synchronize_session=False
|
|
)
|
|
if team_ids:
|
|
PersonalNote.query.filter(PersonalNote.team_id.in_(team_ids)).update(
|
|
{'team_id': None}, synchronize_session=False
|
|
)
|
|
|
|
if match_ids:
|
|
MatchParticipant.query.filter(MatchParticipant.match_id.in_(match_ids)).delete(
|
|
synchronize_session=False
|
|
)
|
|
Match.query.filter(Match.id.in_(match_ids)).delete(synchronize_session=False)
|
|
|
|
if team_ids:
|
|
TeamMember.query.filter(TeamMember.team_id.in_(team_ids)).delete(synchronize_session=False)
|
|
Team.query.filter(Team.id.in_(team_ids)).delete(synchronize_session=False)
|
|
|
|
TryoutRegistration.query.filter_by(tryout_id=tryout_id).delete()
|
|
Evaluation.query.filter_by(tryout_id=tryout_id).delete()
|
|
|
|
db.session.delete(tryout)
|
|
db.session.commit()
|
|
flash(_('Tryout deleted successfully.'), 'success')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|