Browse Source

Ajout exports supplémentaires pour les courses

master
scayac 2 months ago
parent
commit
5dc9ee3b8a
  1. 94
      main/templates/course_detail.html
  2. 2
      main/urls.py
  3. 143
      main/views.py

94
main/templates/course_detail.html

@ -57,20 +57,42 @@ @@ -57,20 +57,42 @@
{% if course.type == 'multi' %}
<button id="btnGroup" type="button" class="btn btn-info mb-2">Grouper par coureur</button>
{% endif %}
<form id="exportCsvForm" method="post" action="{% url 'export_csv' course.id %}" style="display:inline;">
{% csrf_token %}
<input type="hidden" name="rows" id="csvRowsInput">
<button type="submit" class="btn btn-success mb-2" id="btnExportCsv">
<i class="fas fa-file-csv" title="Exporter en CSV"></i>
</button>
</form>
<form id="exportPdfForm" method="post" action="{% url 'export_pdf' course.id %}" style="display:inline;">
{% csrf_token %}
<input type="hidden" name="rows" id="pdfRowsInput">
<button type="submit" class="btn btn-danger mb-2" id="btnExportPdf">
<i class="fas fa-file-pdf" title="Exporter en PDF"></i>
</button>
</form>
{% if export_options %}
<!-- Button to open exports modal -->
<button id="btnMoreExports" type="button" class="btn btn-secondary mb-2" data-toggle="modal" data-target="#exportsModal">
<i class="fas fa-file-export"></i>
</button>
<!-- Exports modal -->
<div class="modal fade" id="exportsModal" tabindex="-1" role="dialog" aria-labelledby="exportsModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exportsModalLabel">Choisir un export</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="list-group">
{% 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 %}
<div class="d-flex align-items-center justify-content-between">
<button type="button" class="list-group-item list-group-item-action export-option-btn flex-grow-1 mr-2" data-url="{{ opt_url }}">{{ opt.label }}</button>
<button type="button" class="btn btn-sm btn-outline-secondary help-btn" data-help="{{ opt.help|escapejs }}" title="Aide">?</button>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<div class="card-body">
@ -241,12 +263,48 @@ function getVisibleRows() { @@ -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 method="post"></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($('<input type="hidden" name="csrfmiddlewaretoken">').val(csrf));
}
var input = $('<input type="hidden" name="rows">').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 <div class="modal fade" id="exportHelpModal" tabindex="-1" role="dialog" aria-hidden="true">\n <div class="modal-dialog" role="document">\n <div class="modal-content">\n <div class="modal-header">\n <h5 class="modal-title">Détails de l\'export</h5>\n <button type="button" class="close" data-dismiss="modal" aria-label="Close">\n <span aria-hidden="true">&times;</span>\n </button>\n </div>\n <div class="modal-body">\n ' + $('<div>').text(helpText).html() + '\n </div>\n <div class="modal-footer">\n <button type="button" class="btn btn-secondary" data-dismiss="modal">Fermer</button>\n </div>\n </div>\n </div>\n </div>';
// Remove any existing help modal to avoid duplicates
$('#exportHelpModal').remove();
$('body').append(modalHtml);
$('#exportHelpModal').modal('show');
});
</script>
{% endblock %}

2
main/urls.py

@ -6,6 +6,8 @@ urlpatterns = [ @@ -6,6 +6,8 @@ urlpatterns = [
path('course/<int:course_id>/', views.course_detail_view, name='course_detail'),
path('course/<int:course_id>/export_csv/', views.export_csv, name='export_csv'),
path('course/<int:course_id>/export_pdf/', views.export_pdf, name='export_pdf'),
path('course/<int:course_id>/export_classe_csv/', views.export_classe_csv, name='export_classe_csv'),
path('course/<int:course_id>/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'),

143
main/views.py

@ -256,13 +256,56 @@ def course_detail_view(request, course_id): @@ -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): @@ -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

Loading…
Cancel
Save