Explanation:
I have a model that has two foreign keys:
class Book(models.Model):
name = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=CASCADE)
created_by = models.ForeignKey(User, blank=True, null=True, on_delete=CASCADE)
I have a modelform for it:
class BookForm(modelForm):
class Meta:
model = Book
exclude = ('author', 'created_by',)
In my business logic, I want to save a new author and a book with it in the same view, and the created_by
could be null in some cases. So, before saving an instance of a BookForm, I will create an author first. So, suppose that I have created an author, then I will do the below to save a book:
f = BookForm(post_data)
if form.is_valid():
instance = f.save(commit=False)
instance.author = author
instance.created_by = user
instance.save()
What I did so far is working fine, but the foreign key fields are not part of the validation. As you can see from the model class, author
field is mandatory, but created_by
field is optional. So, if I want to add some validations for them, I have to add them explicitly by customizing the is_valid()
method, or by adding some if
conditions before instance.author = author
.
Question:
The bigger picture is that I have a model with much more foreign keys; some of them are mandatory, and the others are not. So, using the same approach I did above, I have to add a custom validation for each FK explicitly, which I consider it duplication and unclean as those FK fields validation is defined in my model class.
So, what is the best practice to validate automatically those multiple foreign keys while saving by checking the models validation directly and without mentioning each FK one by one?
CodePudding user response:
Since the foreign key fields are not included in your form, there won't be any form validation on them. What you need is a model validation.
Django provides the model.full_clean()
method, which performs all model validation steps.
You will need to call the method manually, since save() method does not invoke any model clean method.
See documentation here.
For example,
f = BookForm(post_data)
if form.is_valid():
instance = f.save(commit=False)
instance.author = author
instance.created_by = user
try:
instance.full_clean()
except ValidationError as e:
# Do something based on the errors contained in e.message_dict.
# Display them to a user, or handle them programmatically.
pass
instance.save()
If you want to display the error messages to the user, simply pass e to the context and loop through the messages in your template, displaying them wherever you want. This seems the cleaner approach than adding errors to the form, because they are model validation errors not form errors.
In your view:
except ValidationError as e:
context = {'e':e} # add other relevant data to context
return render(request, 'your_template', context)
In your template:
{% for key, values in e.message_dict %}
{{key}}: {{value}}
{% endfor %}
If you want the errors added to the form, then use form.add_error
:
except ValidationError as e:
for key, value in e.items():
# this adds none-field errors to the form for each model error
form.add_error(None, f'{key}: {value}')