Django model validation - django

I'm facing a validation problem.
I need to use form validation and model validation together, but django (1.10) doesn't seem to like this.
Here is a short version of my setup:
class MyModel(models.Model):
fk = models.ForeignKey('ap.Model')
foo = models.CharField(max_length=12)
def clean(self):
if self.fk.som_field != self.foo:
raise ValidationError("This did not validate")
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('fk',)
def view(request):
instance = MyModel(foo='bar')
form = MyModelForm(data=request.POST, instance=instance)
if form.is_valid():
# process
# redirect
# display template
So I have some model field validation in the model itself.
I need this here because it is re-used in other non-form related parts of my application.
And I have some user input validation in the form.
In this case, checking that the provided fk is valid and exists.
But when the form is validated and the user provided 'fk' is not valid, the form is rejected.
But the form also calls MyModel.full_clean add more model validation.
The problem is that calling MyModel.clean() without having any data in the field fk will raise a RelatedObjectDoesNotExist exception.
How can I do this the proper way ?
I feel that MyModel.full_clean() should not be called by the form until the form itself is valid to ensure that the model is validated with at least correct field types in it.
I could embed my MyModel.clean operations in a try/except that would catch a RealtedObjectDoesNotExist, but it has a bad code smell for me.
The fact that MyModel.fk exists should be ensured by the first form layer validation.

