+ {% for opt in export_options %}
+ {% if opt.url_name == 'export_csv' %}
+ {% url 'export_csv' course.id as opt_url %}
+ {% elif opt.url_name == 'export_pdf' %}
+ {% url 'export_pdf' course.id as opt_url %}
+ {% else %}
+ {% url opt.url_name course.id as opt_url %}
+ {% endif %}
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+ {% endif %}
@@ -241,12 +263,48 @@ function getVisibleRows() {
return JSON.stringify(rows);
}
-$('#exportCsvForm').on('submit', function(e) {
- $('#csvRowsInput').val(getVisibleRows());
+// Handle custom export option buttons in the modal
+$('.export-option-btn').on('click', function() {
+ var url = $(this).data('url');
+ if (!url) return;
+ // Create a temporary form and submit POST with rows
+ var form = $('');
+ form.attr('action', url);
+ // Insert CSRF token from cookie
+ function getCookie(name) {
+ var cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = cookies[i].trim();
+ if (cookie.substring(0, name.length + 1) === (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ }
+ var csrf = getCookie('csrftoken');
+ if (csrf) {
+ form.append($('').val(csrf));
+ }
+ var input = $('').val(getVisibleRows());
+ form.append(input);
+ // Append to body and submit
+ $('body').append(form);
+ form.submit();
});
-$('#exportPdfForm').on('submit', function(e) {
- $('#pdfRowsInput').val(getVisibleRows());
+// Help button handler: show help text in a small modal
+$(document).on('click', '.help-btn', function(e){
+ e.stopPropagation(); // prevent triggering parent button
+ var helpText = $(this).data('help') || '';
+ var modalHtml = '\n
\n
\n
\n
\n
Détails de l\'export
\n \n
\n
\n ' + $('
').text(helpText).html() + '\n
\n \n
\n
\n
';
+ // Remove any existing help modal to avoid duplicates
+ $('#exportHelpModal').remove();
+ $('body').append(modalHtml);
+ $('#exportHelpModal').modal('show');
});
{% endblock %}
\ No newline at end of file
diff --git a/main/urls.py b/main/urls.py
index 0aa6d70..eb09599 100644
--- a/main/urls.py
+++ b/main/urls.py
@@ -6,6 +6,8 @@ urlpatterns = [
path('course//', views.course_detail_view, name='course_detail'),
path('course//export_csv/', views.export_csv, name='export_csv'),
path('course//export_pdf/', views.export_pdf, name='export_pdf'),
+ path('course//export_classe_csv/', views.export_classe_csv, name='export_classe_csv'),
+ path('course//export_classe/', views.export_classe, name='export_classe'),
path('scan/', views.scan_view, name='scan'),
path('dossards/', views.dossards_view, name='dossards'),
path('ajax/coureur_autocomplete/', views.coureur_autocomplete, name='coureur_autocomplete'),
diff --git a/main/views.py b/main/views.py
index d14ff8f..c9ad927 100644
--- a/main/views.py
+++ b/main/views.py
@@ -256,13 +256,56 @@ def course_detail_view(request, course_id):
# Formatage de la date pour affichage JJ/MM/AAAA
from main.templatetags.temps_format import format_date
date_str = format_date(course.date)
+
+ # Définir la liste des exports disponibles selon le type de course
+ # Format: list of dicts { 'label': 'Nom export', 'url_name': 'export_xxx' }
+ export_options = []
+ if course.type == Course.TYPE_UNIQUE:
+ export_options = [
+ {
+ 'label': 'Export résultats (CSV)',
+ 'url_name': 'export_csv',
+ 'help': "Télécharge un fichier CSV contenant les rangs, noms, prénoms, classes et temps des arrivées. Si vous avez filtré ou groupé la table, seules les lignes visibles seront exportées.",
+ },
+ {
+ 'label': 'Export résultats (PDF)',
+ 'url_name': 'export_pdf',
+ 'help': "Télécharge un fichier PDF contenant les rangs, noms, prénoms, classes et temps des arrivées. Si vous avez filtré ou groupé la table, seules les lignes visibles seront exportées",
+ },
+ {
+ 'label': 'Calcul des scores par classe (CSV)',
+ 'url_name': 'export_classe_csv',
+ 'help': "Calcule pour chaque classe le score SOMMME DES RANGS / NOMBRE DE COUREURS, et télécharge le classement par score au format CSV.",
+ },
+ {
+ 'label': 'Calcul des scores par classe (PDF)',
+ 'url_name': 'export_classe',
+ 'help': "Calcule pour chaque classe le score SOMMME DES RANGS / NOMBRE DE COUREURS, et télécharge le classement par score au format PDF.",
+ },
+ ]
+ elif course.type == Course.TYPE_MULTI:
+ export_options = [
+ {
+ 'label': 'Export résultats (CSV)',
+ 'url_name': 'export_csv',
+ 'help': "Télécharge un fichier CSV contenant les rangs, noms, prénoms, classes et temps des arrivées. Si vous avez filtré ou groupé la table, seules les lignes visibles seront exportées.",
+ },
+ {
+ 'label': 'Export résultats (PDF)',
+ 'url_name': 'export_pdf',
+ 'help': "Télécharge un fichier PDF contenant les rangs, noms, prénoms, classes et temps des arrivées. Si vous avez filtré ou groupé la table, seules les lignes visibles seront exportées",
+ },
+ ]
+
return render(request, 'course_detail.html', {
'title': f'Course : {course.nom} ({date_str})',
'course': course,
'arrivees': arrivees,
'is_started': is_started,
- 'is_finished': is_finished
+ 'is_finished': is_finished,
+ 'export_options': export_options,
})
+
@login_required
@@ -465,3 +508,101 @@ def generate_dossards_pdf(data, rows, cols):
c.save()
buffer.seek(0)
return buffer
+
+
+@login_required
+def export_classe(request, course_id):
+ """Export PDF du classement par classe pour une course.
+ Le score d'une classe = somme des rangs de la classe / nombre de coureurs de la classe.
+ On ordonne les classes par score croissant (meilleur score en premier).
+ """
+ course = get_object_or_404(Course, id=course_id, owner=request.user)
+ # Récupérer toutes les arrivées pour cette course
+ arrivees = course.arrivees.select_related('coureur').all()
+
+ # Agréger par classe
+ classe_stats = {}
+ for a in arrivees:
+ classe = a.coureur.classe or '—'
+ if classe not in classe_stats:
+ classe_stats[classe] = {'sum_rangs': 0, 'count': 0}
+ classe_stats[classe]['sum_rangs'] += (a.rang or 0)
+ classe_stats[classe]['count'] += 1
+
+ # Calculer le score pour chaque classe
+ rows = []
+ for classe, stats in classe_stats.items():
+ count = stats['count']
+ sum_rangs = stats['sum_rangs']
+ score = sum_rangs / count if count else float('inf')
+ rows.append({'classe': classe, 'count': count, 'sum_rangs': sum_rangs, 'score': score})
+
+ # Trier par score asc
+ rows.sort(key=lambda r: (r['score'], r['classe']))
+
+ # Générer PDF
+ response = HttpResponse(content_type='application/pdf')
+ response['Content-Disposition'] = f'attachment; filename="course_{course_id}_classement_par_classe.pdf"'
+ p = canvas.Canvas(response, pagesize=A4)
+ width, height = A4
+ y = height - 50
+ p.setFont("Helvetica-Bold", 16)
+ p.drawString(50, y, f"Classement par classe - {course.nom} ({course.date})")
+ y -= 30
+ p.setFont("Helvetica-Bold", 12)
+ p.drawString(50, y, "Classe")
+ p.drawString(220, y, "Nombre")
+ p.drawString(320, y, "Somme des rangs")
+ p.drawString(460, y, "Score")
+ y -= 20
+ p.setFont("Helvetica", 12)
+ for r in rows:
+ p.drawString(50, y, str(r['classe']))
+ p.drawString(220, y, str(r['count']))
+ p.drawString(320, y, str(r['sum_rangs']))
+ p.drawString(460, y, f"{r['score']:.2f}")
+ y -= 18
+ if y < 50:
+ p.showPage()
+ y = height - 50
+ p.save()
+ return response
+
+
+@login_required
+def export_classe_csv(request, course_id):
+ """Export CSV du classement par classe pour une course.
+ Le score d'une classe = somme des rangs de la classe / nombre de coureurs de la classe.
+ On ordonne les classes par score croissant (meilleur score en premier).
+ """
+ course = get_object_or_404(Course, id=course_id, owner=request.user)
+ arrivees = course.arrivees.select_related('coureur').all()
+
+ # Agréger par classe
+ classe_stats = {}
+ for a in arrivees:
+ classe = a.coureur.classe or '—'
+ if classe not in classe_stats:
+ classe_stats[classe] = {'sum_rangs': 0, 'count': 0}
+ classe_stats[classe]['sum_rangs'] += (a.rang or 0)
+ classe_stats[classe]['count'] += 1
+
+ # Calculer le score pour chaque classe
+ rows = []
+ for classe, stats in classe_stats.items():
+ count = stats['count']
+ sum_rangs = stats['sum_rangs']
+ score = sum_rangs / count if count else float('inf')
+ rows.append({'classe': classe, 'count': count, 'sum_rangs': sum_rangs, 'score': score})
+
+ # Trier par score asc
+ rows.sort(key=lambda r: (r['score'], r['classe']))
+
+ # Préparer la réponse CSV
+ response = HttpResponse(content_type='text/csv')
+ response['Content-Disposition'] = f'attachment; filename="course_{course_id}_classement_par_classe.csv"'
+ writer = csv.writer(response)
+ writer.writerow(['Classe', 'Nombre', 'Somme des rangs', 'Score'])
+ for r in rows:
+ writer.writerow([r['classe'], r['count'], r['sum_rangs'], f"{r['score']:.2f}"])
+ return response