182

I'm just curious if anyone knows if there's good reason why django's orm doesn't call 'full_clean' on a model unless it is being saved as part of a model form.

Note that full_clean() will not be called automatically when you call your model’s save() method. You’ll need to call it manually when you want to run one-step model validation for your own manually created models. django's full clean doc

(NOTE: quote updated for Django 1.6... previous django docs had a caveat about ModelForms as well.)

Are there good reasons why people wouldn't want this behavior? I'd think if you took the time to add validation to a model, you'd want that validation run every time the model is saved.

I know how to get everything to work properly, I'm just looking for an explanation.

mlissner
  • 17,359
  • 18
  • 106
  • 169
Aaron
  • 4,206
  • 3
  • 24
  • 28
  • 14
    Thank you very much for this question, it stoped me from banging my head against the wall much more time. I created a mixin that might help others. Check out the gist: https://gist.github.com/glarrain/5448253 – glarrain Apr 23 '13 at 23:20
  • And I finally use signal to catch the `pre_save` hook and do `full_clean` on all caught models. – Alfred Huang Sep 07 '15 at 05:52

7 Answers7

68

AFAIK, this is because of backwards compatibility. There are also problems with ModelForms with excluded fields, models with default values, pre_save() signals, etc.

Sources you might be intrested in:

lqc
  • 7,434
  • 1
  • 25
  • 25
  • 4
    The most helpful excerpt (IMHO) from the second reference: "Developing an "automatic" validation option which is both simple enough to actually be useful and robust enough to handle all the edge cases is -- if it's even possible -- far more than can be accomplished on the 1.2 timeframe. Hence, for now, Django doesn't have any such thing, and won't have it in 1.2. If you think you can make it work for 1.3, your best bet is to work up a proposal, including at least some sample code, along with an explanation of how you'll keep it both simple and robust." – Josh Aug 29 '12 at 16:05
  • 2
    This is quite outdated now, do you know if this is still the case? – gdvalderrama Dec 01 '20 at 16:01
37

Because of the compatibility considering, the auto clean on save is not enabled in django kernel.

If we are starting a new project and want the default save method on Model could clean automatically, we can use the following signal to do clean before every model was saved.

from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save

@receiver(pre_save)
def pre_save_handler(sender, instance, *args, **kwargs):
    instance.full_clean()
Alfred Huang
  • 17,654
  • 32
  • 118
  • 189
  • 3
    Why is this better (or worse) than overriding the save method on some BaseModel (which all others will inherit from) to call full_clean first, then call super()? – J__ Jan 19 '17 at 21:53
  • @J__ Obviously, this method applies on all models, not on a specific one, so you can write once, use everywhere. If you override some BaseModel, you have to write for each model. – Alfred Huang Jan 20 '17 at 01:47
  • However, the signal precludes the possibility of having an exception later on. I don't see why I'd need to make an exception, but it is a disadvantage. – J__ Jan 20 '17 at 02:14
  • https://docs.djangoproject.com/en/1.10/ref/models/instances/#django.db.models.Model.clean See, the clean method should throw an ValidationError when validation failed, it is a recommend procedure. BTW, if the answer helps, please upvote. – Alfred Huang Jan 20 '17 at 02:56
  • Uhm, you might be misinterpreting my use of the word 'exception', but I appreciate the response. – J__ Jan 20 '17 at 02:58
  • 11
    I see two problems with this approach 1) in case of ModelForm's full_clean() would be called twice: by the form and by the signal 2) If the form excludes some fields, they would still be validated by the signal. – mehmet Apr 26 '17 at 22:38
  • 1
    @mehmet So may be you can add these `if send == somemodel, then exclude some fields` in `pre_save_handler` – Cloud Sep 21 '17 at 07:54
  • 6
    For those who are using or considering using this approach: keep in mind that this approach is not officially supported by Django and won't be supported in the foreseeable future (see this comment in Django bug tracker: https://code.djangoproject.com/ticket/29655#comment:3), so you are likely to stumble upon some imperfections like authentication stopping to work (https://code.djangoproject.com/ticket/29655) if you enable validation for all models. You will have to deal with such problems yourself. However, there is no better approach atm. – Evgeny A. Sep 04 '18 at 08:28
  • 1
    Thank you for your recommend, but still we can make small effort to filter the models with our custom whitelist or blacklist, and this problem can be avoided. – Alfred Huang Sep 05 '18 at 06:45
  • 2
    As of Django 2.2.3, this causes an issue with the basic authentication system. You will get a `ValidationError: Session with this Session key already exists`. To avoid this, you need to add an if-statement for `sender in list_of_model_classes` to prevent the signal from overriding Django's default auth models. Define `list_of_model_classes` however you choose – Addison Klinke Jul 11 '19 at 19:20
