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.
231 lines
8.2 KiB
Python
231 lines
8.2 KiB
Python
"""Evaluation routes for assessing player performance during tryouts.
|
|
|
|
Uses polymorphic isinstance checks instead of role-string comparisons.
|
|
"""
|
|
|
|
from flask import Blueprint, 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 func
|
|
from sqlalchemy.orm import aliased
|
|
|
|
from app.extensions import db
|
|
from app.forms import flash_validation_errors, form_payload
|
|
from app.models import (
|
|
GAME_POSITIONS,
|
|
Admin,
|
|
Evaluation,
|
|
Player,
|
|
Tryout,
|
|
TryoutRegistration,
|
|
User,
|
|
)
|
|
from app.validators import EvaluationSchema
|
|
|
|
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
|
|
|
|
|
|
@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,
|
|
}
|
|
else:
|
|
# Everyone still here evaluates: players were redirected above, and
|
|
# can_evaluate() is true for the four remaining roles. The former
|
|
# `else` branch listed evaluations *received* — a player's view,
|
|
# unreachable from this point (ARCH-007).
|
|
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 = {}
|
|
|
|
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()
|
|
|
|
def render_evaluation_form():
|
|
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,
|
|
)
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
data = EvaluationSchema().load(form_payload(list_fields=(), optional_blank=()))
|
|
except ValidationError as err:
|
|
flash_validation_errors(err)
|
|
return render_evaluation_form()
|
|
|
|
evaluation = existing_eval
|
|
if evaluation is None:
|
|
evaluation = Evaluation(
|
|
tryout_id=tryout_id,
|
|
player_id=player_id,
|
|
evaluator_id=current_user.id,
|
|
)
|
|
db.session.add(evaluation)
|
|
flash(_('Evaluation submitted successfully!'), 'success')
|
|
else:
|
|
flash(_('Evaluation updated!'), 'success')
|
|
|
|
evaluation.apply_scores(data)
|
|
evaluation.comments = data['comments']
|
|
evaluation.position_recommendation = data['position_recommendation']
|
|
|
|
db.session.commit()
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
return render_evaluation_form()
|
|
|
|
|
|
@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)
|