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. 118
      main/templates/dossards.html
  6. 2
      main/templates/scan.html
  7. 2
      main/templates/scan_result.html
  8. 56
      main/views.py

1
main/forms.py

@ -25,7 +25,6 @@ class CourseForm(forms.ModelForm):
return cleaned_data return cleaned_data
class DossardForm(forms.Form): 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) rows = forms.IntegerField(label="Étiquettes par colonne", min_value=1, initial=2)
cols = forms.IntegerField(label="Étiquettes par ligne", 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 @@
<table class="table table-striped" id="arriveesTable"> <table class="table table-striped" id="arriveesTable">
<thead> <thead>
<tr> <tr>
<th>Rang</th> <th style="width: 1%; white-space: nowrap;">Rang</th>
<th>Nom</th> <th>Nom</th>
<th>Prénom</th>
<th>Classe</th> <th>Classe</th>
<th>Temps</th> <th>Temps</th>
</tr> </tr>
@ -85,6 +86,7 @@
<tr> <tr>
<td>{{ a.rang }}</td> <td>{{ a.rang }}</td>
<td>{{ a.coureur.nom }}</td> <td>{{ a.coureur.nom }}</td>
<td>{{ a.coureur.prenom }}</td>
<td>{{ a.coureur.classe }}</td> <td>{{ a.coureur.classe }}</td>
<td>{% if a.temps %}{{ a.temps|seconds_to_hms }}{% endif %}</td> <td>{% if a.temps %}{{ a.temps|seconds_to_hms }}{% endif %}</td>
</tr> </tr>
@ -94,6 +96,7 @@
<td></td> <td></td>
<td></td> <td></td>
<td></td> <td></td>
<td></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -127,11 +130,43 @@ socket.onmessage = function(e) {
} }
// Vérifie le format des données reçues // Vérifie le format des données reçues
let rowData; 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)) { 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) { } else if (typeof data === 'object' && data !== null) {
// Transforme l'objet en tableau dans l'ordre attendu // 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 { } else {
// Format inconnu, ignore // Format inconnu, ignore
return; return;

118
main/templates/dossards.html

@ -1,18 +1,79 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %}
{% block content %} {% block content %}
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3 d-flex align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">Importer le fichier CSV</h6> <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>
<div class="card-body"> <div class="card-body">
<form method="post" enctype="multipart/form-data"> <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 %} {% csrf_token %}
{{ form.as_p }} <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 <button type="submit" class="btn btn-success">Générer PDF
<i class="fas fa-file-pdf" title="G\00e9n\00e9rer PDF"></i> <i class="fas fa-file-pdf" title="Générer PDF"></i>
</button> </button>
</form> </form>
{% if error %} {% if error %}
@ -29,4 +90,51 @@
</div> </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 %} {% endblock %}

2
main/templates/scan.html

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

2
main/templates/scan_result.html

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

56
main/views.py

@ -222,43 +222,43 @@ def dossards_view(request):
progress = None progress = None
pdf_url = None pdf_url = None
if request.method == 'POST': if request.method == 'POST':
form = DossardForm(request.POST, request.FILES) # Get the list of Coureur IDs from the POST data
if form.is_valid(): coureur_ids_str = request.POST.get('coureur_ids', '')
csv_file = form.cleaned_data['csv_file'] rows = int(request.POST.get('rows', 2))
rows = form.cleaned_data['rows'] cols = int(request.POST.get('cols', 2))
cols = form.cleaned_data['cols']
try: try:
# Build or find Coureur objects for each line. Accept either 'nom;classe' or 'nom;prenom;classe'. coureur_ids = [cid for cid in coureur_ids_str.split(',') if cid.strip()]
data = [] if not coureur_ids:
for line in csv_file.read().decode('utf-8').splitlines(): error = "Aucun coureur sélectionné."
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: else:
continue # Accept both PKs and names for fallback (legacy/fallback)
coureur, _ = Coureur.objects.get_or_create(nom=nom, prenom=prenom, classe=classe) coureurs = list(Coureur.objects.filter(id__in=coureur_ids))
data.append(coureur) # If fallback: try by name if not found by id
total = len(data) if len(coureurs) < len(coureur_ids):
progress = f"G\u00e9n\u00e9ration des dossards : 0/{total}..." missing = set(coureur_ids) - set(str(c.id) for c in coureurs)
buffer = generate_dossards_pdf(data, rows, cols) 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 = HttpResponse(buffer, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="dossards_{rows}x{cols}.pdf"' response['Content-Disposition'] = f'attachment; filename="dossards_{rows}x{cols}.pdf"'
return response return response
except Exception as e: except Exception as e:
error = str(e) error = str(e)
else: # Liste des coureurs et des classes distinctes pour la DataTable
error = "Formulaire invalide." coureurs = Coureur.objects.all().order_by('nom', 'prenom', 'classe')
else: classes = Coureur.objects.values_list('classe', flat=True).distinct().order_by('classe')
form = DossardForm()
return render(request, 'dossards.html', { return render(request, 'dossards.html', {
'title': 'G\u00e9n\u00e9ration des dossards PDF', 'title': 'G\u00e9n\u00e9ration des dossards PDF',
'form': form,
'error': error, 'error': error,
'progress': progress, '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):
label_h = (height - 2 * margin) / rows label_h = (height - 2 * margin) / rows
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, coureur 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)

Loading…
Cancel
Save