|
|
|
@ -31,8 +31,30 @@ def seconds_to_hms(delta: timedelta) -> str: |
|
|
|
seconds = total % 60 |
|
|
|
seconds = total % 60 |
|
|
|
return f"{hours}:{minutes:02d}:{seconds:02d}" |
|
|
|
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): |
|
|
|
def export_csv(request, course_id): |
|
|
|
course = get_object_or_404(Course, id=course_id) |
|
|
|
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) |
|
|
|
@ -52,8 +74,9 @@ def export_csv(request, course_id): |
|
|
|
writer.writerow([a.rang, a.coureur.nom, a.coureur.classe, str(a.temps)]) |
|
|
|
writer.writerow([a.rang, a.coureur.nom, a.coureur.classe, str(a.temps)]) |
|
|
|
return response |
|
|
|
return response |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
def export_pdf(request, course_id): |
|
|
|
def export_pdf(request, course_id): |
|
|
|
course = get_object_or_404(Course, id=course_id) |
|
|
|
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"' |
|
|
|
p = canvas.Canvas(response, pagesize=A4) |
|
|
|
p = canvas.Canvas(response, pagesize=A4) |
|
|
|
@ -102,7 +125,7 @@ def export_pdf(request, course_id): |
|
|
|
|
|
|
|
|
|
|
|
@login_required |
|
|
|
@login_required |
|
|
|
def course_detail_view(request, course_id): |
|
|
|
def course_detail_view(request, course_id): |
|
|
|
course = get_object_or_404(Course, id=course_id) |
|
|
|
course = get_object_or_404(Course, id=course_id, owner=request.user) |
|
|
|
arrivees = course.arrivees.select_related('coureur').order_by('rang') |
|
|
|
arrivees = course.arrivees.select_related('coureur').order_by('rang') |
|
|
|
is_started = course.depart is not None |
|
|
|
is_started = course.depart is not None |
|
|
|
is_finished = course.fin is not None |
|
|
|
is_finished = course.fin is not None |
|
|
|
@ -129,48 +152,30 @@ def course_detail_view(request, course_id): |
|
|
|
'is_started': is_started, |
|
|
|
'is_started': is_started, |
|
|
|
'is_finished': is_finished |
|
|
|
'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 |
|
|
|
@login_required |
|
|
|
def scan_view(request): |
|
|
|
def scan_view(request): |
|
|
|
courses = Course.objects.filter(depart__isnull=False, fin__isnull=True) |
|
|
|
courses = Course.objects.filter(owner=request.user, depart__isnull=False, fin__isnull=True) |
|
|
|
result = None |
|
|
|
result = None |
|
|
|
error = None |
|
|
|
error = None |
|
|
|
course = None |
|
|
|
course = None |
|
|
|
|
|
|
|
|
|
|
|
if request.method == 'POST' and request.headers.get('x-requested-with') == 'XMLHttpRequest': |
|
|
|
if request.method == 'POST' and request.headers.get('x-requested-with') == 'XMLHttpRequest': |
|
|
|
course_id = request.POST.get('course_id') |
|
|
|
course_id = request.POST.get('course_id') |
|
|
|
qrcode = request.POST.get('qrcode') |
|
|
|
qrcode = request.POST.get('qrcode') |
|
|
|
if not course_id or not qrcode: |
|
|
|
if not course_id or not qrcode: |
|
|
|
error = "Param\u00e8tres manquants." |
|
|
|
error = "Param\u00e8tres manquants." |
|
|
|
elif qrcode.count(';') != 1: |
|
|
|
|
|
|
|
error = "Format QR code invalide." |
|
|
|
|
|
|
|
else: |
|
|
|
else: |
|
|
|
nom, classe = qrcode.split(';') |
|
|
|
# ensure user can only scan their own course |
|
|
|
course = get_object_or_404(Course, id=course_id) |
|
|
|
course = get_object_or_404(Course, id=course_id, owner=request.user) |
|
|
|
if not course.depart: |
|
|
|
|
|
|
|
error = "La course n'a pas d\u00e9marr\u00e9." |
|
|
|
# QR code now contains the unique Coureur id |
|
|
|
else: |
|
|
|
coureur = None |
|
|
|
coureur, _ = Coureur.objects.get_or_create(nom=nom.strip(), classe=classe.strip()) |
|
|
|
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(): |
|
|
|
if Arrivee.objects.filter(course=course, coureur=coureur).exists(): |
|
|
|
error = "Ce coureur a d\u00e9j\u00e0 \u00e9t\u00e9 scann\u00e9." |
|
|
|
error = "Ce coureur a d\u00e9j\u00e0 \u00e9t\u00e9 scann\u00e9." |
|
|
|
else: |
|
|
|
else: |
|
|
|
@ -179,6 +184,7 @@ def scan_view(request): |
|
|
|
Arrivee.objects.create(course=course, coureur=coureur, temps=temps, rang=rang) |
|
|
|
Arrivee.objects.create(course=course, coureur=coureur, temps=temps, rang=rang) |
|
|
|
result = { |
|
|
|
result = { |
|
|
|
'nom': coureur.nom, |
|
|
|
'nom': coureur.nom, |
|
|
|
|
|
|
|
'prenom': coureur.prenom, |
|
|
|
'classe': coureur.classe, |
|
|
|
'classe': coureur.classe, |
|
|
|
'rang': rang, |
|
|
|
'rang': rang, |
|
|
|
'temps': str(seconds_to_hms(temps)) |
|
|
|
'temps': str(seconds_to_hms(temps)) |
|
|
|
@ -191,14 +197,17 @@ def scan_view(request): |
|
|
|
'data': result |
|
|
|
'data': result |
|
|
|
} |
|
|
|
} |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if result: |
|
|
|
if result: |
|
|
|
return render(request, 'scan_result.html', {'result': result}) |
|
|
|
return render(request, 'scan_result.html', {'result': result}) |
|
|
|
else: |
|
|
|
else: |
|
|
|
return render(request, 'scan_result.html', {'error': error}) |
|
|
|
return render(request, 'scan_result.html', {'error': error}) |
|
|
|
|
|
|
|
|
|
|
|
else: |
|
|
|
else: |
|
|
|
course_id = request.GET.get('course_id') |
|
|
|
course_id = request.GET.get('course_id') |
|
|
|
if course_id: |
|
|
|
if course_id: |
|
|
|
course = get_object_or_404(Course, id=course_id) |
|
|
|
course = get_object_or_404(Course, id=course_id, owner=request.user) |
|
|
|
|
|
|
|
|
|
|
|
return render(request, 'scan.html', { |
|
|
|
return render(request, 'scan.html', { |
|
|
|
'title': 'Scan course : '+(course.nom+" ("+str(course.date)+")" if course else ''), |
|
|
|
'title': 'Scan course : '+(course.nom+" ("+str(course.date)+")" if course else ''), |
|
|
|
'courses': courses, |
|
|
|
'courses': courses, |
|
|
|
@ -219,11 +228,19 @@ def dossards_view(request): |
|
|
|
rows = form.cleaned_data['rows'] |
|
|
|
rows = form.cleaned_data['rows'] |
|
|
|
cols = form.cleaned_data['cols'] |
|
|
|
cols = form.cleaned_data['cols'] |
|
|
|
try: |
|
|
|
try: |
|
|
|
|
|
|
|
# Build or find Coureur objects for each line. Accept either 'nom;classe' or 'nom;prenom;classe'. |
|
|
|
data = [] |
|
|
|
data = [] |
|
|
|
for line in csv_file.read().decode('utf-8').splitlines(): |
|
|
|
for line in csv_file.read().decode('utf-8').splitlines(): |
|
|
|
if line.count(';') == 1: |
|
|
|
parts = [p.strip() for p in line.split(';') if p.strip() != ''] |
|
|
|
nom, classe = line.split(';') |
|
|
|
if len(parts) == 2: |
|
|
|
data.append((nom.strip(), classe.strip())) |
|
|
|
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) |
|
|
|
total = len(data) |
|
|
|
progress = f"G\u00e9n\u00e9ration des dossards : 0/{total}..." |
|
|
|
progress = f"G\u00e9n\u00e9ration des dossards : 0/{total}..." |
|
|
|
buffer = generate_dossards_pdf(data, rows, cols) |
|
|
|
buffer = generate_dossards_pdf(data, rows, cols) |
|
|
|
@ -258,6 +275,8 @@ def generate_dossards_pdf(data, rows, cols): |
|
|
|
x0, y0 = margin, height - margin - label_h |
|
|
|
x0, y0 = margin, height - margin - label_h |
|
|
|
qr_scale = 0.8 # 80% of the label area for the QR |
|
|
|
qr_scale = 0.8 # 80% of the label area for the QR |
|
|
|
for idx, (nom, classe) in enumerate(data): |
|
|
|
for idx, (nom, classe) in enumerate(data): |
|
|
|
|
|
|
|
# data is now a list of Coureur objects |
|
|
|
|
|
|
|
coureur = data[idx] |
|
|
|
col = idx % cols |
|
|
|
col = idx % cols |
|
|
|
row = (idx // cols) % rows |
|
|
|
row = (idx // cols) % rows |
|
|
|
page = idx // (rows * cols) |
|
|
|
page = idx // (rows * cols) |
|
|
|
@ -269,7 +288,8 @@ def generate_dossards_pdf(data, rows, cols): |
|
|
|
c.setStrokeColorRGB(0, 0, 0) |
|
|
|
c.setStrokeColorRGB(0, 0, 0) |
|
|
|
c.rect(x, y, label_w, label_h) |
|
|
|
c.rect(x, y, label_w, label_h) |
|
|
|
# Generate QR |
|
|
|
# Generate QR |
|
|
|
qr = qrcode.make(f"{nom};{classe}") |
|
|
|
# QR contains the unique coureur id |
|
|
|
|
|
|
|
qr = qrcode.make(f"{coureur.id}") |
|
|
|
qr_img = io.BytesIO() |
|
|
|
qr_img = io.BytesIO() |
|
|
|
qr.save(qr_img, format='PNG') |
|
|
|
qr.save(qr_img, format='PNG') |
|
|
|
qr_img.seek(0) |
|
|
|
qr_img.seek(0) |
|
|
|
@ -279,9 +299,11 @@ def generate_dossards_pdf(data, rows, cols): |
|
|
|
qr_y = y + (label_h - qr_size) / 2 |
|
|
|
qr_y = y + (label_h - qr_size) / 2 |
|
|
|
c.drawInlineImage(qr_pil, qr_x, qr_y, qr_size, qr_size) |
|
|
|
c.drawInlineImage(qr_pil, qr_x, qr_y, qr_size, qr_size) |
|
|
|
c.setFont("Helvetica-Bold", 12) |
|
|
|
c.setFont("Helvetica-Bold", 12) |
|
|
|
c.drawCentredString(x + label_w/2, qr_y - 10, nom) |
|
|
|
# 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.setFont("Helvetica", 10) |
|
|
|
c.drawCentredString(x + label_w/2, qr_y - 24, classe) |
|
|
|
c.drawCentredString(x + label_w/2, qr_y - 24, coureur.classe) |
|
|
|
c.save() |
|
|
|
c.save() |
|
|
|
buffer.seek(0) |
|
|
|
buffer.seek(0) |
|
|
|
return buffer |
|
|
|
return buffer |
|
|
|
|