Files
team-tryouts/app/routes/evaluations.py
T
GGThedandClaude Opus 5 51877b46a0 refactor(arch): un point d entree par usage, et retrait du code mort
ARCH-007.

Trois points d entree, trois configurations differentes
  python app/app.py   0.0.0.0:10000, debogueur desactive par defaut
  python run.py       127.0.0.2:5000, debogueur ACTIVE par defaut
  python wsgi.py      Waitress, production

Le bloc __main__ de app/app.py disparait : ce module expose la fabrique.
run.py reste le point d entree de developpement, wsgi.py celui de
production, et c est tout.

run.py passait FLASK_DEBUG a 'true' par defaut. Le debogueur Werkzeug
execute du code soumis depuis le navigateur ; un processus lance ainsi et
laisse joignable est un shell distant. Le defaut passe a 'false', avec
l avertissement reecrit pour dire ce que le mode implique reellement.
L hote devient 127.0.0.1 -- 127.0.0.2 est une boucle locale valide mais
inhabituelle -- et hote comme port sont surchargeables par DEV_HOST et
DEV_PORT.

Code mort retire
  - discord_bot.py, notify_player_about_one_on_one : jamais appelee, seule
    la variante _direct l est.
  - evaluations.py, branche else de list_evaluations : elle listait les
    evaluations recues, une vue de joueur, alors que les joueurs sont
    rediriges au debut de la fonction et que can_evaluate() est vrai pour
    les quatre roles restants. Inatteignable.

Les autres elements du constat sont deja resorbes : get_auth_logger est
appelee depuis log_auth_event (OBS-001), et ALLOWED_CONTRACT_EXTENSIONS /
ALLOWED_SIGNED_EXTENSIONS sont lues par pdf_upload_error (SEC-021).

wsgi.py n est pas touche : trusted_proxy et HOST attendent la reponse du
developpeur sur la topologie reelle (nginx sur la meme machine ou non).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 15:12:34 -04:00

230 lines
9.7 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 flask_babel import gettext as _
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,
}
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()
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)