Résolue

Django : formulaires vérification de la méthode clean au chargement du formulaire

# Django # Formulaires

Gabriel Trouvé

Mentor

Bonjour,

Je suis fort embêté lol

Code

Premier formulaire

class SlotForm(forms.ModelForm):
    extra_guests = forms.IntegerField(label="Personnes supplémentaires", required=False, initial=0, min_value=0)

    class Meta:
        model = Reservation
        fields = ["start", "end", "extra_guests"]
        widgets = {
            'start': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M'),
            'end': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M'),
        }
        help_texts = {"start": "Choisir un horaire entre 14h et 16h",
                      "end": "Choisir un horaire entre 16h et 18h"}

    def __init__(self, *args, **kwargs):
        self.house = kwargs.pop("house", None)
        super().__init__(*args, **kwargs)

    def clean(self):
        cleaned_data = super().clean()
        start = cleaned_data.get("start")
        end = cleaned_data.get("end")
        guests = cleaned_data.get("extra_guests", 0) + settings.MINI_RESERVATION

        day_reservation_exists = Reservation.objects.filter(start__date=start.date(), house=self.house,
                                                            slot_in_day=True).exists()
        late_night_reservation_exists = Reservation.objects.filter(end__date=start.date(), house=self.house,
                                                                   late=True).exists()
        # Début du créneau du form plus grand ou égal que la fin d'un existant
        # Fin du créneau du form plus petit ou égal que le début d'un existant
        long_term_reservation_exists = Reservation.objects.filter(house=self.house, end__gte=start,
                                                                  start__lte=end).exists()

        # Vérifier que start et end sont définis
        if not start or not end:
            raise ValidationError("Les dates de début et de fin doivent être définies.")

        # Vérifier que le créneau est d'au moins 2 heures
        if end - start < timedelta(hours=2):
            raise ValidationError("La réservation doit être d'au moins 2 heures.")

        # Vérifier que start et end sont sur la même journée
        if start.date() != end.date():
            raise ValidationError("La réservation doit commencer et se terminer le même jour.")

        # Vérifier que le créneau est entre 14h et 18h
        if not (start.time() >= time(14, 0) and end.time() <= time(18, 0)):
            raise ValidationError("La réservation doit être entre 14h et 18h.")

        # Vérifier que l'utilisateur ne réserve pas pour trop de personnes
        if guests > self.house.capacity:
            raise ValidationError(
                f"Capacité maximum de {self.house.capacity} personnes, vous essayez de réserver pour {guests} "
                f"personnes.")

        # Vérifier si une réservation existe ce jour
        if day_reservation_exists or late_night_reservation_exists or long_term_reservation_exists:
            raise ValidationError("Une réservation est déjà prévue ce jour")

        return cleaned_data

Deuxième formulaire

from datetime import time, datetime

from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError

from spa.models import Reservation