As you have found, the model form performs the instance validation (by calling self.instance.full_clean()`) whether or not the form is valid.
You could prevent this behaviour (I might try to override the _post_clean, but I think it's simpler to take account of the behaviour in the clean method.
Rather than catching the exception, it would be simpler to check that self.fk_id is not None before accessing self.fk.
class MyModel(models.Model):
fk = models.ForeignKey('ap.Model')
foo = models.CharField(max_length=12)
def clean(self):
if self.fk_id is not None and self.fk.som_field != self.foo:
raise ValidationError("This did not validate")

Related

Validate multiple foreign keys while saving a ModelForm

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?
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}')

ModelForm and unique_together validation

Django 1.10
The documentaion says that ModelForm validates unique together if clean method is correctly overridden.
https://docs.djangoproject.com/en/1.10/topics/forms/modelforms/#overriding-the-clean-method
I have done something wrong as unique_together validation doesn't work.
>>> from wiki.models import Wiki
>>> Wiki.objects.all()
<QuerySet [<Wiki: image/1/fdfff>, <Wiki: image/1/fdffff>]>
Where image is related_model and 1 is related_id.
Could you help me understand what is wrong with this overriding?
class WikiForm(ModelForm):
class Meta:
model = Wiki
fields = ['related_model', 'related_id', 'article']
unique_together = (("related_model", "related_id"),)
def validate_related_model(self):
...
def validate_related_id(self):
...
def clean(self):
self.validate_related_model()
self.validate_related_id()
# To maintain unique_together validation,
# we must call the parent class’s clean() method.
return super(WikiForm, self).clean()
unique_together is a database-level constraint. It's supposed to be specified in the Meta class of the model, not the model form. For the validation to work as you would like, move it to the Wiki model. In your form, you probably won't even need to have those extra validation methods.
This doesn't look like it'd be the case for you, but also note that in order for the unique validation to work correctly, your model form must include all of the fields that are specified in the unique_together constraint. So, if related_model or related_id were excluded from the form, you'd need to do some extra work in order to allow the correct validation to happen:
def validate_unique(self):
# Must remove organization field from exclude in order
# for the unique_together constraint to be enforced.
exclude = self._get_validation_exclusions()
exclude.remove('organization')
try:
self.instance.validate_unique(exclude=exclude)
except ValidationError, e:
self._update_errors(e.message_dict)
In the example above I am removing organization from the form's list of excluded fields because it's part of the unique_together constraint.

How to auto change db input on IntegrityError in Django?

I'm using a model form to let a user put Data into the database. This form excludes the user field so the user cannot change it himself.
class Meta:
model = Server
exclude = ["user","name", "quirky"]
Instead the value of the user field will be put in after I call .save(commit=False).
if neuer_server_form.has_changed() and neuer_server_form.is_valid():
neuer_server = neuer_server_form.save(commit=False)
neuer_server.user = request.user
neuer_server.name = slugify(neuer_server.name)
neuer_server.save()
Because of that the user field is excluded from validation. Now there is a unique_together between the user field and another char field.
class Meta:
unique_together = ('user', 'name',)
Because the user field is excluded there will be no validation for the unique_together. So when saving the instance there can be an IntegrityError.
Where I'm stuck:
So my first Idea was to check the db if the CharField already exists and if so just change it by adding a number and check again. But if I do this counting upwards an attacker might insert a lot of similar strings so my server has to do this checking indefinitely long.
Possible Solutions:
So for me there would be two acceptable solutions: Either change the CharFields value to something that definitely does not exist yet without trying a lot first. Or make the validation fail and throw the form back to the user.
What I tried:
I think the second would be ideal, but since I'm using model formset and cannot pass the request user to the form it's not possible for me to do that:
Django's ModelForm unique_together validation
Instead I was wondering if it was possible to add self made errors to a form while looping through a formset.
Somewhat like this pseudo code:
for form in formset.forms:
if form.is_valid():
server_name = form.cleaned_data.get("name","")
if Server.objects.get(user=request.user,name=server_name).count():
formset.forms[form].errors += "here is my own error message"
formset.forms[form].fields["name"].errors += "this field is wrong"
Any ideas how to solve that? Thank you for help if you read until here :)
if request.method == 'POST':
server = Server(user=request.user)
form = ServerForm(request.POST, instance=server)
if form.is_valid():
f = form.save()
f.name = slugify(f.name)
f.save()
else:
messages.error(request,
' '.join([v[0].__str__() for k, v in form.errors.items()]))

Django ModelForm individual fields not validated when overriding clean() on Model

I am using standard Django Models and ModelForms.
On the Model, I am overriding the clean() method to check that 2 fields in combination are valid. I am having validators[], not nulls etc defined on other fields.
In the ModelForm, I am not modifying anything. I only set the corresponding Model in the ModelForm's Meta class.
When I try to create a new object through the form, the individual fields in the ModelForm/Model are not validated when I call form.is_valid() in the View. According to the docs (https://docs.djangoproject.com/en/1.6/ref/models/instances/#validating-objects) the is_valid() method on the Form should call the Model's clean_fields() method (first).
This doesn't seem to work when you submit a form without a Model instance (or a new instance not in the db). When I'm editing an existing object, all is well. It nicely triggers invalid values in individual fields before calling the Model's clean() method.
When I remove the overridden clean() method from my Model, all is well. Individual fields are validated both when creating new objects and editing existing ones.
I have also tested this with the admin module's forms. It has exactly the same behaviour.
So the question is, why does overriding the clean() method on my Model prevent the ModelForm validating the individual fields before calling the clean() method to test additional cross-field stuff???
Note that I am not validating the ModelForm. All validation is on the Model itself.
Model:
class Survey(models.Model):
from_date = models.DateField(null=False, blank=False, validators=[...])
to_date = models.DateField(null=False, blank=False, validators=[...])
(...)
def clean(self):
errors = []
# At this point I expect self.to_date already to be validated for not null etc.
# It isn't for new Model instances, only when editing an existing one
if self.to_date < self.from_date:
errors.append(ValidationError("..."))
ModelForm:
class TestForm(ModelForm):
class Meta:
model = Survey
View (to render blank form for new Model data entry):
(...)
if request.method == "POST":
survey_form = TestForm(request.POST)
if '_save' in request.POST:
if survey_form.is_valid():
survey_form.save()
return HttpResponseRedirect(next_url)
else:
return HttpResponseRedirect(next_url)
else:
survey_form = TestForm()
context = {'form': survey_form}
(...)

Why is my forms clean method not doing anything?

I have two basic models that use model forms in the Django admin.
Models.py is similar to:
class FirstModel(models.Model):
name = CharField(max_length=100)
url = URLField()
class OtherModel(models.Model):
model = models.ForeignKey(FirstModel)
##Other fields that show up fine and save fine, but include some localflavor
Forms.py looks similar to:
class FirstModelForm(forms.ModelForm):
def clean(self):
#call the super as per django docs
cleaned_data = super(FirstModelForm, self).clean()
print cleaned_data
class Meta:
model = FirstModel
#other modelform is the same with the appropriate word substitutions and one field that gets overridden to a USZipCodeField
These are a stacked inline ModelAdmin with nothing special in the admin.py:
class OtherModelInline(admin.StackedInline):
model = OtherModel
fields = (#my list of fields works correctly)
readonly_fields = (#couple read onlys that work correctly)
class FirstModelAdmin(admin.ModelAdmin):
inlines = [
OtherModelInline,
]
admin.site.register(FirstModel, FirstModelAdmin)
I do have a User model, form and ModelAdmin that subclasses the User and UserCreationForm and overrides it's own clean method.This works exactly as expected.
The problem is with FirstModel and OtherModel. The clean methods I override in the ModelForm subclasses of FirstModelForm and OtherModelForm don't do anything. No exception thrown or a print of the cleaned_data. Just nothing. Everything else works as expected, but it's like my clean method isn't even there.
I got to be missing something simple, but I can't see what is. Any help would be great. Thanks!
By default, Django dynamically generates a model form for your model admins. You must specify that you want to use your custom forms by setting the form attribute.
class OtherModelInline(admin.StackedInline):
model = OtherModel
fields = (...) # if this doesn't work after specifying the form, set fields for the model form instead
readonly_fields = (#couple read onlys that work correctly)
form = OtherModelForm
class FirstModelAdmin(admin.ModelAdmin):
form = FirstModelForm
inlines = [
OtherModelInline,
]
admin.site.register(FirstModel, FirstModelAdmin)
You need to return the cleaned_data from the clean method in the form. If you look at the documentation for cleaning fields that rely on each other you'll notice:
...
# Always return the full collection of cleaned data.
return cleaned_data
It is possible that nothing survived the parent 'clean' method. If you are submitting data that won't validate because of the way your models are set up, cleaned_data will be empty. This is mentioned in the same doc linked by Timmy, where it says:
By the time the form’s clean() method is called, all the individual field clean methods will have been run (the previous two sections), so self.cleaned_data will be populated with any data that has survived so far. So you also need to remember to allow for the fact that the fields you are wanting to validate might not have survived the initial individual field checks.
In this case, if you have a URLField, the field validation is very strict, and unless you define 'verify_exists=False', it will also check if you are putting in a URL that returns a 404. In your case you would need to do this if you wanted to allow that:
class FirstModel(models.Model):
name = CharField(max_length=100)
url = URLField(verify_exists=False)
Outside of that, I have no idea what could be going on.