23

The simplest way to call the full_clean method is just to override the save method in your model:

class YourModel(models.Model):
    ...  
    
    def save(self, *args, **kwargs):
        self.full_clean()
        return super(YourModel, self).save(*args, **kwargs)
M.Void
  • 2,764
  • 2
  • 29
  • 44
  • 2
    Why is this better (or worse) than using a signal? – J__ Jan 19 '17 at 21:51
  • 13
    I see two problems with this approach 1) in case of ModelForm's full_clean() would be called twice: by the form and by the save 2) If the form excludes some fields, they would still be validated by the save. – mehmet Apr 26 '17 at 22:40
  • 1
    Probably better to define a new clean_save() method which calls full_clean() then save() and use this explicitly when manually saving your custom models? – Finn Andersen Nov 11 '21 at 08:13
3

Commenting on @Alfred Huang's answer and coments on it. One might lock the pre_save hook down to an app by defining a list of classes in the current module (models.py) and checking against it in the pre_save hook:

CUSTOM_CLASSES = [obj for name, obj in
        inspect.getmembers(sys.modules[__name__])
        if inspect.isclass(obj)]

@receiver(pre_save)
def pre_save_handler(sender, instance, **kwargs):
    if type(instance) in CUSTOM_CLASSES:
        instance.full_clean()
Peter Shannon
  • 191
  • 1
  • 3
2

Instead of inserting a piece of code that declares a receiver, we can use an app as INSTALLED_APPS section in settings.py

INSTALLED_APPS = [
    # ...
    'django_fullclean',
    # your apps here,
]

Before that, you may need to install django-fullclean using PyPI:

pip install django-fullclean
Flimm
  • 136,138
  • 45
  • 251
  • 267
Alfred Huang
  • 17,654
  • 32
  • 118
  • 189
  • 27
    Why would you `pip install` some app with 4 lines of code in it (check the [source code](https://github.com/fish-ball/django-fullclean/blob/master/django_fullclean/__init__.py)) instead of writing these lines yourself? – David Dahan Apr 21 '17 at 15:14
  • Another library which I haven't tried myself: github.com/danielgatis/django-smart-save – Flimm Aug 23 '19 at 13:41
2

If you have a model that you want to ensure has at least one FK relationship, and you don't want to use null=False because that requires setting a default FK (which would be garbage data), the best way I've come up with is to add custom .clean() and .save() methods. .clean() raises the validation error, and .save() calls the clean. This way the integrity is enforced both from forms and from other calling code, the command line, and tests. Without this, there is (AFAICT) no way to write a test that ensures that a model has a FK relation to a specifically chosen (not default) other model.

class Payer(models.Model):

    name = models.CharField(blank=True, max_length=100)
    # Nullable, but will enforce FK in clean/save:
    payer_group = models.ForeignKey(PayerGroup, null=True, blank=True,)

    def clean(self):
        # Ensure every Payer is in a PayerGroup (but only via forms)
        if not self.payer_group:
            raise ValidationError(
                {'payer_group': 'Each Payer must belong to a PayerGroup.'})

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

    def __str__(self):
        return self.name
shacker
  • 14,712
  • 8
  • 89
  • 89
2

A global pre_save signal can work well if you want to always ensure model validation. However it will run into issues with Django's auth in current versions (3.1.x) and could cause issues with models from other apps you are using.

Elaborating on @Peter Shannon's answer, this version will only validate models inside the module you execute it in, skips validation with "raw" saves and adds a dispatch_uid to avoid duplicate signals.

from django.db.models.signals import pre_save
import inspect
import sys

MODELS = [obj for name, obj in
    inspect.getmembers(sys.modules[__name__], inspect.isclass)]

def validate_model(sender, instance, **kwargs):
    if 'raw' in kwargs and not kwargs['raw']:
        if type(instance) in MODELS:
            instance.full_clean()

pre_save.connect(validate_model, dispatch_uid='validate_models')
Matt Sanders
  • 8,023
  • 3
  • 37
  • 49