ARCH-005, seconde moitie. Meme forme que pour les matchs : des champs lus a la main sur request.form, deux verifies et le reste cru sur parole. Cote tryout : - game pilote la liste des postes et les champs de gamertag montres au joueur qui s inscrit. Il etait accepte tel quel : une faute de frappe produisait une selection pour laquelle personne ne pouvait etre evalue ; - max_players etait int(x) if x else None — un 500 sur « twelve », et un -3 accepte sans broncher ; - coach_ids etait charge par User.id.in_(...) sans filtre de role. Une soumission fabriquee a la main pouvait donc nommer un joueur coach d une selection, ce qui est une attribution de droits : gerer la selection et evaluer ses joueurs. Ce n est pas un formulaire que l interface propose, et ca marchait. Cote evaluation, validate_score transformait tout ce qui sortait de 1..10 — 11, 0, « bien » — en None. Le critere disparaissait de la moyenne et la page annoncait l evaluation enregistree. Rien ne distinguait « non evalue » de « evalue, refuse, et oublie ». Le calcul de la moyenne remonte sur le modele, en Evaluation.overall_from et apply_scores. Il vivait dans la route, additionnant neuf variables locales, et ne pouvait pas etre exerce sans requete HTTP, session authentifiee et base — c est TEST-002, et c est pourquoi le calcul des scores n avait aucun test. Il en a maintenant six, sans rien monter. Une precision qui compte : aucun critere rempli donne None, pas 0. La grille commence a 1, donc un zero serait une note qu aucun joueur ne peut recevoir, et qui le classerait sous tout le monde dans la liste. Les neuf criteres sont ecrits en toutes lettres dans le schema plutot que generes depuis le modele — un schema se lit — et un test verifie que les deux listes coincident. C est la garde qui empeche la derive, pas l astuce. Douze chaines traduites, dont trois que pybabel avait devinees en fuzzy : une entree fuzzy est ignoree a l execution, le piege est consigne dans docs/translations.md. 30 tests neufs. 477 au total.
646 lines
24 KiB
Python
646 lines
24 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 datetime import datetime
|
|
|
|
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 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.validators import TryoutSchema
|
|
|
|
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()}
|
|
|
|
|
|
@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=datetime.utcnow())
|
|
|
|
|
|
@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 = Tryout.query.get_or_404(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 = Tryout.query.get_or_404(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:
|
|
all_players = User.query.filter_by(role='player').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=datetime.utcnow(),
|
|
)
|
|
|
|
|
|
@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."""
|
|
tryout = Tryout.query.get_or_404(tryout_id)
|
|
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))
|
|
|
|
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 = Tryout.query.get_or_404(tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash(_('Permission denied.'), 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
new_status = request.form.get('status')
|
|
if new_status in ['upcoming', 'in_progress', 'completed']:
|
|
tryout.status = new_status
|
|
db.session.commit()
|
|
flash(_('Tryout status updated to %(new_status)s.', new_status=new_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 = Tryout.query.get_or_404(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()
|
|
new_status = request.form.get('status')
|
|
if new_status in ['registered', 'attended', 'no_show']:
|
|
registration.status = new_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 = Tryout.query.get_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))
|
|
player_id = request.form.get('player_id')
|
|
if not player_id:
|
|
flash(_('Please select a player.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
player = User.query.get_or_404(int(player_id))
|
|
if not isinstance(player, Player):
|
|
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 = Tryout.query.get_or_404(tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash(_('Permission denied.'), 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
|
|
player = User.query.get_or_404(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 = Tryout.query.get_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))
|
|
|
|
team_name = request.form.get('team_name')
|
|
if team_name:
|
|
team = Team(tryout_id=tryout_id, name=team_name, created_by=current_user.id)
|
|
db.session.add(team)
|
|
db.session.commit()
|
|
flash(_('Team "%(team_name)s" created!', team_name=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 = Team.query.get_or_404(team_id)
|
|
tryout = Tryout.query.get_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))
|
|
|
|
# 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)
|
|
|
|
player_id = request.form.get('player_id', type=int)
|
|
if not player_id:
|
|
flash(_('Please select a player.'), 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_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))
|
|
|
|
position = request.form.get('position', '')
|
|
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=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 = Tryout.query.get_or_404(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'))
|