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

67
main/templates/main.html

@ -1,5 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% load temps_format %}
<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">
@ -11,14 +12,23 @@
<ul class="list-group"> <ul class="list-group">
{% for course in courses %} {% for course in courses %}
<li class="list-group-item d-flex justify-content-between align-items-center"> <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> <div>
<a href="{% url 'course_detail' course.id %}" class="btn btn-primary btn-sm mr-2"> <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> <i class="fas fa-eye" title="Détails de la course"></i>
</a> </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> <i class="fas fa-qrcode" title="Accès au mode scan"></i>
</a> </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> </div>
</li> </li>
{% empty %} {% empty %}
@ -36,7 +46,27 @@
</div> </div>
</div> </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 class="col-12">
<div id="newCourseModal" class="modal" tabindex="-1" role="dialog" style="display:none;"> <div id="newCourseModal" class="modal" tabindex="-1" role="dialog" style="display:none;">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
@ -71,6 +101,37 @@
</div> </div>
{% block extra_js %} {% block extra_js %}
<script> <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('btnNewCourse').onclick = function() {
document.getElementById('newCourseModal').style.display = 'block'; document.getElementById('newCourseModal').style.display = 'block';
}; };

15
main/templatetags/temps_format.py

@ -5,6 +5,7 @@ register = template.Library()
import datetime import datetime
def seconds_to_hms(value): def seconds_to_hms(value):
"""Convertit une durée en format HhMMmSSs"""
try: try:
# Si value est un datetime, on convertit en secondes # Si value est un datetime, on convertit en secondes
if isinstance(value, datetime.timedelta): if isinstance(value, datetime.timedelta):
@ -21,4 +22,16 @@ def seconds_to_hms(value):
except (ValueError, TypeError, AttributeError): except (ValueError, TypeError, AttributeError):
return "--:--:--" 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 = [
path('scan/', views.scan_view, name='scan'), path('scan/', views.scan_view, name='scan'),
path('dossards/', views.dossards_view, name='dossards'), path('dossards/', views.dossards_view, name='dossards'),
path('ajax/coureur_autocomplete/', views.coureur_autocomplete, name='coureur_autocomplete'), 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
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils import timezone 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 # Third party
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
@ -23,21 +23,12 @@ from asgiref.sync import async_to_sync
# Local # Local
from .models import Course, Arrivee, Coureur from .models import Course, Arrivee, Coureur
from .forms import CourseForm, ScanForm, DossardForm from .forms import CourseForm, ScanForm, DossardForm
from main.templatetags.temps_format import *
# ===================================== # =====================================
# Fonctions utilitaires # 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 @login_required
@require_GET @require_GET
def coureur_autocomplete(request): def coureur_autocomplete(request):
@ -86,6 +77,17 @@ def main_view(request):
'now': timezone.localdate() '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 # Vues d'export
# ===================================== # =====================================
@ -193,8 +195,11 @@ def course_detail_view(request, course_id):
is_finished = True is_finished = True
return redirect('course_detail', course_id=course.id) 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', { return render(request, 'course_detail.html', {
'title': 'Course : '+course.nom+" ("+str(course.date)+")", 'title': f'Course : {course.nom} ({date_str})',
'course': course, 'course': course,
'arrivees': arrivees, 'arrivees': arrivees,
'is_started': is_started, 'is_started': is_started,
@ -231,12 +236,13 @@ def scan_view(request):
temps = timezone.now() - course.depart temps = timezone.now() - course.depart
rang = Arrivee.objects.filter(course=course).count() + 1 rang = Arrivee.objects.filter(course=course).count() + 1
Arrivee.objects.create(course=course, coureur=coureur, temps=temps, rang=rang) Arrivee.objects.create(course=course, coureur=coureur, temps=temps, rang=rang)
temps = seconds_to_hms(temps)
result = { result = {
'nom': coureur.nom, 'nom': coureur.nom,
'prenom': coureur.prenom, 'prenom': coureur.prenom,
'classe': coureur.classe, 'classe': coureur.classe,
'rang': rang, 'rang': rang,
'temps': str(seconds_to_hms(temps)) 'temps': temps
} }
channel_layer = get_channel_layer() channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)( async_to_sync(channel_layer.group_send)(
@ -257,8 +263,10 @@ def scan_view(request):
if course_id: if course_id:
course = get_object_or_404(Course, id=course_id, owner=request.user) 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', { 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, 'courses': courses,
'result': result, 'result': result,
'error': error, 'error': error,

Loading…
Cancel
Save