Browse Source

Ajout gestion suppression course AJAX

Normalisation conversion date
Ajout badge type course (unique/multi)
master
scayac 2 months ago
parent
commit
f3903e64d7
  1. 2
      main/templates/base.html
  2. 67
      main/templates/main.html
  3. 15
      main/templatetags/temps_format.py
  4. 1
      main/urls.py
  5. 36
      main/views.py

2
main/templates/base.html

@ -62,7 +62,7 @@ @@ -62,7 +62,7 @@
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<span class="navbar-brand font-weight-bold">{{ title }}</span>
<span class="navbar-brand font-weight-bold" style="font-size: 1rem;">{{ title }}</span>
</nav>
{% block content %}{% endblock %}
</div>

67
main/templates/main.html

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% block content %}
{% load temps_format %}
<div class="container-fluid mt-4">
<div class="row">
<div class="col-12">
@ -11,14 +12,23 @@ @@ -11,14 +12,23 @@
<ul class="list-group">
{% for course in courses %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ course.nom }} ({{ course.date }})
<span>{{ course.nom }} ({{ course.date|format_date }})
{% if course.type == 'unique' %}
<span class="badge badge-primary ml-1">Unique</span>
{% elif course.type == 'multi' %}
<span class="badge badge-info ml-1">Multi</span>
{% endif %}
</span>
<div>
<a href="{% url 'course_detail' course.id %}" class="btn btn-primary btn-sm mr-2">
<i class="fas fa-eye" title="Détails de la course"></i>
</a>
<a href="{% url 'scan' %}?course_id={{ course.id }}" class="btn btn-info btn-sm">
<a href="{% url 'scan' %}?course_id={{ course.id }}" class="btn btn-info btn-sm mr-2">
<i class="fas fa-qrcode" title="Accès au mode scan"></i>
</a>
<button class="btn btn-danger btn-sm btn-delete-course" data-course-id="{{ course.id }}" data-course-nom="{{ course.nom }}" title="Supprimer la course" data-toggle="modal" data-target="#deleteCourseModal">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</li>
{% empty %}
@ -36,7 +46,27 @@ @@ -36,7 +46,27 @@
</div>
</div>
</div>
<div class="row">
<!-- Modal de confirmation de suppression -->
<div class="modal fade" id="deleteCourseModal" tabindex="-1" role="dialog" aria-labelledby="deleteCourseModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteCourseModalLabel">Confirmer la suppression</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p id="deleteCourseModalText">Voulez-vous vraiment supprimer cette course ? Cette action est irréversible.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-danger" id="confirmDeleteCourseBtn">Supprimer</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div id="newCourseModal" class="modal" tabindex="-1" role="dialog" style="display:none;">
<div class="modal-dialog" role="document">
@ -71,6 +101,37 @@ @@ -71,6 +101,37 @@
</div>
{% block extra_js %}
<script>
// Suppression d'une course avec confirmation via modal
let courseIdToDelete = null;
let courseNomToDelete = '';
document.querySelectorAll('.btn-delete-course').forEach(function(btn) {
btn.addEventListener('click', function() {
courseIdToDelete = this.getAttribute('data-course-id');
courseNomToDelete = this.getAttribute('data-course-nom');
document.getElementById('deleteCourseModalText').textContent = `Voulez-vous vraiment supprimer la course "${courseNomToDelete}" ? Cette action est irréversible.`;
// Le modal est affiché automatiquement par data-toggle="modal"
});
});
document.getElementById('confirmDeleteCourseBtn').onclick = function() {
if (!courseIdToDelete) return;
fetch(`/course/${courseIdToDelete}/delete/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(r => r.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
alert(data.error || 'Erreur lors de la suppression.');
}
});
// Fermer le modal
$('#deleteCourseModal').modal('hide');
};
document.getElementById('btnNewCourse').onclick = function() {
document.getElementById('newCourseModal').style.display = 'block';
};

15
main/templatetags/temps_format.py

@ -5,6 +5,7 @@ register = template.Library() @@ -5,6 +5,7 @@ register = template.Library()
import datetime
def seconds_to_hms(value):
"""Convertit une durée en format HhMMmSSs"""
try:
# Si value est un datetime, on convertit en secondes
if isinstance(value, datetime.timedelta):
@ -21,4 +22,16 @@ def seconds_to_hms(value): @@ -21,4 +22,16 @@ def seconds_to_hms(value):
except (ValueError, TypeError, AttributeError):
return "--:--:--"
register.filter('seconds_to_hms', seconds_to_hms)
def format_date(value):
"""Convertit une date en format JJ/MM/AAAA"""
try:
if isinstance(value, str):
value = datetime.datetime.strptime(value, '%Y-%m-%d').date()
if isinstance(value, (datetime.date, datetime.datetime)):
return value.strftime('%d/%m/%Y')
return value
except (ValueError, TypeError, AttributeError):
return value
register.filter('seconds_to_hms', seconds_to_hms)
register.filter('format_date', format_date)

1
main/urls.py

@ -9,4 +9,5 @@ urlpatterns = [ @@ -9,4 +9,5 @@ urlpatterns = [
path('scan/', views.scan_view, name='scan'),
path('dossards/', views.dossards_view, name='dossards'),
path('ajax/coureur_autocomplete/', views.coureur_autocomplete, name='coureur_autocomplete'),
path('course/<int:course_id>/delete/', views.delete_course, name='delete_course'),
]

36
main/views.py

@ -9,7 +9,7 @@ from django.http import HttpResponse, JsonResponse @@ -9,7 +9,7 @@ from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.utils import timezone
from django.views.decorators.http import require_GET
from django.views.decorators.http import require_GET, require_POST
# Third party
from reportlab.pdfgen import canvas
@ -23,21 +23,12 @@ from asgiref.sync import async_to_sync @@ -23,21 +23,12 @@ from asgiref.sync import async_to_sync
# Local
from .models import Course, Arrivee, Coureur
from .forms import CourseForm, ScanForm, DossardForm
from main.templatetags.temps_format import *
# =====================================
# Fonctions utilitaires
# =====================================
def seconds_to_hms(delta: timedelta) -> str:
"""Convertit une durée timedelta en chaîne formatée HhMMmSSs"""
if delta is None:
return ''
total = int(delta.total_seconds())
hours = total // 3600
minutes = (total % 3600) // 60
seconds = total % 60
return f"{hours}h{minutes:02d}m{seconds:02d}s"
@login_required
@require_GET
def coureur_autocomplete(request):
@ -86,6 +77,17 @@ def main_view(request): @@ -86,6 +77,17 @@ def main_view(request):
'now': timezone.localdate()
})
@login_required
@require_POST
def delete_course(request, course_id):
"""Supprime une course appartenant à l'utilisateur (AJAX)."""
try:
course = Course.objects.get(id=course_id, owner=request.user)
course.delete()
return JsonResponse({'success': True})
except Course.DoesNotExist:
return JsonResponse({'success': False, 'error': "Course introuvable ou non autorisée."})
# =====================================
# Vues d'export
# =====================================
@ -193,8 +195,11 @@ def course_detail_view(request, course_id): @@ -193,8 +195,11 @@ def course_detail_view(request, course_id):
is_finished = True
return redirect('course_detail', course_id=course.id)
# Formatage de la date pour affichage JJ/MM/AAAA
from main.templatetags.temps_format import format_date
date_str = format_date(course.date)
return render(request, 'course_detail.html', {
'title': 'Course : '+course.nom+" ("+str(course.date)+")",
'title': f'Course : {course.nom} ({date_str})',
'course': course,
'arrivees': arrivees,
'is_started': is_started,
@ -231,12 +236,13 @@ def scan_view(request): @@ -231,12 +236,13 @@ def scan_view(request):
temps = timezone.now() - course.depart
rang = Arrivee.objects.filter(course=course).count() + 1
Arrivee.objects.create(course=course, coureur=coureur, temps=temps, rang=rang)
temps = seconds_to_hms(temps)
result = {
'nom': coureur.nom,
'prenom': coureur.prenom,
'classe': coureur.classe,
'rang': rang,
'temps': str(seconds_to_hms(temps))
'temps': temps
}
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
@ -257,8 +263,10 @@ def scan_view(request): @@ -257,8 +263,10 @@ def scan_view(request):
if course_id:
course = get_object_or_404(Course, id=course_id, owner=request.user)
# Formatage de la date pour affichage JJ/MM/AAAA
date_str = format_date(course.date) if course else ''
return render(request, 'scan.html', {
'title': 'Scan course : '+(course.nom+" ("+str(course.date)+")" if course else ''),
'title': f'Scan course : {course.nom} ({date_str})' if course else '',
'courses': courses,
'result': result,
'error': error,

Loading…
Cancel
Save