ajout dune page batch evaluation

This commit is contained in:
cedrick2711
2026-08-17 18:16:22 -04:00
parent 2b943c5c22
commit a0e31d1a2f
5 changed files with 217 additions and 46 deletions
+1
View File
@@ -20,6 +20,7 @@ from app.models._constants import (
GAME_PLATFORMS, GAME_PLATFORMS,
PLATFORM_CODES, PLATFORM_CODES,
TRN_URLS, TRN_URLS,
EVALUATION_CRITERIA,
) )
# ========================================================================= # =========================================================================
+15
View File
@@ -9,6 +9,21 @@ Contains game lists, position mappings, platform codes, and TRN URL templates.
USER_TYPES = ['admin', 'manager', 'coach', 'player', 'scout'] USER_TYPES = ['admin', 'manager', 'coach', 'player', 'scout']
# Ordered list of (field_name, human_label) pairs for the player evaluation
# score criteria. Kept in a single place so the evaluation forms, batch
# evaluation page, and any future reporting all stay in sync.
EVALUATION_CRITERIA = [
('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'),
]
ESPORT_GAMES = [ ESPORT_GAMES = [
'Valorant', 'Valorant',
'League of Legends', 'League of Legends',
+108 -2
View File
@@ -9,7 +9,7 @@ from app.extensions import db
from app.models import ( from app.models import (
Admin, Coach, Manager, Player, Admin, Coach, Manager, Player,
User, Tryout, Evaluation, TryoutRegistration, User, Tryout, Evaluation, TryoutRegistration,
OrgTeam, GAME_POSITIONS, OrgTeam, GAME_POSITIONS, EVALUATION_CRITERIA,
) )
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
@@ -30,6 +30,21 @@ def validate_score(score_value):
return None return None
def compute_overall(scores):
"""Average the non-None scores, or return None if there are none."""
valid = [s for s in scores if s is not None]
return sum(valid) / len(valid) if valid else None
def _apply_evaluation(evaluation, scores, comments, position):
"""Write validated scores/comments/position onto an Evaluation instance."""
for field_name, _ in EVALUATION_CRITERIA:
setattr(evaluation, field_name, scores[field_name])
evaluation.overall_score = compute_overall(list(scores.values()))
evaluation.comments = comments
evaluation.position_recommendation = position
@evaluations_bp.route('') @evaluations_bp.route('')
@login_required @login_required
def list_evaluations(): def list_evaluations():
@@ -230,4 +245,95 @@ def players_to_evaluate(tryout_id):
players.append({'player': p, 'evaluated': existing is not None, players.append({'player': p, 'evaluated': existing is not None,
'registration': reg}) 'registration': reg})
return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players) 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)
+27
View File
@@ -1955,3 +1955,30 @@ a:hover { color: var(--primary-dark); }
[data-theme="dark"] .error-container p { [data-theme="dark"] .error-container p {
color: var(--text-secondary); color: var(--text-secondary);
} }
/* Batch Evaluation - 2 cards wide layout */
.batch-eval-form {
max-width: none;
}
.batch-eval-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 20px;
}
.batch-eval-grid .card {
margin-bottom: 0;
}
/* Allow criteria rows to wrap for a 3x3 grid inside each card */
.batch-eval-grid .form-row {
flex-wrap: wrap;
}
@media (max-width: 1100px) {
.batch-eval-grid {
grid-template-columns: 1fr;
}
}
+66 -44
View File
@@ -6,54 +6,76 @@
{% block content %} {% block content %}
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3><i class="fas fa-users"></i> Players in {{ tryout.title }}</h3> <h3><i class="fas fa-users"></i> Select players to evaluate in {{ tryout.title }}</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="table-container"> <p class="text-muted mb-3">Choose the players you want to evaluate, then load all of them on a single page.</p>
<table class="table"> <form method="GET" action="{{ url_for('evaluations.batch_evaluate', tryout_id=tryout.id) }}">
<thead> <div class="table-container">
<tr> <table class="table">
<th>Player</th> <thead>
<th>Contact</th> <tr>
<th>Attendance</th> <th><input type="checkbox" id="select-all" onclick="toggleAll(this)"></th>
<th>Status</th> <th>Player</th>
<th>Actions</th> <th>Contact</th>
</tr> <th>Attendance</th>
</thead> <th>Status</th>
<tbody> <th>Actions</th>
{% for entry in players %} </tr>
<tr> </thead>
<td> <tbody>
<div class="user-mini"> {% for entry in players %}
<tr>
<td>
<input type="checkbox" name="player_ids" value="{{ entry.player.id }}" class="player-check">
</td>
<td>
<div class="user-mini">
<div class="avatar-sm">{{ entry.player.username[:2] | upper }}</div> <div class="avatar-sm">{{ entry.player.username[:2] | upper }}</div>
<span>{{ entry.player.username }}</span> <span>{{ entry.player.username }}</span>
</div> </div>
</td> </td>
<td>{{ entry.player.email }}</td> <td>{{ entry.player.email }}</td>
<td> <td>
<span class="badge badge-{{ entry.registration.status }}">{{ entry.registration.status }}</span> <span class="badge badge-{{ entry.registration.status }}">{{ entry.registration.status }}</span>
</td> </td>
<td> <td>
{% if entry.evaluated %} {% if entry.evaluated %}
<span class="badge badge-success">Evaluated</span> <span class="badge badge-success">Evaluated</span>
{% else %} {% else %}
<span class="badge badge-warning">Not Evaluated</span> <span class="badge badge-warning">Not Evaluated</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a href="{{ url_for('evaluations.evaluate_player', tryout_id=tryout.id, player_id=entry.player.id) }}" class="btn btn-sm btn-primary"> <a href="{{ url_for('evaluations.evaluate_player', tryout_id=tryout.id, player_id=entry.player.id) }}" class="btn btn-sm btn-outline">
<i class="fas fa-clipboard"></i> {% if entry.evaluated %}View/Edit{% else %}Evaluate{% endif %} <i class="fas fa-clipboard"></i> {% if entry.evaluated %}View/Edit{% else %}Single{% endif %}
</a> </a>
</td> </td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td colspan="5" class="text-center">No players registered for this tryout.</td> <td colspan="6" class="text-center">No players registered for this tryout.</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="form-actions">
<a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-clipboard-check"></i> Evaluate Selected
</button>
</div>
</form>
</div> </div>
</div> </div>
<script>
function toggleAll(master) {
var boxes = document.querySelectorAll('.player-check');
for (var i = 0; i < boxes.length; i++) {
boxes[i].checked = master.checked;
}
}
</script>
{% endblock %} {% endblock %}