2

Let's say I have a Basket model and I want to validate that no more than 5 Items can be added to it:

class Basket(models.Model):
    items = models.ManyToManyField('Item')

    def save(self, *args, **kwargs):
        self.full_clean()
        super(Basket, self).save(*args, **kwargs)

    def clean(self):
        super(Basket, self).clean()
        if self.items.count() > 5:
            raise ValidationError('This basket can\'t have so many items')

But when trying to save a Basket a RuntimeError is thrown because the maximum recursion depth is exceeded.

The error is the following:

ValueError: "<Basket: Basket>" needs to have a value for field "basket" before this many-to-many relationship can be used.

It happens in the if self.items.count() > 5: line.

Apparently Django's intricacies simply won't allow you to validate m2m relationships when saving a model. How can I validate them then?

dabadaba
  • 9,064
  • 21
  • 85
  • 155
  • These things don't have anything to do with each other. If you have a recursion error, you probably have a method that calls itself somewhere. This has nothing to do with validation. Please show the traceback. – Daniel Roseman Sep 22 '17 at 10:16
  • I tracebacked and yeah, the max recursion depth problem is happening elsewhere: when calling the `__unicode__()` method in th traceback itself. But after all the problem is happening in the `clean()` method because I am trying to access the m2m relationship, so nothing changes. – dabadaba Sep 22 '17 at 10:24
  • 3
    Well you still need to show the part of the traceback that shows that error, then. As it is we can have no idea what is going wrong. – Daniel Roseman Sep 22 '17 at 10:26
  • perhaps I am not understanding something properly, but might there an issue with calling `self.full_clean()` in your Basket.save? – Uvar Sep 22 '17 at 10:44
  • please check this answer this will help you mate. https://stackoverflow.com/questions/7986510/django-manytomany-model-validation – bhatt ravii Sep 22 '17 at 11:06
  • Apparently you try validating before saving anything, that is probably causing this error -> The model does not exist yet when trying to validate. – Uvar Sep 22 '17 at 11:28
  • @Uvar yeah I figured as much, but then I am forced with checking post-save and deleting the created object. That's awkward. – dabadaba Sep 22 '17 at 11:44
  • 1
    Possible duplicate of [Django ManyToMany model validation](https://stackoverflow.com/questions/7986510/django-manytomany-model-validation) – Louis Feb 25 '19 at 15:32

2 Answers2

2

You can never validate relationships in the clean method of the model. This is because at clean time, the model may not yet exist, as is the case with your Basket. Something that does not exist, can also not have relationships.

You either need to do your validation on the form data as pointed out by @bhattravii, or call form.save(commit=False) and implement a method called save_m2m, which implements the limit.

To enforce the limit at the model level, you need to listen to the m2m_changed signal. Note that providing feedback to the end user is a lot harder, but it does prevent overfilling the basket through different means.

  • I'll take it. Although both solutions are tied to forms, while I was looking for an answer to model creation that wasn't necessarily related to forms, but to the creation itself no matter how you do it. – dabadaba Sep 22 '17 at 11:47
1

I've been discussing this on the Django Developers list and have in fact tabled a method of doing this for consideration in the Django core in one form or another. The method is not fully tested nor finalised but results for now are very encouraging and I'm employing it on a site of mine with success.

In principle it relies on:

  1. Using PostgreSQL as your database engine (we're fairly sure it won't work on Lightdb or MySQL, but keen for anyone to test this)

  2. Overriding the post() method of your (Class based) view such that it:

    1. Opens an atomic transaction
    2. Saves the form
    3. Saves all the formsets if any
    4. Calls Model.clean() or something else like Model.full_clean()
  3. In your Model then, in the method called in 2.4 above you will see all your many to many and one to many relations in place. You can validate them and throw a ValidationError to see the whole transaction rolled back and no impact on the database.

This is working wonderfully for me:

def post(self, request, *args, **kwargs):
    # The self.object atttribute MUST exist and be None in a CreateView. 
    self.object = None
    self.form = self.get_form()     
    self.success_url = reverse_lazy('view', kwargs=self.kwargs)

    if connection.vendor == 'postgresql':
        if self.form.is_valid():
            try:
                with transaction.atomic():
                    self.object = self.form.save()
                    save_related_forms(self) # A separate routine that collects all the formsets in the request and saves them
                    
                    if (hasattr(self.object, 'full_clean') and callable(self.object.full_clean)):
                        self.object.full_clean()
            except (IntegrityError, ValidationError) as e:
                if hasattr(e, 'error_dict') and isinstance(e.error_dict, dict):
                    for field, errors in e.error_dict.items():
                        for error in errors:
                            self.form.add_error(field, error)
                return self.form_invalid(self.form)                    
                                      
            return self.form_valid(self.form)
        else:
            return self.form_invalid(self.form)
       
    else:
        # The standard Djangop post() method
        if self.form.is_valid():
            self.object = self.form.save()
            save_related_forms(self)
            return self.form_valid(self.form)
        else:
            return self.form_invalid(self.form)

And the conversation on the Developers list is here:

https://groups.google.com/forum/#!topic/django-developers/pQ-8LmFhXFg

if you'd like to contribute any experience you gain from experimenting with this (perhaps with other database backends).

The one big caveat in the above approach is it delegates saving to the post() method which in the default view is done in the form_valid() method, so you need to override form_valid() as well, otherwise a post() like the one above will see you saving the form twice. Which is just a waste of time on an UpdateView but rather disastrous on a CreateView.

Bernd Wechner
  • 1,854
  • 1
  • 15
  • 32