Tijs
Tijs

Reputation: 1297

Django: form_valid and model validation

I am creating a car reservation application in Django. Users can create a Reservation for a given Car. This happens at /reservation/<car_id>/add. This form does not contain the Car field because the id is given in the URL. Now I want to add a validation to the Reservation model so no reservations can overlap. I have the following code (minimal version):

# models.py

class Car(models.Model):
    name = models.CharField(max_length=200)


class Reservation(models.Model):
    car = models.ForeignKey(Car, on_delete=models.CASCADE)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    start_time = models.DateTimeField()
    end_time = models.DateTimeField()

    def clean(self):
        # Check if overlaps with other reservations in self.car.reservation_set

# views.py

class ReservationAdd(LoginRequiredMixin, CreateView):
    template_name = 'reservation/reservation_add.html'
    model = Reservation
    form_class = ReservationAddForm

    def form_valid(self, form):
        form.instance.car = Car.objects.get(pk=self.kwargs['car_id'])
        form.instance.owner = self.request.user
        return super().form_valid(form)

# forms.py

class ReservationAddForm(forms.ModelForm):

    class Meta:
        fields = ('start_time', 'end_time')
        model = Reservation

Now I get the following error at the Reservation.clean method:

reservation.models.Reservation.car.RelatedObjectDoesNotExist: Reservation has no car.

It seems to me that the clean method gets called before the form_valid method. So my design is not entirely correct I think. What is the correct way to do this?

Upvotes: 2

Views: 2433

Answers (2)

Hari
Hari

Reputation: 1623

I think the better approach is

class ReservationAdd(LoginRequiredMixin, CreateView):

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs["car"] = Car.objects.get(pk=self.kwargs['car_id'])
        return kwargs

and the model form override the __init__ method

class ReservationAddForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        self.car = kwargs.pop("car")
        super().__init__(*args, **kwargs)

   def clean(self):
       # cleaned data
       cleaned_data = super().cleaned_data()
       # access the car instance using self.car
       # to do validation

       return cleaned_data

Upvotes: 1

Daniel Roseman
Daniel Roseman

Reputation: 599630

Yes. form_valid means "code to run once the form is confirmed as valid".

You need to make a couple of changes. Firstly, move your validation to the ReservationAddForm itself; and secondly, pass the Car instance into that form. So:

class ReservationAddForm(forms.ModelForm):

    class Meta:
        fields = ('start_time', 'end_time')
        model = Reservation

    def __init__(self, *args, **kwargs):
        self.car = kwargs.pop('car')
        super().__init__(*args, **kwargs)

    def clean(self):
        ... do something with self.car

class ReservationAdd(LoginRequiredMixin, CreateView):
    template_name = 'reservation/reservation_add.html'
    model = Reservation
    form_class = ReservationAddForm

    def get_form_kwargs(self):
        self.car = Car.objects.get(pk=self.kwargs['car_id'])
        kwargs = super().get_form_kwargs()
        kwargs['car'] = self.car
        return kwargs

    def form_valid(self, form):
        form.instance.car = self.car
        form.instance.owner = self.request.user
        return super().form_valid(form)

Upvotes: 6

Related Questions