347 lines
13 KiB
Python
347 lines
13 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 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, Coach, Manager, Player,
|
|
User, Tryout, Evaluation, TryoutRegistration,
|
|
OrgTeam, GAME_POSITIONS, EVALUATION_CRITERIA,
|
|
)
|
|
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('/<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 = 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('/<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 = 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)
|
|
|
|
|
|
@evaluations_bp.route('/<int:tryout_id>/batch', methods=['GET', 'POST'])
|
|
@login_required
|
|
def batch_evaluate(tryout_id):
|
|
"""Evaluate multiple players at once in a tryout.
|
|
|
|
GET renders a single form listing every selected player with their
|
|
evaluation criteria. POST saves (creates or updates) all of them.
|
|
"""
|
|
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'))
|
|
|
|
# Resolve selected player ids (query string on GET, hidden fields on POST).
|
|
player_ids = []
|
|
for raw in request.values.getlist('player_ids'):
|
|
try:
|
|
pid = int(raw)
|
|
except (ValueError, TypeError):
|
|
continue
|
|
if pid not in player_ids:
|
|
player_ids.append(pid)
|
|
|
|
if not player_ids:
|
|
flash('Please select at least one player to evaluate.', 'warning')
|
|
return redirect(url_for('evaluations.players_to_evaluate', tryout_id=tryout_id))
|
|
|
|
players = []
|
|
for pid in player_ids:
|
|
player = User.query.get(pid)
|
|
if not player or not isinstance(player, Player):
|
|
continue
|
|
is_registered = TryoutRegistration.query.filter_by(
|
|
tryout_id=tryout_id, player_id=pid,
|
|
).first() is not None
|
|
if not is_registered:
|
|
continue
|
|
existing = Evaluation.query.filter_by(
|
|
tryout_id=tryout_id, player_id=pid, evaluator_id=current_user.id,
|
|
).first()
|
|
existing_scores = {
|
|
field_name: getattr(existing, field_name) if existing else None
|
|
for field_name, _ in EVALUATION_CRITERIA
|
|
}
|
|
players.append({
|
|
'player': player,
|
|
'existing': existing,
|
|
'existing_scores': existing_scores,
|
|
})
|
|
|
|
if not players:
|
|
flash('No valid players selected for evaluation.', 'danger')
|
|
return redirect(url_for('evaluations.players_to_evaluate', tryout_id=tryout_id))
|
|
|
|
if request.method == 'POST':
|
|
saved = 0
|
|
for entry in players:
|
|
pid = entry['player'].id
|
|
scores = {
|
|
field_name: validate_score(request.form.get(f'{field_name}_{pid}'))
|
|
for field_name, _ in EVALUATION_CRITERIA
|
|
}
|
|
comments = request.form.get(f'comments_{pid}')
|
|
position = request.form.get(f'position_recommendation_{pid}')
|
|
|
|
existing = entry['existing']
|
|
if existing:
|
|
_apply_evaluation(existing, scores, comments, position)
|
|
else:
|
|
evaluation = Evaluation(
|
|
tryout_id=tryout_id, player_id=pid,
|
|
evaluator_id=current_user.id,
|
|
)
|
|
_apply_evaluation(evaluation, scores, comments, position)
|
|
db.session.add(evaluation)
|
|
saved += 1
|
|
|
|
db.session.commit()
|
|
flash(f'Saved evaluations for {saved} player(s).', 'success')
|
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
|
|
|
return render_template('pages/batch_evaluate.html',
|
|
tryout=tryout, players=players,
|
|
evaluation_criteria=EVALUATION_CRITERIA,
|
|
game_positions=GAME_POSITIONS)
|