Browse Source

Gestion courses multi tours

Nettoyage views.py
Amélioration affichage page principale
Update .gitignore
master
scayac 2 months ago
parent
commit
ddfd32ceb8
  1. 2
      .gitignore
  2. 26
      main/migrations/0002_alter_arrivee_unique_together_arrivee_tour_and_more.py
  3. 6
      main/models.py
  4. 70
      main/templates/course_detail.html
  5. 26
      main/templates/main.html
  6. 9
      main/templates/scan_result.html
  7. 325
      main/views.py

2
.gitignore vendored

@ -6,6 +6,6 @@ @@ -6,6 +6,6 @@
staticfiles/*
__pycache__
__init__.py
db.sqlite3
*.sqlite3
*.crt
*.key

26
main/migrations/0002_alter_arrivee_unique_together_arrivee_tour_and_more.py

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
# Generated by Django 5.2.7 on 2025-10-08 18:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0001_initial'),
]
operations = [
migrations.AlterUniqueTogether(
name='arrivee',
unique_together=set(),
),
migrations.AddField(
model_name='arrivee',
name='tour',
field=models.PositiveIntegerField(default=1),
),
migrations.AlterUniqueTogether(
name='arrivee',
unique_together={('course', 'coureur', 'tour')},
),
]

6
main/models.py

@ -43,10 +43,14 @@ class Arrivee(models.Model): @@ -43,10 +43,14 @@ class Arrivee(models.Model):
coureur = models.ForeignKey('Coureur', on_delete=models.CASCADE)
temps = models.DurationField()
rang = models.PositiveIntegerField()
# numéro de tour (pour les courses de type multi)
tour = models.PositiveIntegerField(default=1)
date_arrivee = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('course', 'coureur')
# Allow multiple arrivals for the same coureur in the same course
# by including the tour in the uniqueness constraint
unique_together = ('course', 'coureur', 'tour')
ordering = ['rang']
def __str__(self):

70
main/templates/course_detail.html

@ -53,6 +53,9 @@ @@ -53,6 +53,9 @@
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">Arrivées</h6>
<div>
{% 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">
@ -74,7 +77,7 @@ @@ -74,7 +77,7 @@
<table class="table table-striped" id="arriveesTable">
<thead>
<tr>
<th style="width: 1%; white-space: nowrap;">Rang</th>
<th style="width: 1%; white-space: nowrap;">{% if course.type == 'multi' %}Tour{% else %}Rang{% endif %}</th>
<th>Nom</th>
<th>Prénom</th>
<th>Classe</th>
@ -84,7 +87,7 @@ @@ -84,7 +87,7 @@
<tbody>
{% for a in arrivees %}
<tr>
<td>{{ a.rang }}</td>
<td>{% if course.type == 'multi' %}{{ a.tour }}{% else %}{{ a.rang }}{% endif %}</td>
<td>{{ a.coureur.nom }}</td>
<td>{{ a.coureur.prenom }}</td>
<td>{{ a.coureur.classe }}</td>
@ -115,7 +118,11 @@ @@ -115,7 +118,11 @@
<script src="{% static 'bootstrap/dataTables.bootstrap4.min.js' %}"></script>
<script src="{% static 'jquery/datatables.fr.js' %}"></script>
<script>
// Style for inserted group header rows
$('<style>').prop('type', 'text/css').html('\n #arriveesTable tbody tr.group td { background: #f8f9fa; font-weight: 600; }\n').appendTo('head');
const courseId = "{{ course.id }}";
const courseType = "{{ course.type }}";
const wsScheme = window.location.protocol === "https:" ? "wss" : "ws";
const wsUrl = `${wsScheme}://${window.location.host}/ws/course/${courseId}/`;
const socket = new WebSocket(wsUrl);
@ -133,14 +140,15 @@ socket.onmessage = function(e) { @@ -133,14 +140,15 @@ socket.onmessage = function(e) {
let rowData;
if (typeof data === 'object' && data !== null) {
// Transforme l'objet en tableau dans l'ordre attendu
rowData = [
data.rang,
data.nom || (data.coureur && data.coureur.nom),
data.prenom || (data.coureur && data.coureur.prenom),
data.classe || (data.coureur && data.coureur.classe),
data.temps
];
// Transforme l'objet en tableau dans l'ordre attendu (tour/rang, nom, prenom, classe, temps)
let firstCell = (courseType === 'multi') ? data.tour : (data.rang || data.tour);
rowData = [
firstCell,
data.nom || (data.coureur && data.coureur.nom),
data.prenom || (data.coureur && data.coureur.prenom),
data.classe || (data.coureur && data.coureur.classe),
data.temps
];
} else {
// Format inconnu, ignore
return;
@ -156,7 +164,47 @@ document.getElementById('btnFinish').onclick = function() { @@ -156,7 +164,47 @@ document.getElementById('btnFinish').onclick = function() {
// Initialisation DataTables au chargement
$(document).ready(function() {
$('#arriveesTable').DataTable();
// Initialize DataTable with options
var groupBy = false;
var table = $('#arriveesTable').DataTable({
order: [],
pageLength: 25,
drawCallback: function(settings) {
var api = this.api();
// Remove previously inserted group rows
api.rows().nodes().to$().filter('tr.group').remove();
if (!groupBy) return;
var rows = api.rows({page:'current'}).nodes();
var last = null;
api.column(1, {page:'current'} ).data().each(function(name, i){
var prenom = api.column(2, {page:'current'} ).data()[i];
var key = name + '|' + prenom;
if (last !== key) {
$(rows).eq(i).before(
'<tr class="group"><td colspan="5">'+ $('<div>').text(name + ' ' + prenom).html() +'</td></tr>'
);
last = key;
}
});
}
});
// Keep the current ordering so we can restore it
var storedOrder = table.order();
$('#btnGroup').on('click', function(){
groupBy = !groupBy;
$(this).text(groupBy ? 'Désactiver le groupement' : 'Grouper par coureur');
if (groupBy) {
// Sort by Nom (col 1) then Prénom (col 2) to make grouping contiguous
table.order([[1, 'asc'], [2, 'asc']]);
} else {
// Restore previous ordering
table.order(storedOrder);
}
table.draw();
});
});
// Export CSV/PDF des données filtrées
function getVisibleRows() {

26
main/templates/main.html

@ -12,13 +12,21 @@ @@ -12,13 +12,21 @@
<ul class="list-group">
{% for course in courses %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>{{ course.nom }} ({{ course.date|format_date }})
{% if course.type == 'unique' %}
<span class="badge badge-primary ml-1">Unique</span>
{% elif course.type == 'multi' %}
<span class="badge badge-info ml-1">Multi</span>
{% endif %}
</span>
<div class="row align-items-center col-xs-12 col-lg-4">
<span>{{ course.nom }}<br/>{{ course.date|format_date }}<br/>{{ course.scan_count }} scan{% if course.scan_count > 1 %}s{% endif %}</span>
{% if course.type == 'unique' %}
<span class="badge badge-primary ml-2 mr-1">Unique</span>
{% elif course.type == 'multi' %}
<span class="badge badge-info ml-2 mr-1">Multi</span>
{% endif %}
{% if course.status == 'not_started' %}
<span class="badge badge-danger ml-1 mr-1">En attente</span>
{% elif course.status == 'ongoing' %}
<span class="badge badge-success ml-1 mr-1">En cours</span>
{% elif course.status == 'finished' %}
<span class="badge badge-secondary ml-1 mr-1">Terminée</span>
{% endif %}
</div>
<div>
<a href="{% url 'course_detail' course.id %}" class="btn btn-primary btn-sm mr-2">
<i class="fas fa-eye" title="Détails de la course"></i>
@ -102,8 +110,8 @@ @@ -102,8 +110,8 @@
{% block extra_js %}
<script>
// Suppression d'une course avec confirmation via modal
let courseIdToDelete = null;
let courseNomToDelete = '';
var courseIdToDelete = null;
var courseNomToDelete = '';
document.querySelectorAll('.btn-delete-course').forEach(function(btn) {
btn.addEventListener('click', function() {
courseIdToDelete = this.getAttribute('data-course-id');

9
main/templates/scan_result.html

@ -3,8 +3,13 @@ @@ -3,8 +3,13 @@
<strong>Arrivée enregistrée :</strong><br>
Coureur : {{ result.nom }} {{ result.prenom }}<br>
Classe : {{ result.classe }}<br>
Rang : {{ result.rang }}<br>
Temps : {{ result.temps }}
{% if course_type == 'multi' %}
Tour : {{ result.tour }}<br>
Temps (tour) : {{ result.temps }}
{% else %}
Rang : {{ result.rang }}<br>
Temps : {{ result.temps }}
{% endif %}
</div>
{% elif error %}
<div class="alert alert-danger">{{ error }}</div>

325
main/views.py

@ -1,17 +1,29 @@ @@ -1,17 +1,29 @@
# Standard library
# Bibliothèques standard
import csv
import io
from datetime import timedelta
# Django
# Django (importations)
from django.db import models
from django.db.models import Count
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
# Bibliothèques standard
import csv
import io
from datetime import timedelta
# Django (importations)
from django.db import models
from django.db.models import Count
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.utils import timezone
from django.views.decorators.http import require_GET, require_POST
# Third party
# Bibliothèques tierces
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
@ -20,44 +32,59 @@ from PIL import Image @@ -20,44 +32,59 @@ from PIL import Image
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
# Local
# Importations locales
from .models import Course, Arrivee, Coureur
from .forms import CourseForm, ScanForm, DossardForm
from main.templatetags.temps_format import *
from main.templatetags.temps_format import seconds_to_hms
# =====================================
# Fonctions utilitaires
# =====================================
@login_required
@require_GET
def coureur_autocomplete(request):
"""Endpoint AJAX pour l'autocomplétion des coureurs.
Recherche sur nom, prénom, classe et dossard.
Retourne les 10 premiers résultats au format {id, label}."""
q = request.GET.get('q', '').strip()
results = []
if len(q) >= 2:
qs = Coureur.objects.filter(
models.Q(nom__icontains=q) |
models.Q(prenom__icontains=q) |
models.Q(classe__icontains=q) |
models.Q(id__icontains=q)
).order_by('nom', 'prenom')[:10]
for c in qs:
label = f"{c.nom} {c.prenom} ({c.classe}) [dossard: {c.id}]"
results.append({'id': c.id, 'label': label})
return JsonResponse(results, safe=False)
"""Point d'API AJAX pour l'autocomplétion des coureurs.
Recherche par nom, prénom, classe ou identifiant (dossard).
Retourne les 10 premiers résultats sous la forme [{'id': ..., 'label': ...}]."""
q = request.GET.get('q', '').strip()
results = []
if len(q) >= 2:
qs = Coureur.objects.filter(
models.Q(nom__icontains=q) |
models.Q(prenom__icontains=q) |
models.Q(classe__icontains=q) |
models.Q(id__icontains=q)
).order_by('nom', 'prenom')[:10]
for c in qs:
label = f"{c.nom} {c.prenom} ({c.classe})"
results.append({'id': c.id, 'label': label})
return JsonResponse(results, safe=False)
# =====================================
# Vues principales
# =====================================
@login_required
def main_view(request):
"""Page d'accueil listant les courses de l'utilisateur.
Permet aussi la création AJAX de nouvelles courses."""
courses = Course.objects.filter(owner=request.user)
Permet également la création AJAX de nouvelles courses."""
# Annotate each course with the number of scans (arrivees)
courses = Course.objects.filter(owner=request.user).annotate(scan_count=Count('arrivees'))
# Ajoute un attribut simple 'status' à chaque instance de course afin que les
# templates et les appels AJAX puissent connaître l'état : non démarrée / en cours / terminée.
for c in courses:
if c.depart is None:
c.status = 'not_started'
elif c.fin is None:
c.status = 'ongoing'
else:
c.status = 'finished'
if request.method == 'POST' and request.headers.get('x-requested-with') == 'XMLHttpRequest':
nom = request.POST.get('nom')
type_ = request.POST.get('type', 'unique')
@ -69,7 +96,8 @@ def main_view(request): @@ -69,7 +96,8 @@ def main_view(request):
if Course.objects.filter(nom=nom, date=date).exists():
return JsonResponse({'success': False, 'error': "Une course avec ce nom existe déjà aujourd'hui."})
course = Course.objects.create(nom=nom, date=date, type=type_, owner=request.user)
return JsonResponse({'success': True, 'course_id': course.id})
# newly created course hasn't started yet
return JsonResponse({'success': True, 'course_id': course.id, 'status': 'not_started', 'scan_count': 0})
form = CourseForm()
return render(request, 'main.html', {
@ -79,10 +107,12 @@ def main_view(request): @@ -79,10 +107,12 @@ def main_view(request):
'now': timezone.localdate()
})
@login_required
@require_POST
def delete_course(request, course_id):
"""Supprime une course appartenant à l'utilisateur (AJAX)."""
"""Supprime une course appartenant à l'utilisateur via une requête AJAX."""
try:
course = Course.objects.get(id=course_id, owner=request.user)
course.delete()
@ -90,94 +120,98 @@ def delete_course(request, course_id): @@ -90,94 +120,98 @@ def delete_course(request, course_id):
except Course.DoesNotExist:
return JsonResponse({'success': False, 'error': "Course introuvable ou non autorisée."})
# =====================================
# Vues d'export
# =====================================
@login_required
def export_csv(request, course_id):
"""Export des résultats d'une course en CSV.
Supporte soit l'export direct depuis la base, soit l'export des lignes filtrées envoyées en POST."""
course = get_object_or_404(Course, id=course_id, owner=request.user)
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="course_{course_id}_resultats.csv"'
writer = csv.writer(response)
writer.writerow(['Rang', 'Nom', 'Classe', 'Temps'])
import json
rows_json = request.POST.get('rows')
if request.method == "POST" and rows_json:
try:
rows = json.loads(rows_json)
for row in rows:
writer.writerow(row)
except Exception:
pass
else:
arrivees = course.arrivees.select_related('coureur').order_by('rang')
for a in arrivees:
writer.writerow([a.rang, a.coureur.nom, a.coureur.classe, str(a.temps)])
return response
"""Export CSV des résultats d'une course.
Supporte l'export direct depuis la base ou l'export des lignes filtrées envoyées en POST."""
course = get_object_or_404(Course, id=course_id, owner=request.user)
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="course_{course_id}_resultats.csv"'
writer = csv.writer(response)
writer.writerow(['Rang', 'Nom', 'Classe', 'Temps'])
import json
rows_json = request.POST.get('rows')
if request.method == "POST" and rows_json:
try:
rows = json.loads(rows_json)
for row in rows:
writer.writerow(row)
except Exception:
pass
else:
arrivees = course.arrivees.select_related('coureur').order_by('rang')
for a in arrivees:
writer.writerow([a.rang, a.coureur.nom, a.coureur.classe, str(a.temps)])
return response
@login_required
def export_pdf(request, course_id):
"""Export des résultats d'une course en PDF.
Supporte soit l'export direct depuis la base, soit l'export des lignes filtrées envoyées en POST."""
course = get_object_or_404(Course, id=course_id, owner=request.user)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="course_{course_id}_resultats.pdf"'
# Configuration du document PDF
p = canvas.Canvas(response, pagesize=A4)
width, height = A4
# En-tête
y = height - 50
p.setFont("Helvetica-Bold", 16)
p.drawString(50, y, f"Résultats - {course.nom} ({course.date})")
# En-tête du tableau
y -= 40
p.setFont("Helvetica", 12)
p.drawString(50, y, "Rang")
p.drawString(100, y, "Nom")
p.drawString(300, y, "Classe")
p.drawString(400, y, "Temps")
y -= 20
# Contenu : soit les lignes filtrées, soit toutes les arrivées
import json
rows_json = request.POST.get('rows')
if request.method == "POST" and rows_json:
try:
rows = json.loads(rows_json)
for row in rows:
p.drawString(50, y, str(row[0]))
p.drawString(100, y, str(row[1]))
p.drawString(300, y, str(row[2]))
p.drawString(400, y, str(row[3]))
y -= 20
# Nouvelle page si nécessaire
if y < 50:
p.showPage()
y = height - 50
except Exception:
pass
else:
arrivees = course.arrivees.select_related('coureur').order_by('rang')
for a in arrivees:
p.drawString(50, y, str(a.rang))
p.drawString(100, y, a.coureur.nom)
p.drawString(300, y, a.coureur.classe)
p.drawString(400, y, str(a.temps))
y -= 20
# Nouvelle page si nécessaire
if y < 50:
p.showPage()
y = height - 50
p.save()
return response
"""Export PDF des résultats d'une course.
Supporte l'export direct depuis la base ou l'export des lignes filtrées envoyées en POST."""
course = get_object_or_404(Course, id=course_id, owner=request.user)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="course_{course_id}_resultats.pdf"'
# Configuration du document PDF
p = canvas.Canvas(response, pagesize=A4)
width, height = A4
# En-tête du document
y = height - 50
p.setFont("Helvetica-Bold", 16)
p.drawString(50, y, f"Résultats - {course.nom} ({course.date})")
# En-tête du tableau
y -= 40
p.setFont("Helvetica", 12)
p.drawString(50, y, "Rang")
p.drawString(100, y, "Nom")
p.drawString(300, y, "Classe")
p.drawString(400, y, "Temps")
y -= 20
# Contenu : soit les lignes filtrées envoyées en POST, soit toutes les arrivées en base
import json
rows_json = request.POST.get('rows')
if request.method == "POST" and rows_json:
try:
rows = json.loads(rows_json)
for row in rows:
p.drawString(50, y, str(row[0]))
p.drawString(100, y, str(row[1]))
p.drawString(300, y, str(row[2]))
p.drawString(400, y, str(row[3]))
y -= 20
# Nouvelle page si nécessaire
if y < 50:
p.showPage()
y = height - 50
except Exception:
pass
else:
arrivees = course.arrivees.select_related('coureur').order_by('rang')
for a in arrivees:
p.drawString(50, y, str(a.rang))
p.drawString(100, y, a.coureur.nom)
p.drawString(300, y, a.coureur.classe)
p.drawString(400, y, str(a.temps))
y -= 20
# Nouvelle page si nécessaire
if y < 50:
p.showPage()
y = height - 50
p.save()
return response
@login_required
def course_detail_view(request, course_id):
@ -187,6 +221,7 @@ def course_detail_view(request, course_id): @@ -187,6 +221,7 @@ def course_detail_view(request, course_id):
is_finished = course.fin is not None
if request.method == 'POST':
# Démarrer ou terminer la course via le formulaire de détail
if 'start' in request.POST and not is_started:
course.depart = timezone.now()
course.save()
@ -208,8 +243,10 @@ def course_detail_view(request, course_id): @@ -208,8 +243,10 @@ def course_detail_view(request, course_id):
'is_finished': is_finished
})
@login_required
def scan_view(request):
# Courses démarrées et pas encore terminées (candidates pour le scan)
courses = Course.objects.filter(owner=request.user, depart__isnull=False, fin__isnull=True)
result = None
error = None
@ -219,12 +256,12 @@ def scan_view(request): @@ -219,12 +256,12 @@ def scan_view(request):
course_id = request.POST.get('course_id')
qrcode = request.POST.get('qrcode')
if not course_id or not qrcode:
error = "Param\u00e8tres manquants."
error = "Paramètres manquants."
else:
# ensure user can only scan their own course
# S'assurer que l'utilisateur scanne uniquement ses propres courses
course = get_object_or_404(Course, id=course_id, owner=request.user)
# QR code now contains the unique Coureur id
# Le QR code contient maintenant l'identifiant unique du Coureur
coureur = None
try:
coureur = Coureur.objects.get(id=qrcode.strip())
@ -232,20 +269,55 @@ def scan_view(request): @@ -232,20 +269,55 @@ def scan_view(request):
error = "Coureur introuvable pour ce code QR."
if coureur:
if Arrivee.objects.filter(course=course, coureur=coureur).exists():
error = "Ce coureur a d\u00e9j\u00e0 \u00e9t\u00e9 scann\u00e9."
# Si la course est de type unique : comportement existant (1 arrivée par coureur)
if course.type == Course.TYPE_UNIQUE:
if Arrivee.objects.filter(course=course, coureur=coureur).exists():
error = "Ce coureur a déjà été scanné."
else:
temps = timezone.now() - course.depart
rang = Arrivee.objects.filter(course=course).count() + 1
arr = Arrivee.objects.create(course=course, coureur=coureur, temps=temps, rang=rang, tour=1)
temps_str = seconds_to_hms(temps)
# nombre total de scans pour cette course
scan_count = Arrivee.objects.filter(course=course).count()
result = {
'nom': coureur.nom,
'prenom': coureur.prenom,
'classe': coureur.classe,
'rang': arr.rang,
'temps': temps_str,
'tour': arr.tour,
'scan_count': scan_count
}
# Pour les courses multi, autoriser plusieurs scans par coureur et calculer le tour/temps de tour
else:
temps = timezone.now() - course.depart
# Compter les arrivées précédentes pour ce coureur sur cette course
previous = Arrivee.objects.filter(course=course, coureur=coureur).order_by('tour', 'date_arrivee')
last_arr = previous.last()
if last_arr:
# prochain tour = dernier tour + 1
next_tour = last_arr.tour + 1
lap_time = timezone.now() - last_arr.date_arrivee
else:
# premier scan pour ce coureur : tour 1, temps depuis le départ de la course
next_tour = 1
lap_time = timezone.now() - course.depart
# rang = nombre d'arrivées global + 1
rang = Arrivee.objects.filter(course=course).count() + 1
Arrivee.objects.create(course=course, coureur=coureur, temps=temps, rang=rang)
temps = seconds_to_hms(temps)
arr = Arrivee.objects.create(course=course, coureur=coureur, temps=lap_time, rang=rang, tour=next_tour)
lap_str = seconds_to_hms(lap_time)
scan_count = Arrivee.objects.filter(course=course).count()
result = {
'nom': coureur.nom,
'prenom': coureur.prenom,
'classe': coureur.classe,
'rang': rang,
'temps': temps
'rang': arr.rang,
'temps': lap_str,
'tour': arr.tour,
'lap_seconds': int(lap_time.total_seconds()),
'scan_count': scan_count
}
# Diffuser via le canal websocket
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
f'course_{course.id}',
@ -256,9 +328,9 @@ def scan_view(request): @@ -256,9 +328,9 @@ def scan_view(request):
)
if result:
return render(request, 'scan_result.html', {'result': result})
return render(request, 'scan_result.html', {'result': result, 'course_type': course.type if course else None})
else:
return render(request, 'scan_result.html', {'error': error})
return render(request, 'scan_result.html', {'error': error, 'course_type': course.type if course else None})
else:
course_id = request.GET.get('course_id')
@ -267,12 +339,15 @@ def scan_view(request): @@ -267,12 +339,15 @@ def scan_view(request):
# Formatage de la date pour affichage JJ/MM/AAAA
date_str = format_date(course.date) if course else ''
# nombre actuel de scans pour cette course
scan_count = Arrivee.objects.filter(course=course).count() if course else 0
return render(request, 'scan.html', {
'title': f'Scan course : {course.nom} ({date_str})' if course else '',
'courses': courses,
'result': result,
'error': error,
'course': course
'course': course,
'scan_count': scan_count
})
@login_required
@ -281,7 +356,7 @@ def dossards_view(request): @@ -281,7 +356,7 @@ def dossards_view(request):
progress = None
pdf_url = None
if request.method == 'POST':
# Get the list of Coureur IDs from the POST data
# Récupère la liste des IDs de Coureurs envoyés en POST
coureur_ids_str = request.POST.get('coureur_ids', '')
rows = int(request.POST.get('rows', 2))
cols = int(request.POST.get('cols', 2))
@ -290,9 +365,9 @@ def dossards_view(request): @@ -290,9 +365,9 @@ def dossards_view(request):
if not coureur_ids:
error = "Aucun coureur sélectionné."
else:
# Accept both PKs and names for fallback (legacy/fallback)
# Accepte à la fois les PKs et les noms en secours (compatibilité)
coureurs = list(Coureur.objects.filter(id__in=coureur_ids))
# If fallback: try by name if not found by id
# En cas de fallback : tenter par nom si non trouvé par id
if len(coureurs) < len(coureur_ids):
missing = set(coureur_ids) - set(str(c.id) for c in coureurs)
for nom in missing:
@ -332,7 +407,7 @@ def generate_dossards_pdf(data, rows, cols): @@ -332,7 +407,7 @@ def generate_dossards_pdf(data, rows, cols):
label_w = (width - 2 * margin) / cols
label_h = (height - 2 * margin) / rows
x0, y0 = margin, height - margin - label_h
qr_scale = 0.8 # 80% of the label area for the QR
qr_scale = 0.9 # 90% of the label area for the QR
for idx, coureur in enumerate(data):
col = idx % cols
row = (idx // cols) % rows
@ -341,11 +416,11 @@ def generate_dossards_pdf(data, rows, cols): @@ -341,11 +416,11 @@ def generate_dossards_pdf(data, rows, cols):
c.showPage()
x = x0 + col * label_w
y = y0 - row * label_h
c.setLineWidth(3)
c.setStrokeColorRGB(0, 0, 0)
c.rect(x, y, label_w, label_h)
# Generate QR
# QR contains the unique coureur id
#c.setLineWidth(3)
#c.setStrokeColorRGB(0, 0, 0)
#c.rect(x, y, label_w, label_h)
# Génération du QR
# Le QR contient l'identifiant unique du coureur
qr = qrcode.make(f"{coureur.id}")
qr_img = io.BytesIO()
qr.save(qr_img, format='PNG')

Loading…
Cancel
Save