SEC-AUTHZ-002. add_to_team recevait tryout_id et team_id independamment
dans l'URL, controlait l'autorisation sur le tryout, puis operait sur
l'equipe sans jamais etablir de lien entre les deux. Un gestionnaire du
tryout A pouvait donc modifier une equipe du tryout B.
Le lint pointait exactement dessus : `team` etait charge ligne 463 puis
jamais utilise. La correction automatique proposee etait de supprimer la
variable, ce qui aurait fait taire l'avertissement en cimentant la faille.
Elle est desormais utilisee pour ce a quoi elle servait.
Trois defauts sur la meme route, corriges ensemble :
- team.tryout_id != tryout_id repond maintenant 404
- seuls les joueurs inscrits au tryout peuvent rejoindre ses equipes
- int(player_id) sur une entree de formulaire brute levait ValueError,
donc une erreur 500, sur toute valeur non numerique
Nettoyage automatique par ruff : 34 imports et variables morts retires
sur l'ensemble du paquet. La suite de tests a servi de filet, elle passe
a l'identique avant et apres. Aucun changement de comportement.
A noter, OneOnOneRequestSchema figurait aussi parmi les imports morts :
c'est un quatrieme schema jamais appele, la route one_on_one validant ses
dates a la main. Unifier la validation reste a faire (ARCH-005).
Co-Authored-By: Claude Opus 5 <[email protected]>
233 lines
9.8 KiB
Python
233 lines
9.8 KiB
Python
"""Evaluation routes for assessing player performance during tryouts.
|
|
|
|
Uses polymorphic isinstance checks instead of role-string comparisons.
|
|
"""
|
|
|
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
|
from flask_login import login_required, current_user
|
|
from app.extensions import db
|
|
from app.models import (
|
|
Admin, Player,
|
|
User, Tryout, Evaluation, TryoutRegistration,
|
|
GAME_POSITIONS,
|
|
)
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import aliased
|
|
|
|
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
|
|
|
|
|
|
def validate_score(score_value):
|
|
"""Validate that a score is between 1 and 10."""
|
|
if score_value is None:
|
|
return None
|
|
try:
|
|
score = int(score_value)
|
|
if 1 <= score <= 10:
|
|
return score
|
|
return None
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
|
|
@evaluations_bp.route('')
|
|
@login_required
|
|
def list_evaluations():
|
|
"""List all evaluations accessible to the current user."""
|
|
user = current_user
|
|
|
|
if isinstance(user, Player):
|
|
flash('You do not have permission to view evaluations.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
sort_column = request.args.get('sort', 'created_at')
|
|
sort_order = request.args.get('order', 'desc')
|
|
if sort_order not in ('asc', 'desc'):
|
|
sort_order = 'desc'
|
|
|
|
player_alias = aliased(User, name='eval_player')
|
|
evaluator_alias = aliased(User, name='eval_evaluator')
|
|
|
|
sort_map = {
|
|
'tryout': Tryout.title,
|
|
'player': player_alias.username,
|
|
'evaluator': evaluator_alias.username,
|
|
'mecanics_score': Evaluation.mecanics_score,
|
|
'cohesion_score': Evaluation.cohesion_score,
|
|
'communication_score': Evaluation.communication_score,
|
|
'gamesense_score': Evaluation.gamesense_score,
|
|
'versatility_score': Evaluation.versatility_score,
|
|
'discipline_score': Evaluation.discipline_score,
|
|
'analysis_score': Evaluation.analysis_score,
|
|
'sport_ethics_score': Evaluation.sport_ethics_score,
|
|
'mental_score': Evaluation.mental_score,
|
|
'overall_score': Evaluation.overall_score,
|
|
'position_recommendation': Evaluation.position_recommendation,
|
|
'created_at': Evaluation.created_at,
|
|
}
|
|
|
|
sort_expr = sort_map.get(sort_column, Evaluation.created_at)
|
|
if sort_order == 'asc':
|
|
sort_expr = sort_expr.asc()
|
|
else:
|
|
sort_expr = sort_expr.desc()
|
|
|
|
if isinstance(user, Admin):
|
|
evaluations = Evaluation.query \
|
|
.outerjoin(Tryout, Evaluation.tryout_id == Tryout.id) \
|
|
.outerjoin(player_alias, Evaluation.player_id == player_alias.id) \
|
|
.outerjoin(evaluator_alias, Evaluation.evaluator_id == evaluator_alias.id) \
|
|
.order_by(sort_expr).all()
|
|
avg_scores = db.session.query(
|
|
Evaluation.player_id,
|
|
func.count(Evaluation.id).label('eval_count'),
|
|
func.avg(Evaluation.overall_score).label('avg_score'),
|
|
).group_by(Evaluation.player_id).all()
|
|
player_scores = {}
|
|
for row in avg_scores:
|
|
p = User.query.get(row.player_id)
|
|
if p:
|
|
player_scores[p.id] = {
|
|
'player': p, 'count': row.eval_count,
|
|
'avg': round(row.avg_score, 1) if row.avg_score else 0,
|
|
}
|
|
elif user.can_evaluate():
|
|
evaluations = Evaluation.query \
|
|
.outerjoin(Tryout, Evaluation.tryout_id == Tryout.id) \
|
|
.outerjoin(player_alias, Evaluation.player_id == player_alias.id) \
|
|
.outerjoin(evaluator_alias, Evaluation.evaluator_id == evaluator_alias.id) \
|
|
.filter(Evaluation.evaluator_id == user.id) \
|
|
.order_by(sort_expr).all()
|
|
player_scores = {}
|
|
else:
|
|
evaluations = Evaluation.query \
|
|
.outerjoin(Tryout, Evaluation.tryout_id == Tryout.id) \
|
|
.outerjoin(player_alias, Evaluation.player_id == player_alias.id) \
|
|
.outerjoin(evaluator_alias, Evaluation.evaluator_id == evaluator_alias.id) \
|
|
.filter(Evaluation.player_id == user.id) \
|
|
.order_by(sort_expr).all()
|
|
player_scores = {}
|
|
|
|
return render_template('pages/evaluations.html',
|
|
evaluations=evaluations, player_scores=player_scores,
|
|
sort_column=sort_column, sort_order=sort_order)
|
|
|
|
|
|
@evaluations_bp.route('/<int:tryout_id>/<int:player_id>', methods=['GET', 'POST'])
|
|
@login_required
|
|
def evaluate_player(tryout_id, player_id):
|
|
"""Evaluate a specific player in a tryout."""
|
|
if not current_user.can_evaluate():
|
|
flash('You do not have permission to evaluate players.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
tryout = Tryout.query.get_or_404(tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash('You do not have permission to evaluate players in this tryout.', 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
|
|
is_registered = TryoutRegistration.query.filter_by(
|
|
tryout_id=tryout_id, player_id=player_id,
|
|
).first() is not None
|
|
if not is_registered:
|
|
flash('Player is not registered for this tryout.', 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
player = User.query.get_or_404(player_id)
|
|
if not isinstance(player, Player):
|
|
flash('Can only evaluate players.', 'danger')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
existing_eval = Evaluation.query.filter_by(
|
|
tryout_id=tryout_id, player_id=player_id, evaluator_id=current_user.id,
|
|
).first()
|
|
|
|
if request.method == 'POST':
|
|
mecanics = validate_score(request.form.get('mecanics_score'))
|
|
cohesion = validate_score(request.form.get('cohesion_score'))
|
|
communication = validate_score(request.form.get('communication_score'))
|
|
gamesense = validate_score(request.form.get('gamesense_score'))
|
|
versatility = validate_score(request.form.get('versatility_score'))
|
|
discipline = validate_score(request.form.get('discipline_score'))
|
|
analysis = validate_score(request.form.get('analysis_score'))
|
|
sport_ethics = validate_score(request.form.get('sport_ethics_score'))
|
|
mental = validate_score(request.form.get('mental_score'))
|
|
comments = request.form.get('comments')
|
|
position = request.form.get('position_recommendation')
|
|
|
|
scores = [s for s in [mecanics, cohesion, communication, gamesense,
|
|
versatility, discipline, analysis, sport_ethics, mental]
|
|
if s is not None]
|
|
overall = sum(scores) / len(scores) if scores else None
|
|
|
|
if existing_eval:
|
|
existing_eval.mecanics_score = mecanics
|
|
existing_eval.cohesion_score = cohesion
|
|
existing_eval.communication_score = communication
|
|
existing_eval.gamesense_score = gamesense
|
|
existing_eval.versatility_score = versatility
|
|
existing_eval.discipline_score = discipline
|
|
existing_eval.analysis_score = analysis
|
|
existing_eval.sport_ethics_score = sport_ethics
|
|
existing_eval.mental_score = mental
|
|
existing_eval.overall_score = overall
|
|
existing_eval.comments = comments
|
|
existing_eval.position_recommendation = position
|
|
flash('Evaluation updated!', 'success')
|
|
else:
|
|
evaluation = Evaluation(
|
|
tryout_id=tryout_id, player_id=player_id,
|
|
evaluator_id=current_user.id,
|
|
mecanics_score=mecanics, cohesion_score=cohesion,
|
|
communication_score=communication, gamesense_score=gamesense,
|
|
versatility_score=versatility, discipline_score=discipline,
|
|
analysis_score=analysis, sport_ethics_score=sport_ethics,
|
|
mental_score=mental, overall_score=overall,
|
|
comments=comments, position_recommendation=position,
|
|
)
|
|
db.session.add(evaluation)
|
|
flash('Evaluation submitted successfully!', 'success')
|
|
|
|
db.session.commit()
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
evaluators = None
|
|
if isinstance(current_user, Admin):
|
|
all_evaluations = Evaluation.query.filter_by(
|
|
tryout_id=tryout_id, player_id=player_id,
|
|
).all()
|
|
evaluators = [{'evaluator': User.query.get(e.evaluator_id), 'eval': e}
|
|
for e in all_evaluations]
|
|
|
|
return render_template('pages/evaluate_player.html',
|
|
tryout=tryout, player=player,
|
|
existing_eval=existing_eval,
|
|
evaluators=evaluators,
|
|
game_positions=GAME_POSITIONS)
|
|
|
|
|
|
@evaluations_bp.route('/<int:tryout_id>/players')
|
|
@login_required
|
|
def players_to_evaluate(tryout_id):
|
|
"""List players that need evaluation in a specific tryout."""
|
|
if not current_user.can_evaluate():
|
|
flash('Permission denied.', 'danger')
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
tryout = Tryout.query.get_or_404(tryout_id)
|
|
if not current_user.can_manage_this_tryout(tryout):
|
|
flash('You do not have permission to evaluate players in this tryout.', 'danger')
|
|
return redirect(url_for('tryouts.list_tryouts'))
|
|
|
|
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
|
|
players = []
|
|
for reg in registrations:
|
|
p = User.query.get(reg.player_id)
|
|
if p and isinstance(p, Player):
|
|
existing = Evaluation.query.filter_by(
|
|
tryout_id=tryout_id, player_id=p.id, evaluator_id=current_user.id,
|
|
).first()
|
|
players.append({'player': p, 'evaluated': existing is not None,
|
|
'registration': reg})
|
|
|
|
return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players) |