diff --git a/main/views.py b/main/views.py index d96f536..687009d 100644 --- a/main/views.py +++ b/main/views.py @@ -1,146 +1,179 @@ -from django.http import JsonResponse -from django.views.decorators.http import require_GET +# Standard library import csv import io from datetime import timedelta -from django.http import HttpResponse +# Django +from django.db import models +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 +# Third party from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 from reportlab.lib.units import mm - +import qrcode +from PIL import Image from channels.layers import get_channel_layer from asgiref.sync import async_to_sync +# Local from .models import Course, Arrivee, Coureur -from django.db import models from .forms import CourseForm, ScanForm, DossardForm -import qrcode -from PIL import Image +# ===================================== +# Fonctions utilitaires +# ===================================== 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" + """Convertit une durée timedelta en chaîne formatée HhMMmSSs""" + 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 @require_GET def coureur_autocomplete(request): - q = request.GET.get('q', '').strip() - results = [] - if len(q) >= 2: - # Recherche sur nom, prénom, classe, dossard (id) - 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) + """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) + +# ===================================== +# Vues principales +# ===================================== @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() - }) + """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) + + if request.method == 'POST' and request.headers.get('x-requested-with') == 'XMLHttpRequest': + 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éjà 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() + }) + +# ===================================== +# Vues d'export +# ===================================== @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 + """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 @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 + """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 - y = height - 50 - p.setFont("Helvetica-Bold", 16) - p.drawString(50, y, f"R\u00e9sultats - {course.nom} ({course.date})") + # En-tête + y = height - 50 + p.setFont("Helvetica-Bold", 16) + p.drawString(50, y, f"Résultats - {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 + # 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 @login_required def course_detail_view(request, course_id):