import csv import io from datetime import timedelta from django.http import HttpResponse from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.utils import timezone from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 from reportlab.lib.units import mm from channels.layers import get_channel_layer from asgiref.sync import async_to_sync from .models import Course, Arrivee, Coureur from .forms import CourseForm, ScanForm, DossardForm import qrcode from PIL import Image def seconds_to_hms(delta: timedelta) -> str: if delta is None: return '' total = int(delta.total_seconds()) hours = total // 3600 minutes = (total % 3600) // 60 seconds = total % 60 return f"{hours}h{minutes:02d}m{seconds:02d}s" @login_required def main_view(request): courses = Course.objects.filter(owner=request.user) if request.method == 'POST' and request.headers.get('x-requested-with') == 'XMLHttpRequest': from django.http import JsonResponse nom = request.POST.get('nom') date = timezone.localdate() if not nom: return JsonResponse({'success': False, 'error': "Le nom de la course est requis."}) if Course.objects.filter(nom=nom, date=date).exists(): return JsonResponse({'success': False, 'error': "Une course avec ce nom existe d\u00e9j\u00e0 aujourd'hui."}) course = Course.objects.create(nom=nom, date=date, owner=request.user) return JsonResponse({'success': True, 'course_id': course.id}) form = CourseForm() return render(request, 'main.html', { 'title': 'Accueil', 'courses': courses, 'form': form, 'now': timezone.localdate() }) @login_required def export_csv(request, course_id): 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): 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"' p = canvas.Canvas(response, pagesize=A4) width, height = A4 y = height - 50 p.setFont("Helvetica-Bold", 16) p.drawString(50, y, f"R\u00e9sultats - {course.nom} ({course.date})") 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 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 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 if y < 50: p.showPage() y = height - 50 p.save() return response @login_required def course_detail_view(request, course_id): course = get_object_or_404(Course, id=course_id, owner=request.user) arrivees = course.arrivees.select_related('coureur').order_by('rang') is_started = course.depart is not None is_finished = course.fin is not None if request.method == 'POST': if 'start' in request.POST and not is_started: course.depart = timezone.now() course.save() is_started = True elif 'finish' in request.POST and is_started and not is_finished: course.fin = timezone.now() course.save() is_finished = True return redirect('course_detail', course_id=course.id) return render(request, 'course_detail.html', { 'title': 'Course : '+course.nom+" ("+str(course.date)+")", 'course': course, 'arrivees': arrivees, 'is_started': is_started, 'is_finished': is_finished }) @login_required def scan_view(request): courses = Course.objects.filter(owner=request.user, depart__isnull=False, fin__isnull=True) result = None error = None course = None if request.method == 'POST' and request.headers.get('x-requested-with') == 'XMLHttpRequest': course_id = request.POST.get('course_id') qrcode = request.POST.get('qrcode') if not course_id or not qrcode: error = "Param\u00e8tres manquants." else: # ensure user can only scan their own course course = get_object_or_404(Course, id=course_id, owner=request.user) # QR code now contains the unique Coureur id coureur = None try: coureur = Coureur.objects.get(id=qrcode.strip()) except Coureur.DoesNotExist: 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." else: temps = timezone.now() - course.depart rang = Arrivee.objects.filter(course=course).count() + 1 Arrivee.objects.create(course=course, coureur=coureur, temps=temps, rang=rang) result = { 'nom': coureur.nom, 'prenom': coureur.prenom, 'classe': coureur.classe, 'rang': rang, 'temps': str(seconds_to_hms(temps)) } channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( f'course_{course.id}', { 'type': 'send_arrivee', 'data': result } ) if result: return render(request, 'scan_result.html', {'result': result}) else: return render(request, 'scan_result.html', {'error': error}) else: course_id = request.GET.get('course_id') if course_id: course = get_object_or_404(Course, id=course_id, owner=request.user) return render(request, 'scan.html', { 'title': 'Scan course : '+(course.nom+" ("+str(course.date)+")" if course else ''), 'courses': courses, 'result': result, 'error': error, 'course': course }) @login_required def dossards_view(request): error = None progress = None pdf_url = None if request.method == 'POST': # Get the list of Coureur IDs from the POST data coureur_ids_str = request.POST.get('coureur_ids', '') rows = int(request.POST.get('rows', 2)) cols = int(request.POST.get('cols', 2)) try: coureur_ids = [cid for cid in coureur_ids_str.split(',') if cid.strip()] if not coureur_ids: error = "Aucun coureur sélectionné." else: # Accept both PKs and names for fallback (legacy/fallback) coureurs = list(Coureur.objects.filter(id__in=coureur_ids)) # If fallback: try by name if not found by id if len(coureurs) < len(coureur_ids): missing = set(coureur_ids) - set(str(c.id) for c in coureurs) for nom in missing: c = Coureur.objects.filter(nom=nom).first() if c: coureurs.append(c) if not coureurs: error = "Aucun coureur trouvé pour les IDs fournis." else: buffer = generate_dossards_pdf(coureurs, rows, cols) response = HttpResponse(buffer, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="dossards_{rows}x{cols}.pdf"' return response except Exception as e: error = str(e) # Liste des coureurs et des classes distinctes pour la DataTable coureurs = Coureur.objects.all().order_by('nom', 'prenom', 'classe') classes = Coureur.objects.values_list('classe', flat=True).distinct().order_by('classe') return render(request, 'dossards.html', { 'title': 'G\u00e9n\u00e9ration des dossards PDF', 'error': error, 'progress': progress, 'pdf_url': pdf_url, 'coureurs': coureurs, 'classes': classes, }) def generate_dossards_pdf(data, rows, cols): import io from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 buffer = io.BytesIO() c = canvas.Canvas(buffer, pagesize=A4) width, height = A4 margin = 10 * mm 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 for idx, coureur in enumerate(data): col = idx % cols row = (idx // cols) % rows page = idx // (rows * cols) if idx > 0 and row == 0 and col == 0: 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 qr = qrcode.make(f"{coureur.id}") qr_img = io.BytesIO() qr.save(qr_img, format='PNG') qr_img.seek(0) qr_pil = Image.open(qr_img) qr_size = min(label_w, label_h) * qr_scale qr_x = x + (label_w - qr_size) / 2 qr_y = y + (label_h - qr_size) / 2 c.drawInlineImage(qr_pil, qr_x, qr_y, qr_size, qr_size) c.setFont("Helvetica-Bold", 12) # Display nom, prenom, classe full_name = f"{coureur.nom} {coureur.prenom}".strip() c.drawCentredString(x + label_w/2, qr_y - 10, full_name) c.setFont("Helvetica", 10) c.drawCentredString(x + label_w/2, qr_y - 24, coureur.classe) c.save() buffer.seek(0) return buffer