Résolue

Problème d'envoi de requête AJAX lors du changement de date

# HTMX # Django # JavaScript

Bonjour ! Toujours dans le cadre de mon application de prise de rendez-vous :D

J'explique un peu le contexte de mon application :

J'ai 3 modèles :

models.py

class Service(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class AppointmentDatetime(models.Model):
    thedatetime = models.DateTimeField()
    is_bookable = models.BooleanField(default=True)


    @staticmethod
    def get_dates_times():
        available_date = AppointmentDatetime.objects.filter(is_bookable=True).values_list('thedatetime', flat=True)

        dates = [date.date().strftime('%Y-%m-%d') for date in available_date]
        time_slots = [time.time().strftime('%H:%M') for time in available_date]

        return dates,time_slots

    def __str__(self):
        return self.thedatetime.strftime("%Y-%m-%d %H:%M")


class Appointment(models.Model):
    user = models.ForeignKey(to=User, on_delete=models.CASCADE)
    service = models.ForeignKey(to=Service, on_delete=models.CASCADE)
    is_passed = models.BooleanField(default=False)
    appointment_datetime = models.ForeignKey(
        to=AppointmentDatetime, on_delete=models.CASCADE
    )

    @staticmethod
    def book_appointment(user, service, appointment_datetime):

        appointment = Appointment(
            user=user,
            service=service,
            appointment_datetime=appointment_datetime,
        )

        appointment_datetime.is_bookable = False
        appointment_datetime.save()

        appointment.save()

        return appointment

    @staticmethod
    def can_book_appointment(user, service):
        if Appointment.objects.filter(user=user, service=service).exists():
            return False
        return True

    def __str__(self):
        return f"{self.user.username}"

J'ai stocké volontairement la date et l'heure en un seul enregistrement avec datetime. Je récupère le temps avec une requête qui me donnera tous les time associés à une date donnée (avec is_bookable = True).

Voici mon forms.py :

class AppointmentForm(forms.ModelForm):
    class Meta:
        model = Appointment
        fields = ['service','appointment_datetime']
        widgets = {
            'appointment_datetime': forms.TextInput(attrs={
                'id': 'datetime',
                'placeholder': 'Selectionnez une',
                'class': 'block w-full px-2 border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-l-md shadow-sm',
                'required': True
            })
               }

Voici la vue get_times :

views.py

def get_times(request):
    date = request.GET.get('date')
    if date:
        available_times = AppointmentDatetime.objects.filter(
            thedatetime__date=date, is_bookable=True
        ).values_list("thedatetime", flat=True)

        time_slots = [time.time().strftime("%H:%M") for time in available_times]
    else:
        time_slots = []

    return render(request, 'booking/time_slots.html', {'time_slots': time_slots})

time_slots.html est un "composant" qui va juste contenir les heures dispo pour une date donnée.

Et voici mon template booking.html où il y aura le formulaire django avec flatpicker ( désolé pour le désordre avec des scripts partout, je testais simplement plein de trucs)

booking.html

{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block content %}
<button hx-get="{% url 'booking:test_response' %}" hx-target="#result" hx-trigger="click" id="test-button">Click me</button>
<div id="result"></div>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Voir les horaires disponibles</button>
</form>
<div id="time-slots">
<!-- Les créneaux horaires -->
</div>
<link href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://unpkg.com/[email protected]"></script>
<script>
    document.addEventListener('DOMContentLoaded', function() {
        flatpickr("#datetime", {
            enable: {{ dates|safe }},
            dateFormat: "Y-m-d",
            onChange: function(selectedDates, dateStr, instance) {
                console.log("La data a changé: " + dateStr); 
                var dateInput = document.getElementById('datetime');
                dateInput.setAttribute('hx-get', "{% url 'booking:get_times' %}?date=" + dateStr);
                dateInput.setAttribute('hx-trigger', 'change');
                dateInput.setAttribute('hx-target', '#time-slots');


            }
        });
    });
</script>
{% endblock %}

Donc là, j'ajoute avec JS les attributs HTMX afin de faire une requête AJAX qui me renvoie " time_slots". Les attributs sont bien ajoutés, et j'ai le console.log qui s'affiche bien lorsque je change une date dans mon formulaire. Cependant, la requête AJAX n'est pas envoyée lorsque je change de date et je n'ai donc pas l'affichage de time_slots qui se fait. Je ne trouve pas de solution à cela.

Pour mieux illsutrer, je souhaite que ceci :

time_slots.html

{% if time_slots %}
    <ul>
        {% for time in time_slots %}
            <li>{{ time }}</li>
        {% endfor %}
    </ul>
{% else %}
    <p>Aucun créneau horaire disponible pour cette date.</p>
{% endif %}

S'affiche ici dans booking.html :

<div id="time-slots">
<!-- Les créneaux horaires -->
</div>

Et aussi, voici le input de flatpicker avant mon event onChange :

<input class="block w-full px-2 border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-l-md shadow-sm flatpickr-input" id="datetime" name="appointment_datetime" placeholder="Selectionnez une" readonly="readonly" required="" type="text"/>

et le voici après :

<input class="block w-full px-2 border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-l-md shadow-sm flatpickr-input" hx-get="/booking/get-times/?date=2024-08-07" hx-target="#time-slots" hx-trigger="change" id="datetime" name="appointment_datetime" placeholder="Selectionnez une" readonly="readonly" required="" type="text"/>

Thibault houdon

Mentor

Salut Yanis !

Beau problème que tu as ici ^^ En fait si je ne me trompe pas le souci vient du fait que tu ajoutes dynamiquement les données pour HTMX. Ça ne fonctionne pas car HTMX a besoin de "register" ces événements dans sa boucle, si tu fais juste ajouter des attributs dynamiquement ça ne fonctionnera pas.

Ce que je te conseille déjà pour vérifier que le problème est bien là c'est de passer par une requête AJAX "normale", sans HTMX, avec fetch, quelque chose comme ça :

{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block content %}
    <form method="POST">
        {% csrf_token %}
        {{ form.as_p }}
    </form>
<div id="time-slots"></div> <!-- conteneur des créneaux horaires -->
<script>
        document.addEventListener('DOMContentLoaded', function() {
            flatpickr("#datetime", {
                enable: {{ dates|safe }},
                dateFormat: "Y-m-d",
                onChange: function(selectedDates, dateStr, instance) {
                    console.log("La data a changé: " + dateStr); 

                    // Faire une requête AJAX manuellement
                    fetch("{% url 'booking:get_times' %}?date=" + dateStr)
                        .then(response => response.text())
                        .then(html => {
                            document.getElementById('time-slots').innerHTML = html;
                        });
                }
            });
        });
    </script>
{% endblock %}

Je viens tout juste d'essayer et ça marche avec fetch ! J'arrive à récuperer les bonnes données. Je n'ai jamais fait de requête ajax telle quelle, je ne sais donc pas si c'est ce qui est fait en arrière plan par hx-get hx-trigger et hx-target et si je peux tout simplement laisser comme ça vu que ça marche...

Thibault houdon

Mentor

Salut Yanis !

HTMX te permet en fait d'éviter d'avoir à écrire du javascript un peu verbeux à chaque fois. Tu retrouves dans ce code les trois éléments (hx-get, hx-trigger et hx-target).

Le trigger se fait avec l'événement onChange, qui ici est appliqué sur le flatpickr :

onChange: function(selectedDates, dateStr, instance) {

Tu fais ensuite une requête vers une URL en back-end (l'équivalent du hx-get) :

fetch("{% url 'booking:get_times' %}?date=" + dateStr)

Si tu voulais faire un hx-post, il faudrait juste définir des options différentes, par exemple :

const options = {
  method: 'POST', // Méthode HTTP
  headers: {
    'Content-Type': 'application/json' // Type de contenu
  },
  body: JSON.stringify(data) // Convertir les données en chaîne JSON
};

// Effectuer la requête POST
fetch(url, options)

Et le hx-target se trouve après la requête, dans le then :

.then(html => {
    document.getElementById('time-slots').innerHTML = html;
});

Tu récupères l'élément dans lequel tu souhaites intégrer le HTML retourné par la vue : document.getElementById('time-slots') et tu changes son innerHTML.

J'espère que ça te permet d'y voir plus clair :)

Je te conseille de regarder un peu l'API Fetch de JavaScript, et JavaScript en général, c'est toujours bon d'avoir quelques bases sur ça avant d'utiliser un framework qui te permet de faire abstraction de tout ça.

Inscris-toi

(c'est gratuit !)

Inscris-toi

Tu dois créer un compte pour participer aux discussions.

Créer un compte

Rechercher sur le site

Formulaire de contact

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.