"""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.pagination import paginate from app.validators import EvaluationSchema evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations') def _users_by_id(user_ids): """Load a set of users once for aggregate/list views.""" 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()} @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_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) ) 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() ) 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) ) player_scores = {} return render_template( 'pages/evaluations.html', evaluations=evaluations_page.items, pagination=evaluations_page, player_scores=player_scores, sort_column=sort_column, sort_order=sort_order, ) @evaluations_bp.route('//', 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 = db.get_or_404(Tryout, 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 = db.get_or_404(User, 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_by_id = _users_by_id(e.evaluator_id for e in all_evaluations) evaluators = [ {'evaluator': evaluators_by_id.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('//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 = db.get_or_404(Tryout, 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_by_id = _users_by_id(reg.player_id for reg in registrations) evaluated_player_ids = { player_id for (player_id,) in db.session.query(Evaluation.player_id) .filter_by(tryout_id=tryout_id, evaluator_id=current_user.id) .all() } players = [ { 'player': player, 'evaluated': player.id in evaluated_player_ids, 'registration': registration, } for registration in registrations if (player := players_by_id.get(registration.player_id)) and isinstance(player, Player) ] return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players)