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: """Format a timedelta as H:MM:SS (handles days too).""" if delta is None: return '' total = int(delta.total_seconds()) hours = total // 3600 minutes = (total % 3600) // 60 seconds = total % 60 return f"{hours}:{minutes:02d}:{seconds:02d}" def export_csv(request, course_id): course = get_object_or_404(Course, id=course_id) 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 def export_pdf(request, course_id): course = get_object_or_404(Course, id=course_id) 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) 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) if request.headers.get('x-requested-with') == 'XMLHttpRequest': from django.template.loader import render_to_string tbody = render_to_string('arrivees_tbody.html', {'arrivees': arrivees}) return HttpResponse(tbody) 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 main_view(request): courses = Course.objects.all() 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) 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 scan_view(request): courses = Course.objects.filter(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." elif qrcode.count(';') != 1: error = "Format QR code invalide." else: nom, classe = qrcode.split(';') course = get_object_or_404(Course, id=course_id) if not course.depart: error = "La course n'a pas d\u00e9marr\u00e9." else: coureur, _ = Coureur.objects.get_or_create(nom=nom.strip(), classe=classe.strip()) 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, '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) 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': form = DossardForm(request.POST, request.FILES) if form.is_valid(): csv_file = form.cleaned_data['csv_file'] rows = form.cleaned_data['rows'] cols = form.cleaned_data['cols'] try: data = [] for line in csv_file.read().decode('utf-8').splitlines(): if line.count(';') == 1: nom, classe = line.split(';') data.append((nom.strip(), classe.strip())) total = len(data) progress = f"G\u00e9n\u00e9ration des dossards : 0/{total}..." buffer = generate_dossards_pdf(data, 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) else: error = "Formulaire invalide." else: form = DossardForm() return render(request, 'dossards.html', { 'title': 'G\u00e9n\u00e9ration des dossards PDF', 'form': form, 'error': error, 'progress': progress, 'pdf_url': pdf_url }) 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, (nom, classe) 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 = qrcode.make(f"{nom};{classe}") 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) c.drawCentredString(x + label_w/2, qr_y - 10, nom) c.setFont("Helvetica", 10) c.drawCentredString(x + label_w/2, qr_y - 24, classe) c.save() buffer.seek(0) return buffer