changement visualistation des evalutations
CI - Security, Lint & Tests / validate (push) Canceled after 0s

This commit is contained in:
cedrick2711
2026-09-09 12:15:25 -04:00
parent d994d88c12
commit 30069d782f
4 changed files with 700 additions and 122 deletions
+239 -68
View File
@@ -3,11 +3,13 @@
Uses polymorphic isinstance checks instead of role-string comparisons.
"""
import math
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 import false, func, select
from sqlalchemy.orm import aliased
from app.extensions import db
@@ -16,14 +18,18 @@ from app.models import (
EVALUATION_CRITERIA,
GAME_POSITIONS,
Admin,
Coach,
Evaluation,
Manager,
Player,
Scout,
Tryout,
TryoutRegistration,
User,
)
from app.pagination import paginate
from app.pagination import DEFAULT_PER_PAGE, MAX_PER_PAGE
from app.responders import respond_after_change
from app.time_utils import utc_now_naive
from app.validators import EvaluationSchema
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
@@ -63,95 +69,260 @@ def _apply_evaluation(evaluation, scores, comments, position):
evaluation.position_recommendation = position
def _visible_evaluations_query(user):
"""Evaluations the acting user is allowed to see, as a query.
Admins and scouts see everything. Managers and coaches see the
evaluations authored by managers and coaches inside the tryouts they
manage; players never reach the evaluation pages.
"""
if isinstance(user, (Admin, Scout)):
return Evaluation.query
tryout_ids = [t.id for t in user.get_visible_tryouts()]
if not tryout_ids:
return Evaluation.query.filter(false())
evaluator_alias = aliased(User, name='visible_evaluator')
return (
Evaluation.query
.join(evaluator_alias, Evaluation.evaluator_id == evaluator_alias.id)
.filter(evaluator_alias.role.in_(['manager', 'coach']))
.filter(Evaluation.tryout_id.in_(tryout_ids))
)
def _visible_evaluation_ids(user):
"""A ``select`` of the ids of evaluations the acting user may see."""
ids = _visible_evaluations_query(user).with_entities(Evaluation.id).subquery()
return select(ids.c.id)
def _average(values):
"""Mean of the non-None values, rounded to one decimal (or None)."""
values = [value for value in values if value is not None]
if not values:
return None
return round(sum(values) / len(values), 1)
def _aggregate(evaluations):
"""Per-criterion averages plus a combined overall average."""
criterion_averages = [
(label, _average([getattr(e, field) for e in evaluations]))
for field, label in EVALUATION_CRITERIA
]
return {
'criterion_averages': criterion_averages,
'combined_average': _average([e.overall_score for e in evaluations]),
'count': len(evaluations),
}
def _evaluation_view(evaluation, evaluator, tryout):
"""Shape an Evaluation for the templates (scores in criterion order)."""
return {
'evaluation': evaluation,
'evaluator': evaluator,
'tryout': tryout,
'scores': [getattr(evaluation, field) for field, _ in EVALUATION_CRITERIA],
'overall': evaluation.overall_score,
'position': evaluation.position_recommendation,
'comment': evaluation.comments,
'date': evaluation.created_at,
}
def _players_by_average(user):
"""Players with visible evaluations, best combined average first."""
visible_ids = _visible_evaluation_ids(user)
rows = (
db.session.query(
Evaluation.player_id,
func.avg(Evaluation.overall_score).label('avg_score'),
)
.filter(Evaluation.id.in_(visible_ids))
.group_by(Evaluation.player_id)
.all()
)
players_by_id = _users_by_id(row.player_id for row in rows)
ordered = [
(players_by_id[row.player_id], row.avg_score)
for row in rows
if row.player_id in players_by_id and isinstance(players_by_id[row.player_id], Player)
]
ordered.sort(
key=lambda pair: (
pair[1] is None,
-(pair[1] if pair[1] is not None else 0),
pair[0].username.lower(),
)
)
return ordered
@evaluations_bp.route('')
@login_required
def list_evaluations():
"""List all evaluations accessible to the current user."""
"""List player evaluations grouped by player, with a per-player view."""
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_column = request.args.get('sort', 'overall_score')
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')
visible_ids = _visible_evaluation_ids(user)
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_page = paginate(
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, Evaluation.id)
aggregate = (
db.session.query(
Evaluation.player_id,
func.count(Evaluation.id).label('eval_count'),
func.avg(Evaluation.overall_score).label('avg_score'),
func.max(Evaluation.created_at).label('last_eval'),
)
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)
.filter(Evaluation.id.in_(visible_ids))
.group_by(Evaluation.player_id)
.all()
)
players_by_id = _users_by_id(row.player_id for row in aggregate)
grouped = []
for row in aggregate:
player = players_by_id.get(row.player_id)
if not player or not isinstance(player, Player):
continue
grouped.append(
{
'player': player,
'avg': round(row.avg_score, 1) if row.avg_score is not None else None,
'count': row.eval_count,
'last_eval': row.last_eval,
'evaluations': [],
}
)
def sort_value(entry):
if sort_column == 'player':
return entry['player'].username.lower()
if sort_column == 'count':
return entry['count']
if sort_column == 'created_at':
return entry['last_eval'] or utc_now_naive()
return entry['avg'] if entry['avg'] is not None else float('-inf')
grouped.sort(key=sort_value, reverse=(sort_order == 'desc'))
page = request.args.get('page', 1, type=int) or 1
page = max(1, page)
per_page = request.args.get('per_page', DEFAULT_PER_PAGE, type=int) or DEFAULT_PER_PAGE
per_page = max(1, min(per_page, MAX_PER_PAGE))
total = len(grouped)
pages = math.ceil(total / per_page) if total else 1
page = min(page, pages)
start = (page - 1) * per_page
page_items = grouped[start:start + per_page]
players_on_page = {entry['player'].id: entry for entry in page_items}
if players_on_page:
evaluations = (
Evaluation.query
.filter(Evaluation.player_id.in_(players_on_page))
.filter(Evaluation.id.in_(visible_ids))
.order_by(Evaluation.created_at.desc(), Evaluation.id.desc())
.all()
)
players_by_id = _users_by_id(row.player_id for row in avg_scores)
player_scores = {}
for row in avg_scores:
p = players_by_id.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_page = paginate(
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, Evaluation.id)
evaluators_by_id = _users_by_id(e.evaluator_id for e in evaluations)
tryout_ids = [e.tryout_id for e in evaluations if e.tryout_id]
tryouts_by_id = (
{t.id: t for t in Tryout.query.filter(Tryout.id.in_(tryout_ids)).all()}
if tryout_ids
else {}
)
player_scores = {}
for evaluation in evaluations:
players_on_page[evaluation.player_id]['evaluations'].append(
_evaluation_view(
evaluation,
evaluators_by_id.get(evaluation.evaluator_id),
tryouts_by_id.get(evaluation.tryout_id),
)
)
pagination = {
'page': page,
'pages': pages,
'total': total,
'has_prev': page > 1,
'has_next': page < pages,
'prev_num': page - 1,
'next_num': page + 1,
}
return render_template(
'pages/evaluations.html',
evaluations=evaluations_page.items,
pagination=evaluations_page,
player_scores=player_scores,
players=page_items,
pagination=pagination,
sort_column=sort_column,
sort_order=sort_order,
evaluation_criteria=EVALUATION_CRITERIA,
first_player_id=grouped[0]['player'].id if grouped else None,
)
@evaluations_bp.route('/player/<int:player_id>')
@login_required
def player_evaluations(player_id):
"""One player's evaluations, with prev/next navigation between players."""
user = current_user
if not user.can_evaluate():
flash(_('You do not have permission to view evaluations.'), 'danger')
return redirect(url_for('main.dashboard'))
player = db.get_or_404(User, player_id)
if not isinstance(player, Player):
flash(_('Can only view evaluations for players.'), 'danger')
return redirect(url_for('evaluations.list_evaluations'))
ordered = _players_by_average(user)
index = next((i for i, (p, _avg) in enumerate(ordered) if p.id == player.id), None)
prev_player = ordered[index - 1][0] if index is not None and index > 0 else None
next_player = ordered[index + 1][0] if index is not None and index + 1 < len(ordered) else None
visible_ids = _visible_evaluation_ids(user)
evaluations = (
Evaluation.query
.filter(Evaluation.player_id == player.id)
.filter(Evaluation.id.in_(visible_ids))
.order_by(Evaluation.created_at.desc(), Evaluation.id.desc())
.all()
)
evaluators_by_id = _users_by_id(e.evaluator_id for e in evaluations)
tryout_ids = [e.tryout_id for e in evaluations if e.tryout_id]
tryouts_by_id = (
{t.id: t for t in Tryout.query.filter(Tryout.id.in_(tryout_ids)).all()}
if tryout_ids
else {}
)
evaluation_views = [
_evaluation_view(e, evaluators_by_id.get(e.evaluator_id), tryouts_by_id.get(e.tryout_id))
for e in evaluations
]
aggregates = _aggregate(evaluations)
return render_template(
'pages/player_evaluations.html',
player=player,
evaluations=evaluation_views,
prev_player=prev_player,
next_player=next_player,
criterion_averages=aggregates['criterion_averages'],
combined_average=aggregates['combined_average'],
evaluation_count=aggregates['count'],
evaluation_criteria=EVALUATION_CRITERIA,
)