Browse Source

Amélioration de la gestion des dossards et de l'import des coureurs

master
scayac 3 months ago
parent
commit
0edcc52eea
  1. 1
      main/forms.py
  2. 0
      main/forms_coureur_import.py
  3. BIN
      main/templates/arrivees_tbody.html
  4. 41
      main/templates/course_detail.html
  5. 144
      main/templates/dossards.html
  6. 4
      main/templates/scan.html
  7. 2
      main/templates/scan_result.html
  8. 70
      main/views.py

1
main/forms.py

@ -25,7 +25,6 @@ class CourseForm(forms.ModelForm): @@ -25,7 +25,6 @@ class CourseForm(forms.ModelForm):
return cleaned_data
class DossardForm(forms.Form):
csv_file = forms.FileField(label="Fichier CSV (nom;classe)")
rows = forms.IntegerField(label="Étiquettes par colonne", min_value=1, initial=2)
cols = forms.IntegerField(label="Étiquettes par ligne", min_value=1, initial=2)

0
main/forms_coureur_import.py

BIN
main/templates/arrivees_tbody.html

Binary file not shown.

41
main/templates/course_detail.html

@ -74,8 +74,9 @@ @@ -74,8 +74,9 @@
<table class="table table-striped" id="arriveesTable">
<thead>
<tr>
<th>Rang</th>
<th style="width: 1%; white-space: nowrap;">Rang</th>
<th>Nom</th>
<th>Prénom</th>
<th>Classe</th>
<th>Temps</th>
</tr>
@ -85,6 +86,7 @@ @@ -85,6 +86,7 @@
<tr>
<td>{{ a.rang }}</td>
<td>{{ a.coureur.nom }}</td>
<td>{{ a.coureur.prenom }}</td>
<td>{{ a.coureur.classe }}</td>
<td>{% if a.temps %}{{ a.temps|seconds_to_hms }}{% endif %}</td>
</tr>
@ -94,6 +96,7 @@ @@ -94,6 +96,7 @@
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{% endfor %}
</tbody>
@ -127,11 +130,43 @@ socket.onmessage = function(e) { @@ -127,11 +130,43 @@ socket.onmessage = function(e) {
}
// Vérifie le format des données reçues
let rowData;
function formatHMS(seconds) {
// Copie la logique du templatetag temps_format
if (seconds === undefined || seconds === null || seconds === "") return "--:--:--";
let total_seconds = 0;
if (typeof seconds === 'string' && seconds.includes(':')) {
// déjà formaté
return seconds;
}
if (!isNaN(Number(seconds))) {
total_seconds = Math.floor(Number(seconds));
} else {
return "--:--:--";
}
let h = Math.floor(total_seconds / 3600);
let m = Math.floor((total_seconds % 3600) / 60);
let s = total_seconds % 60;
return (h < 10 ? '0' : '') + h + 'h' + (m < 10 ? '0' : '') + m + 'm' + (s < 10 ? '0' : '') + s + 's';
}
if (Array.isArray(data)) {
rowData = data;
// Si la ligne reçue n'a pas 5 colonnes, on complète avec des vides
rowData = data.slice(0, 5);
while (rowData.length < 5) rowData.push('');
// Si le temps est en secondes, on le formate
if (rowData.length > 4 && /^\d+$/.test(rowData[4])) {
rowData[4] = formatHMS(rowData[4]);
}
} else if (typeof data === 'object' && data !== null) {
// Transforme l'objet en tableau dans l'ordre attendu
rowData = [data.rang, data.nom || (data.coureur && data.coureur.nom), data.classe || (data.coureur && data.coureur.classe), data.temps];
let temps = data.temps;
if (/^\d+$/.test(temps)) temps = formatHMS(temps);
rowData = [
data.rang,
data.nom || (data.coureur && data.coureur.nom),
data.prenom || (data.coureur && data.coureur.prenom),
data.classe || (data.coureur && data.coureur.classe),
temps
];
} else {
// Format inconnu, ignore
return;

144
main/templates/dossards.html

@ -1,32 +1,140 @@ @@ -1,32 +1,140 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row">
<div class="col-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Importer le fichier CSV</h6>
<div class="card-header py-3 d-flex align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">Liste des coureurs</h6>
<div>
<label for="filterClasse" class="mb-0 mr-2">Filtrer par classe :</label>
<select id="filterClasse" class="form-control d-inline-block" style="width:auto;">
<option value="">Toutes</option>
{% for classe in classes %}
<option value="{{ classe }}">{{ classe }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-success">Générer PDF
<i class="fas fa-file-pdf" title="G\00e9n\00e9rer PDF"></i>
</button>
</form>
{% if error %}
<div class="alert alert-danger mt-3">{{ error }}</div>
{% endif %}
{% if progress %}
<div class="alert alert-info mt-3">{{ progress }}</div>
{% endif %}
{% if pdf_url %}
<a href="{{ pdf_url }}" class="btn btn-primary mt-3">T\00e9l\00e9charger le PDF</a>
{% endif %}
<div class="table-responsive">
<table id="coureursTable" class="table table-bordered table-striped">
<thead>
<tr>
<th>Nom</th>
<th>Prénom</th>
<th>Classe</th>
</tr>
</thead>
<tbody>
{% for c in coureurs %}
<tr data-id="{{ c.id }}">
<td>{{ c.nom }}</td>
<td>{{ c.prenom }}</td>
<td>{{ c.classe }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- BOUTON pour ouvrir le modal -->
<div class="card-footer text-right">
<button class="btn btn-success" id="openDossardsModal">
<i class="fas fa-file-pdf"></i> <span id="mainDossardsCountText">Générer 0 dossards</span>
</button>
</div>
</div>
</div>
</div>
<!-- Modal pour le formulaire dossards -->
<div class="modal fade" id="dossardsModal" tabindex="-1" role="dialog" aria-labelledby="dossardsModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="dossardsModalLabel">Configuration des dossards</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form method="post" id="dossardsForm" action="{% url 'dossards' %}">
{% csrf_token %}
<input type="hidden" name="coureur_ids" id="coureurIdsInput">
<div class="form-row">
<div class="form-group col-6">
<label for="rowsInput">Étiquettes par colonne</label>
<input type="number" class="form-control" id="rowsInput" name="rows" min="1" value="2" required>
</div>
<div class="form-group col-6">
<label for="colsInput">Étiquettes par ligne</label>
<input type="number" class="form-control" id="colsInput" name="cols" min="1" value="2" required>
</div>
</div>
<button type="submit" class="btn btn-success">Générer PDF
<i class="fas fa-file-pdf" title="Générer PDF"></i>
</button>
</form>
{% if error %}
<div class="alert alert-danger mt-3">{{ error }}</div>
{% endif %}
{% if progress %}
<div class="alert alert-info mt-3">{{ progress }}</div>
{% endif %}
{% if pdf_url %}
<a href="{{ pdf_url }}" class="btn btn-primary mt-3">T\00e9l\00e9charger le PDF</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% block extra_js %}
<script src="{% static 'jquery/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'bootstrap/dataTables.bootstrap4.min.js' %}"></script>
<script src="{% static 'jquery/datatables.fr.js' %}"></script>
<script>
$(document).ready(function() {
var table = $('#coureursTable').DataTable({
pageLength: 50,
});
function updateMainDossardsCount() {
var count = table.rows({search:'applied'}).count();
$('#mainDossardsCountText').text('Générer ' + count + ' dossards');
}
updateMainDossardsCount();
table.on('draw', updateMainDossardsCount);
$('#filterClasse').on('change', updateMainDossardsCount);
$('#filterClasse').on('change', function() {
var val = $(this).val();
if(val) {
table.column(2).search('^'+val+'$', true, false).draw();
} else {
table.column(2).search('').draw();
}
});
// Ouvre le modal au clic sur le bouton
$('#openDossardsModal').on('click', function(e) {
e.preventDefault();
$('#dossardsModal').modal('show');
});
// Prépare la soumission du formulaire pour n'envoyer que les IDs filtrés
$('#dossardsForm').on('submit', function(e) {
var ids = [];
table.rows({search:'applied'}).every(function(){
var row = this.node();
var id = $(row).data('id');
if (id) ids.push(id);
});
$('#coureurIdsInput').val(ids.join(','));
// Ferme le modal juste après la soumission
$('#dossardsModal').modal('hide');
// continue submit
});
});
</script>
{% endblock %}
{% endblock %}

4
main/templates/scan.html

@ -156,7 +156,9 @@ function onScanSuccess(decodedText, decodedResult) { @@ -156,7 +156,9 @@ function onScanSuccess(decodedText, decodedResult) {
.then(response => response.text())
.then(html => {
document.getElementById('scanResult').innerHTML = html;
window.scanDebounce = false;
setTimeout(function() {
window.scanDebounce = false;
}, 100); // 100ms de délai avant d'autoriser un nouveau scan
})
.catch(() => {
window.scanDebounce = false;

2
main/templates/scan_result.html

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
{% if result %}
<div class="alert alert-success">
<strong>Arrivée enregistrée :</strong><br>
Nom : {{ result.nom }}<br>
Coureur : {{ result.nom }} {{ result.prenom }}<br>
Classe : {{ result.classe }}<br>
Rang : {{ result.rang }}<br>
Temps : {{ result.temps }}

70
main/views.py

@ -222,43 +222,43 @@ def dossards_view(request): @@ -222,43 +222,43 @@ def dossards_view(request):
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()
# Get the list of Coureur IDs from the POST data
coureur_ids_str = request.POST.get('coureur_ids', '')
rows = int(request.POST.get('rows', 2))
cols = int(request.POST.get('cols', 2))
try:
coureur_ids = [cid for cid in coureur_ids_str.split(',') if cid.strip()]
if not coureur_ids:
error = "Aucun coureur sélectionné."
else:
# Accept both PKs and names for fallback (legacy/fallback)
coureurs = list(Coureur.objects.filter(id__in=coureur_ids))
# If fallback: try by name if not found by id
if len(coureurs) < len(coureur_ids):
missing = set(coureur_ids) - set(str(c.id) for c in coureurs)
for nom in missing:
c = Coureur.objects.filter(nom=nom).first()
if c:
coureurs.append(c)
if not coureurs:
error = "Aucun coureur trouvé pour les IDs fournis."
else:
buffer = generate_dossards_pdf(coureurs, 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)
# Liste des coureurs et des classes distinctes pour la DataTable
coureurs = Coureur.objects.all().order_by('nom', 'prenom', 'classe')
classes = Coureur.objects.values_list('classe', flat=True).distinct().order_by('classe')
return render(request, 'dossards.html', {
'title': 'G\u00e9n\u00e9ration des dossards PDF',
'form': form,
'error': error,
'progress': progress,
'pdf_url': pdf_url
'pdf_url': pdf_url,
'coureurs': coureurs,
'classes': classes,
})
@ -274,9 +274,7 @@ def generate_dossards_pdf(data, rows, cols): @@ -274,9 +274,7 @@ def generate_dossards_pdf(data, rows, 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]
for idx, coureur in enumerate(data):
col = idx % cols
row = (idx // cols) % rows
page = idx // (rows * cols)

Loading…
Cancel
Save