class NightReservationForm(forms.Form):
    start = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), label="Arrivée")
    end = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), label="Départ")
    late = forms.BooleanField(label="Départ tardif",
                              help_text="Départ à 16h au lieu de 12h. Supp 25€/personne",
                              widget=forms.CheckboxInput(attrs={"style": "border: 1px solid #000;"}), required=False)
    # Options
    extra_guests = forms.IntegerField(initial=0, required=False, label="Personnes supplémentaires",
                                      help_text="supp 80€ / personne", min_value=0)
    massage_1h = forms.IntegerField(initial=0, required=False, label="Massage 1h", help_text="supp 50€/personne",
                                    min_value=0)
    massage_30min = forms.IntegerField(initial=0, required=False, label="Massage 30 min", help_text="supp 30€/personne",
                                       min_value=0)
    cocktails = forms.IntegerField(initial=0, required=False, label="Fours et Cocktails (2 personnes)",
                                   help_text="supp 40€ pour 2 personnes", min_value=0)
    breakfast = forms.IntegerField(initial=0, required=False, label="Petit Déjeuner",
                                   help_text="supp 35€ pour 2 personnes", min_value=0)
    decoration = forms.BooleanField(label="Décoration", help_text="supp 50€",
                                    widget=forms.CheckboxInput(attrs={"style": "border: 1px solid #000;"}),
                                    required=False)

    def __init__(self, *args, **kwargs):
        self.house = kwargs.pop("house", None)
        super().__init__(*args, **kwargs)

    def clean(self):
        cleaned_data = super().clean()
        start = cleaned_data.get("start")
        end = cleaned_data.get("end")
        late = cleaned_data.get("late")
        guests = cleaned_data.get("extra_guests", 0) + settings.MINI_RESERVATION

        # Vérifier que start et end sont définis
        if not start or not end:
            raise ValidationError("Les dates de début et de fin doivent être définies.")

        # Vérifier que la date de fin est après la date de début
        if end <= start:
            raise ValidationError("La date de fin doit être postérieure à la date de début.")

        # Vérifier la capacité de l'établissement
        if guests > self.house.capacity:
            raise ValidationError(
                f"Capacité maximum de {self.house.capacity} personnes, vous essayez de réserver pour {guests} "
                f"personnes.")

        # Si départ tardif, mais qu'il y a une réservation en journée le lendemain ce n'est pas possible
        if late:
            if Reservation.objects.filter(start__date=end, house=self.house, slot_in_day=True).exists():
                raise ValidationError("Il n'est pas possible de faire un départ tardif sur cette période.")

        # Convertir les dates de début et de fin en datetime avec les heures spécifiques
        start_dt = datetime.combine(start, time(19, 0))  # Début à 19h00
        end_dt = datetime.combine(end, time(16, 0) if late else time(12, 0))

        # Vérifier s'il n'existe pas déjà une réservation
        if Reservation.objects.filter(house=self.house, start__lt=end_dt, end__gt=start_dt).exists():
            raise ValidationError("Il y a déjà une réservation pour ces dates.")

        return cleaned_data

Question

En fait j'ai bien les messages d'erreurs quand il faut à la soumission. Mais j'aimerais faire un truc genre charger clean au chargement de la page (ça me parait pas possible mais on ne sait jamais lol) pour griser les dates et heures non possibles.

J'ai demandé à GPT mais je n'ai pas trop compris lol.

Ce que je demande c'est faisable ?

Merci d'avance :)

Salut Gabriel,

Je vais essayer de reprendre la réponse que je t'avais déjà apportée en message privé.
Je pense que tu prends le problème du mauvais côté : tu souhaites que les dates déjà réservées soient indiquées lorsque le formulaire est soumis. A mon sens, c'est une mauvaise idée pour deux raisons :

  • Niveau utilisation du site : la bonne information n'est pas renvoyée directement et cela peut frustrer l'internaute

  • Niveau expérience utilisateur : cela demande à l'utilisateur de resoumettre n fois le formulaire pour tomber sur la bonne réservation

A ce titre, la bonne logique sera de passer les dates de réservation au moment du chargement de la page. Dans ce cas - et je laisse les experts confirmer, la méthode clean n'est pas utile car elle ne s'applique qu'une fois le formulaire soumis.
Voici un début de solution un peu crado mais qui peut t'orienter vers quelque chose de cool. Ca implique l'utilisation de jQuery.

Dans ton base.html, inclure :

   <link href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" rel="stylesheet"/>
<script src="//code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="//code.jquery.com/ui/1.12.1/jquery-ui.js"></script>

Dans ta vue, inclure (par exemple) et passer au contexte :

#Exemple avec juste la start date
disabled_dates = [DateFormat(reservation.start).format('Y-m-d') for reservervation in reservations]
    print(disabled_dates)

Dans ton forms.py :

    start = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d', id='start_date'), label="Arrivée")

Dans ton template :

<form method="POST">
   {% csrf_token %}
   {{ form.start }}
</form>

