37

I'm still trying to understand the correct way to validate a Django model object using a custom validator at the model level. I know that validation is usually done within a form or model form. However, I want to ensure the integrity of my data at the model level if I'm interacting with it via the ORM in the Python shell. Here's my current approach:

from django.db import models
from django.core import validators
from django.core exceptions import ValidationError


def validate_gender(value):
    """ Custom validator """
    if not value in ('m', 'f', 'M', 'F'):
        raise ValidationError(u'%s is not a valid value for gender.' % value)


class Person(models.Model):
    name = models.CharField(max_length=128)
    age = models.IntegerField()
    gender = models.CharField(maxlength=1, validators=[validate_gender])

    def save(self, *args, **kwargs):
        """ Override Person's save """
        self.full_clean(exclude=None)
        super(Person, self).save(*args, **kwargs)

Here are my questions:

  1. Should I create a custom validation function, designate it as a validator, and then override the Person's save() function as I've done above? (By the way, I know I could validate my gender choices using the 'choices' field option but I created 'validate_gender' for the purpose of illustration).

  2. If I really want to ensure the integrity of my data, should I not only write Django unit tests for testing at the model layer but also equivalent database-level unit tests using Python/Psycopg? I've noticed that Django unit tests, which raise ValidationErrors, only test the model's understanding of the database schema using a copy of the database. Even if I were to use South for migrations, any database-level constraints are limited to what Django can understand and translate into a Postgres constraint. If I need a custom constraint that Django can't replicate, I could potentially enter data into my database that violates that constraint if I'm interacting with the database directly via the psql terminal.

Thanks!

mcastle
  • 2,882
  • 3
  • 25
  • 43
Jim
  • 13,430
  • 26
  • 104
  • 155
  • I'm not sure how question 1 is different to your previous questions on this topic. Note that it still doesn't prevent you inserting invalid data using the ORM. Consider `Person.objects.update(gender='a')`. – Alasdair Oct 23 '12 at 20:30
  • 1
    You are correct except that in my previous question I didn't include a custom validator function as I did here. As to your other observation about .update, I guess I now have an additional problem that adds to my confusion. I'm really struggling with understanding what's the proper way to do this. While the Django docs are pretty good, IMHO, they're long on snippets and "hand-waving" but short on complete examples that would help a "newb" like myself understand the correct way to solve this problem. Unfortunately, I work alone and don't have more experienced developers to discuss this with. – Jim Oct 24 '12 at 22:12

2 Answers2

25

I had a similar misunderstanding of the ORM when I first started with Django.

  1. No, don't put self.full_clean() inside of save. Either

A) use a ModelForm (which will cause all the same validation to occur - note: ModelForm.is_valid() won't call Model.full_clean explicitly, but will perform the exact same checks as Model.full_clean). Example:

class PersonForm(forms.ModelForm):
    class Meta:
        model = Person

def add_person(request):
    if request.method == 'POST':
        form = PersonForm(request.POST, request.FILES)
        if form.is_valid():  # Performs your validation, including ``validate_gender``
            person = form.save()
            return redirect('some-other-view')
    else:
        form = PersonForm()
        # ... return response with ``form`` in the context for rendering in a template

Also note, forms aren't for use only in views that render them in templates - they're great for any sort of use, including an API, etc. After running form.is_valid() and getting errors, you'll have form.errors which is a dictionary containing all the errors in the form, including a key called '__all__' which will contain non-field errors.

B) Simply use model_instance.full_clean() in your view (or other logical application layer), instead of using a form, but forms are a nice abstraction for this.

  1. I don't really have a solution to, but I've never run into such a problem, even in large projects (the current project I work with my company on has 146 tables) and I don't suspect it'll be a concern in your case either.
dKen
  • 3,078
  • 1
  • 28
  • 37
orokusaki
  • 55,146
  • 59
  • 179
  • 257
  • 3
    Are you saying that I should use a form or model form to validate my data models even if I'm not actually presenting the model object in an actual template form to a user? Thanks. – Jim Oct 24 '12 at 22:14
  • 2
    @RobertF. - that's correct. The `ModelForm` class is a decent abstraction for model validation, whether updating or creating instances, even when you don't intend on displaying the form in a template. – orokusaki Oct 29 '12 at 01:30
  • 1
    You said to either A) do this, and what is option B ? – rgenito Jul 23 '16 at 03:40
  • By B) Do you mean e.g `model_instance = Person('Natalie'); model_instance.full_clean(); model_instance.save();` ? – stelios Jul 10 '18 at 20:24
  • 1
    @chefarov yes, except without semicolons (don’t use those in Python) and also you would need, e.g., `Person(name=‘Natalie’)` (vs simply passing the name as an ordered argument). – orokusaki Jul 10 '18 at 20:26
  • The import for `forms` is `from django import forms`. Also, after Django 1.8, you need to specify which fields to add to the form. Add the line `fields = '__all__'` to the meta of your form – dKen Feb 25 '22 at 09:41
0

As of django 4.1, self.full_clean by default also runs self.validate_constraints (docs).

After makemigrations, there will be a migrations.AddConstraint statement included in the migration.

In your case:

from django.db import models

class Person(models.Model):
    class Gender(models.TextChoices):
        M = "M", "Male"
        F = "F", "Female"

    name = models.CharField(max_length=128)
    age = models.IntegerField()
    gender = models.CharField(maxlength=1, choices=Gender.choices)

    class Meta:
        constraints = [
            models.CheckConstraint(
                check=models.Q(gender__in=Gender.values), name="valid_gender"
            ),
            models.CheckConstraint(
                check=models.Q(age__gte=18), name="adults_only"
            ),
        ]

After running migrations, if you now try to self.save(), will raise an IntegrityError!

ddelange
  • 1,037
  • 10
  • 24