Frage

I'm not sure how to properly raise a validation error in a model's save method and send back a clear message to the user.

Basically I want to know how each part of the "if" should end, the one where I want to raise the error and the one where it actually saves:

def save(self, *args, **kwargs):
    if not good_enough_to_be_saved:
        raise ValidationError
    else:
        super(Model, self).save(*args, **kwargs)

Then I want to know what to do to send a validation error that says exactly to the user what's wrong just like the one Django automatically returns if for example a value is not unique. I'm using a (ModelForm) and tune everything from the model.

War es hilfreich?

Lösung

Most Django views e.g. the Django admin will not be able to handle a validation error in the save method, so your users will get 500 errors.

You should do validation on the model form or on the model, and raise ValidationError there. Then call save() only if the model form data is 'good enough to save'.

Andere Tipps

Bastian, I explain to you my code templating, I hope that helps to you:

Since django 1.2 it is able to write validation code on model. When we work with modelforms, instance.full_clean() is called on form validation.

In each model I overwrite clean() method with a custom function (this method is automatically called from full_clean() on modelform validation ):

from django.db import models
 
class Issue(models.Model):
    ....
    def clean(self): 
        rules.Issue_clean(self)  #<-- custom function invocation

from issues import rules
rules.connect()

Then in rules.py file I write bussiness rules. Also I connect pre_save() to my custom function to prevent save a model with wrong state:

from issues.models import Issue

def connect():    
    from django.db.models.signals import post_save, pre_save, pre_delete
    #issues 
    pre_save.connect(Issue_pre_save, sender = Incidencia ) 
    post_save.connect(Issue_post_save, sender = Incidencia )
    pre_delete.connect(Issue_pre_delete, sender= Incidencia) 

def Incidencia_clean( instance ):    #<-- custom function 
    import datetime as dt    
    errors = {}

    #dia i hora sempre informats     
    if not instance.dia_incidencia:   #<-- business rules
        errors.setdefault('dia_incidencia',[]).append(u'Data missing: ...')
        
    #dia i hora sempre informats     
    if not  instance.franja_incidencia: 
        errors.setdefault('franja_incidencia',[]).append(u'Falten Dades: ...')
 
    #Només es poden posar incidències més ennlà de 7 dies 
    if instance.dia_incidencia < ( dt.date.today() + dt.timedelta( days = -7) ): 
        errors.setdefault('dia_incidencia 1',[]).append(u'''blah blah error desc)''')
 
    #No incidències al futur. 
    if instance.getDate() > datetime.now(): 
        errors.setdefault('dia_incidencia 2',[]).append(u'''Encara no pots ....''') 
    ... 

    if len( errors ) > 0: 
        raise ValidationError(errors)  #<-- raising errors

def Issue_pre_save(sender, instance, **kwargs): 
    instance.clean()     #<-- custom function invocation

Then, modelform calls model's clean method and my custon function check for a right state or raise a error that is handled by model form.

In order to show errors on form, you should include this on form template:

{% if form.non_field_errors %}
      {% for error in form.non_field_errors %}
        {{error}}
      {% endfor %}
{% endif %}  

The reason is that model validation erros ara binded to non_field_errors error dictionary entry.

When you save or delete a model out of a form you should remember that a error may be raised:

try:
    issue.delete()
except ValidationError, e:
    import itertools
    errors = list( itertools.chain( *e.message_dict.values() ) )

Also, you can add errors to a form dictionary on no modelforms:

    try:
        #provoco els errors per mostrar-los igualment al formulari.
        issue.clean()
    except ValidationError, e:
        form._errors = {}
        for _, v in e.message_dict.items():
            form._errors.setdefault(NON_FIELD_ERRORS, []).extend(  v  )

Remember that this code is not execute on save() method: Note that full_clean() will not be called automatically when you call your model’s save() method, nor as a result of ModelForm validation. Then, you can add errors to a form dictionary on no modelforms:

    try:
        #provoco els errors per mostrar-los igualment al formulari.
        issue.clean()
    except ValidationError, e:
        form._errors = {}
        for _, v in e.message_dict.items():
            form._errors.setdefault(NON_FIELD_ERRORS, []).extend(  v  )

I think this is more clear way to do that for Django 1.2+

In forms it will be raised as non_field_error, in other cases, like DRF you have to check this case manual, because it will be 500 error.

class BaseModelExt(models.Model):
is_cleaned = False

def clean(self):
    # check validation rules here

    self.is_cleaned = True

def save(self, *args, **kwargs):
    if not self.is_cleaned:
        self.clean()

    super().save(*args, **kwargs)

In the Django documentation they raise the ValueError in the .save method, it's maybe useful for you. https://docs.djangoproject.com/en/3.1/ref/models/instances/

Edit: This answer assumes that you have a scenario that does not allow you to edit the currently implemented User class, because you are not starting a project from scratch, the current implementation does not already use a custom User class, and you instead have to figure out how to accomplish this task by modifying Django's built in User model behavior.

You can just stick a clean method to your model most of the time, but you don't have that option necessarily with the built in auth.User model. This solution will allow you to create a clean method for the auth.User model in such a way that ValidationErrors will propagate to forms where the clean method is called (including admin forms).

The below example raises an error if someone attempts to create or edit an auth.User instance to have the same email address as an existing auth.User instance. Disclaimer, if you are exposing a registration form to new users, you do not want your validation error to call out usernames as mine does below.

from django.contrib.auth.models import User
from django.forms import ValidationError as FormValidationError

def clean_user_email(self):
    instance = self
    super(User, self).clean()
    if instance.email:
        if User.objects.filter(id=instance.id, email=instance.email).exists():
            pass  # email was not modified
        elif User.objects.filter(email=instance.email).exists():
            other_users = [*User.objects.filter(email=instance.email).values_list('username', flat=True)]
            raise FormValidationError(f'At least one other user already has this email address: '
                                      f'{", ".join(other_users)}'
                                      , code='invalid')

# assign the above function to the User.clean method
User.add_to_class("clean", clean_user_email)

I have this at the bottom of my_app.models but I am sure it would work as long as you stick it somewhere that is loaded before the form in question.

def clean(self):
    raise ValidationError("Validation Error")

def save(self, *args, **kwargs):
    if some condition:
        #do something here
    else:
        self.full_clean()
    super(ClassName, self).save(*args, **kwargs)
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top