You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

309 lines
9.6 KiB

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}"
@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)
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 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':
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:
# Build or find Coureur objects for each line. Accept either 'nom;classe' or 'nom;prenom;classe'.
data = []
for line in csv_file.read().decode('utf-8').splitlines():
parts = [p.strip() for p in line.split(';') if p.strip() != '']
if len(parts) == 2:
nom, classe = parts
prenom = ''
elif len(parts) == 3:
nom, prenom, classe = parts
else:
continue
coureur, _ = Coureur.objects.get_or_create(nom=nom, prenom=prenom, classe=classe)
data.append(coureur)
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):
# data is now a list of Coureur objects
coureur = data[idx]
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