Et à passer en script (généré par chatGPT dans ce cas mais ce n'est que quelques lignes) :
<script>
   $(document).ready(function () {
   var disabledDates = {{ disabled_dates|safe }};
   $("#start_date").datepicker({
   beforeShowDay: function (date) {
   var string = jQuery.datepicker.formatDate('yy-mm-dd', date);
   return [disabledDates.indexOf(string) == -1];
   }
   });
   });
</script>

Cet exemple ne prend que la date de départ, mais inclure la date d'arrivée n'est pas forcément plus compliqué.
Voici un exemple de réflexion, à toi de l'adapter.

Bon j'ai testé je n'y arrive pas du tout. Merci en tous cas Vincent. Si jamais il n'y a pas d'autres alternatives on peut clôturer le question.

Pour l'instance je fais comme ça:

j'affiche les dates déjà reservées à côté du formulaire. Comme ça les utilisateurs savent d'office ce qu'ils ne peuvent pas réserver.

Dans tous les cas je garde ma logique backend avec le formulaire car même si j'avais un calendrier grisé le front ça peut se contourner.

Salut Gabriel,

la réponse de Vincent est top mais c'est vrai peut paraitre un peu complexe si tu n'es pas à l'aise avec jQuery ou la manipulation du DOM avec JavaScript. L'idée de montrer les dates réservées à côté du formulaire est une bonne alternative pour informer les utilisateurs sans qu'ils aient à deviner ou tester chaque date. Si tu valides via la front, je suis d'accord avec Vincent, le clean n'est plus nécessaire mais franchement je le laisserais quand même, on ne sait jamais ce qu'un user mal intentionné peut réussir à faire en loosdé =D. Je sais pas comment mais je pense que c'est possible 😅

Donc pour moi c'est une bonne idée de garder ta logique de validation côté backend avec Django clean methods.

Si tu veux améliorer l'expérience utilisateur sans trop de JavaScript, tu pourrais passer les dates réservées au contexte du template et simplement les afficher dans le calendrier sous forme de texte grisé ou barré. Cela pourrait se faire côté serveur, en préparant la liste des jours réservés et en passant cette liste au template où tu utiliserais une simple boucle pour les afficher comme non disponibles. C'est un peu ce que tu fais déjà dans à côté de ton formulaire.

Bref, c'est déjà une bonne pratique d'informer tes utilisateurs des disponibilités à côté du formulaire, et tu as une logique backend solide pour t'assurer que les réservations se passent comme prévu.

Et merci encore pour ta réponse Vincent qui apporte une expertise côté front.

Gabriel Trouvé

Mentor

Merci PA !

Je n'ai pas trop compris. Car oui à côté de mon formulaire je passe les dates.
Et la solution front ce n'est même pas la peine (pour mon level en front), avec toutes les conditions que j'ai...

Toi tu veux les afficher où ? directement dans le calendrier ?
Je crois que j'ai mal compris lol

Merci d'avance ^^

No souci P-A, avec plaisir.
Pour moi le plus simple reste effectivement de pré-filtrer les dates côté back, de les passer au contexte. Ensuite, on gère les dates indisponibles côté front.
A titre perso, je l'aurais fait en JS. Mais la solution en jQuery était la plus "simple".

Vincent: 🤜

Gabriel: oui l'idée c'était ça: de mettre des cases grisées directement sur le calendrier de sélection.

Gabriel Trouvé

Mentor

J'avoue que je n'ai pas le level là pour le calendrier grisé. j'ai du mal à comprendre :s.
Le JS j'en intègre que très en général.

Merci à vous deux en tous cas.

J'espère que ma solution d'écrire les dates à côté conviednra ... :s

Je peux clôturer si rien à redire ? ^^

Pour moi tu peux clôturer, rien à dire côté back et le front on est d'accord qu'il faut du JS.

Gabriel Trouvé

Mentor

Et bien merci ! :)

hello gab comme discute voici une proposition fonctionnelle . je remet dans ce fil pour garder une trace.

https://github.com/p-acDev/datePickerDjango

Gabriel Trouvé

Mentor

oui merci PA ! je fais ça ce soir :)

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.