|
|
|
@ -1,29 +1,35 @@ |
|
|
|
from django.http import JsonResponse |
|
|
|
# Standard library |
|
|
|
from django.views.decorators.http import require_GET |
|
|
|
|
|
|
|
import csv |
|
|
|
import csv |
|
|
|
import io |
|
|
|
import io |
|
|
|
from datetime import timedelta |
|
|
|
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.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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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 |
|
|
|
|
|
|
|
import qrcode |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Local |
|
|
|
from .models import Course, Arrivee, Coureur |
|
|
|
from .models import Course, Arrivee, Coureur |
|
|
|
from django.db import models |
|
|
|
|
|
|
|
from .forms import CourseForm, ScanForm, DossardForm |
|
|
|
from .forms import CourseForm, ScanForm, DossardForm |
|
|
|
|
|
|
|
|
|
|
|
import qrcode |
|
|
|
# ===================================== |
|
|
|
from PIL import Image |
|
|
|
# Fonctions utilitaires |
|
|
|
|
|
|
|
# ===================================== |
|
|
|
|
|
|
|
|
|
|
|
def seconds_to_hms(delta: timedelta) -> str: |
|
|
|
def seconds_to_hms(delta: timedelta) -> str: |
|
|
|
|
|
|
|
"""Convertit une durée timedelta en chaîne formatée HhMMmSSs""" |
|
|
|
if delta is None: |
|
|
|
if delta is None: |
|
|
|
return '' |
|
|
|
return '' |
|
|
|
total = int(delta.total_seconds()) |
|
|
|
total = int(delta.total_seconds()) |
|
|
|
@ -35,10 +41,12 @@ def seconds_to_hms(delta: timedelta) -> str: |
|
|
|
@login_required |
|
|
|
@login_required |
|
|
|
@require_GET |
|
|
|
@require_GET |
|
|
|
def coureur_autocomplete(request): |
|
|
|
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() |
|
|
|
q = request.GET.get('q', '').strip() |
|
|
|
results = [] |
|
|
|
results = [] |
|
|
|
if len(q) >= 2: |
|
|
|
if len(q) >= 2: |
|
|
|
# Recherche sur nom, prénom, classe, dossard (id) |
|
|
|
|
|
|
|
qs = Coureur.objects.filter( |
|
|
|
qs = Coureur.objects.filter( |
|
|
|
models.Q(nom__icontains=q) | |
|
|
|
models.Q(nom__icontains=q) | |
|
|
|
models.Q(prenom__icontains=q) | |
|
|
|
models.Q(prenom__icontains=q) | |
|
|
|
@ -50,19 +58,26 @@ def coureur_autocomplete(request): |
|
|
|
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 |
|
|
|
|
|
|
|
# ===================================== |
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
@login_required |
|
|
|
def main_view(request): |
|
|
|
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) |
|
|
|
courses = Course.objects.filter(owner=request.user) |
|
|
|
|
|
|
|
|
|
|
|
if request.method == 'POST' and request.headers.get('x-requested-with') == 'XMLHttpRequest': |
|
|
|
if request.method == 'POST' and request.headers.get('x-requested-with') == 'XMLHttpRequest': |
|
|
|
from django.http import JsonResponse |
|
|
|
|
|
|
|
nom = request.POST.get('nom') |
|
|
|
nom = request.POST.get('nom') |
|
|
|
date = timezone.localdate() |
|
|
|
date = timezone.localdate() |
|
|
|
if not nom: |
|
|
|
if not nom: |
|
|
|
return JsonResponse({'success': False, 'error': "Le nom de la course est requis."}) |
|
|
|
return JsonResponse({'success': False, 'error': "Le nom de la course est requis."}) |
|
|
|
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\u00e9j\u00e0 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, owner=request.user) |
|
|
|
course = Course.objects.create(nom=nom, date=date, owner=request.user) |
|
|
|
return JsonResponse({'success': True, 'course_id': course.id}) |
|
|
|
return JsonResponse({'success': True, 'course_id': course.id}) |
|
|
|
|
|
|
|
|
|
|
|
form = CourseForm() |
|
|
|
form = CourseForm() |
|
|
|
return render(request, 'main.html', { |
|
|
|
return render(request, 'main.html', { |
|
|
|
'title': 'Accueil', |
|
|
|
'title': 'Accueil', |
|
|
|
@ -71,13 +86,20 @@ def main_view(request): |
|
|
|
'now': timezone.localdate() |
|
|
|
'now': timezone.localdate() |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ===================================== |
|
|
|
|
|
|
|
# Vues d'export |
|
|
|
|
|
|
|
# ===================================== |
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
@login_required |
|
|
|
def export_csv(request, course_id): |
|
|
|
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) |
|
|
|
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"' |
|
|
|
writer = csv.writer(response) |
|
|
|
writer = csv.writer(response) |
|
|
|
writer.writerow(['Rang', 'Nom', 'Classe', 'Temps']) |
|
|
|
writer.writerow(['Rang', 'Nom', 'Classe', 'Temps']) |
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
@ -95,16 +117,22 @@ def export_csv(request, course_id): |
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
@login_required |
|
|
|
def export_pdf(request, course_id): |
|
|
|
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) |
|
|
|
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"' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Configuration du document PDF |
|
|
|
p = canvas.Canvas(response, pagesize=A4) |
|
|
|
p = canvas.Canvas(response, pagesize=A4) |
|
|
|
width, height = A4 |
|
|
|
width, height = A4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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\u00e9sultats - {course.nom} ({course.date})") |
|
|
|
p.drawString(50, y, f"Résultats - {course.nom} ({course.date})") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# En-tête du tableau |
|
|
|
y -= 40 |
|
|
|
y -= 40 |
|
|
|
p.setFont("Helvetica", 12) |
|
|
|
p.setFont("Helvetica", 12) |
|
|
|
p.drawString(50, y, "Rang") |
|
|
|
p.drawString(50, y, "Rang") |
|
|
|
@ -112,6 +140,8 @@ def export_pdf(request, course_id): |
|
|
|
p.drawString(300, y, "Classe") |
|
|
|
p.drawString(300, y, "Classe") |
|
|
|
p.drawString(400, y, "Temps") |
|
|
|
p.drawString(400, y, "Temps") |
|
|
|
y -= 20 |
|
|
|
y -= 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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: |
|
|
|
@ -123,6 +153,7 @@ def export_pdf(request, course_id): |
|
|
|
p.drawString(300, y, str(row[2])) |
|
|
|
p.drawString(300, y, str(row[2])) |
|
|
|
p.drawString(400, y, str(row[3])) |
|
|
|
p.drawString(400, y, str(row[3])) |
|
|
|
y -= 20 |
|
|
|
y -= 20 |
|
|
|
|
|
|
|
# Nouvelle page si nécessaire |
|
|
|
if y < 50: |
|
|
|
if y < 50: |
|
|
|
p.showPage() |
|
|
|
p.showPage() |
|
|
|
y = height - 50 |
|
|
|
y = height - 50 |
|
|
|
@ -136,9 +167,11 @@ def export_pdf(request, course_id): |
|
|
|
p.drawString(300, y, a.coureur.classe) |
|
|
|
p.drawString(300, y, a.coureur.classe) |
|
|
|
p.drawString(400, y, str(a.temps)) |
|
|
|
p.drawString(400, y, str(a.temps)) |
|
|
|
y -= 20 |
|
|
|
y -= 20 |
|
|
|
|
|
|
|
# Nouvelle page si nécessaire |
|
|
|
if y < 50: |
|
|
|
if y < 50: |
|
|
|
p.showPage() |
|
|
|
p.showPage() |
|
|
|
y = height - 50 |
|
|
|
y = height - 50 |
|
|
|
|
|
|
|
|
|
|
|
p.save() |
|
|
|
p.save() |
|
|
|
return response |
|
|
|
return response |
|
|
|
|
|
|
|
|
|
|
|
|