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