Django forms raise general validation error if all fields empty - django

I have a form where none of the fields are required indivdually, but I want to raise a validation error if all fields are left empty. What is the best way to do this? I tried the following but it it didn't work:
def clean(self):
cleaned_data = super(SearchForm, self).clean()
if len(cleaned_data) == 0:
raise forms.ValidationError(ugettext_lazy("You must fill at least one field!"))

Rather than checking the length of the cleaned_data (it should always contain one entry for each form field), you should check each entry and confirm that the values are all empty.
Here's an example of how you could do it.
def clean(self):
cleaned_data = super(SearchForm, self).clean()
form_empty = True
for field_value in cleaned_data.itervalues():
# Check for None or '', so IntegerFields with 0 or similar things don't seem empty.
if field_value is not None and field_value != '':
form_empty = False
break
if form_empty:
raise forms.ValidationError(ugettext_lazy("You must fill at least one field!"))
return cleaned_data # Important that clean should return cleaned_data!

Ignore fields filled with just whitespace as well: and not field_value.isspace()

Related

Custom validation in the clean method triggers before checking the fields exist or not in Django

I have the following code in my models file in Django:
class MyModel(models.Model):
...
foo = models.IntegerField()
bar = models.IntegerField()
def validate_foo_bar(self):
self._validation_errors = {}
if self.foo > self.bar:
self._validation_errors['foo'] = ['Must be greater than bar.']
self._validation_errors['bar'] = ['Must be less than foo.']
def clean(self):
self.validate_foo_bar()
if bool(self._validation_errors):
raise ValidationError(self._validation_errors)
super(MyModel, self).clean()
Hopefully the idea is clear. I check for errors in the clean method and raise them if they occur. When I use an admin form to create an object, if I leave the foo and bar fields empty, I get the following error:
if self.foo > self.bar:
TypeError: '>' not supported between instances of 'NoneType' and 'NoneType'
Why is this happening? Shouldn't the requirement check trigger before the method I wrote? Thanks for any help.
EDIT
Due to the nature of the answers and comments, I feel compelled to add this. I know the problem can be solved simply by doing the following:
def validate_foo_bar(self):
self._validation_errors = {}
if self.foo and self.bar:
if self.foo > self.bar:
self._validation_errors['foo'] = ['Must be greater than bar.']
self._validation_errors['bar'] = ['Must be less than foo.']
However, that is missing the point because shouldn't this check be done by the built-in form methods themselves before the validate_foo_bar() method is triggered?
According to the docs, the clean_fields method is called before the clean method. The clean_fields method in fact skips validation for fields with a None type:
def clean_fields(self, exclude=None):
"""
Clean all fields and raise a ValidationError containing a dict
of all validation errors if any occur.
"""
if exclude is None:
exclude = []
errors = {}
for f in self._meta.fields:
if f.name in exclude:
continue
# Skip validation for empty fields with blank=True. The developer
# is responsible for making sure they have a valid value.
raw_value = getattr(self, f.attname)
if f.blank and raw_value in f.empty_values:
continue
try:
setattr(self, f.attname, f.clean(raw_value, self))
except ValidationError as e:
errors[f.name] = e.error_list
if errors:
raise ValidationError(errors)
You can read up more about the reasons why here, where it says:
It is valid based on blank=True though. A use case for blank=True, null=False would be a field that gets populated in save(), for example. If we changed the behavior, it would be backwards incompatible and wouldn't allow this use case anymore. If you want the field required for model validation but not for form validation, then you should drop blank=True on the model and customize the form.

How to raise a ValidationError AND return a response?

After checking my form is valid I perform additional validation:
def reset_passwd(req):
if req.method == 'POST':
form = ResetPasswdForm(req.POST)
if form.is_valid():
# extract data
passwd1 = form.cleaned_data['passwd1']
passwd2 = form.cleaned_data['passwd2']
# validate passwd
if passwd1 != passwd2:
raise forms.ValidationError('passwords are not the same', 'passwd_mismatch')
# do stuff...
return HttpResponseRedirect('/success')
else:
form = ResetPasswdForm(req.POST)
return render(req, 'reset_passwd_form.html', {'form': form})
Problem is raising a ValidationError which is an Exception of course breaks execution of the view function so no response is returned!
How is one suppose to return their bound form showing validation errors for validation not performed by form.is_valid()?
(The confusing thing is the django documentation say form.is_valid() throws ValidtionErrors if the form is invalid, however it must handel them as debugging it execution continues when is_valid() is false.)
To validate such cases, you should use clean() method of a form, rather than raising an error in the view.
This is nicely explained at Cleaning and validating fields that depend on each other
You need to override the is_valid() method, call the super is_valid() first ( return false if it returns false ), and then handle your error case.
If you use clean() instead you won't benefit for things such as "required=True" in your other fields and will need to check everything manually.
super().clean() just ... does not check for it afaik, and it could give you KeyError when accessing cleaned_data['passwd1'] unless checking it yourself.
class MyResetPasswdForm(forms.Form):
passwd1 = forms.CharField(widget=forms.PasswordInput, required=True)
passwd2 = forms.CharField(widget=forms.PasswordInput, required=True)
def is_valid(self):
valid = super(MyResetPasswdForm).is_valid()
if not valid:
return valid
if self.cleaned_data.get("passwd1") != self.cleaned_data.get("passwd2"):
# You cannot raise a ValidationError because you are not in the clean() method, so you need to add the error through the add_error method.
self.add_error("passwd2", ValidationError('passwords are not the same', 'passwd_mismatch')
# You could also use None instead of "passwd2" if you do not want the error message to be linked to the field.
if not self.errors:
return True
return False
This way, you get the pre-set error messages, django handles the fields requirements when you call the super().is_valid().

Django - how to know in clean method if the form-data is new or if old data is being changed

How can I know in MyForm.clean() whether the data is new, or if already saved data is being modifed?
What should is_this_new_data() look like in the following code?
class MyForm(forms.ModelForm):
def clean(self):
cleaned_data = self.cleaned_data
if is_this_new_data(self):
# perform some checks if this is new data
else:
# do nothing if this is data being modifed
return cleaned_data
Check self.cleaned_data['some_field'] against self.instance.some_field.
A quick way to check if the object is new is to see if self.instance.pk has a value. It will be None unless the object already exists.
In the clean you can access the changed_data attribute, which is a list of the names of the fields which have changed.
def clean(self):
cleaned_data = self.cleaned_data:
for field_name in self.changed_data:
# loop through the fields which have changed
print "field %s has changed. new value %s" % (field_name, cleaned_data[field_name])
do_something()
return cleaned_data

Django modelform remove "required" attribute based on other field choice

I have ModelForm with several fields. Some of fields are required, some not. Also I have Select field with different choices, and I want to make some of fields "required" or not based on this Select field choice.
I tried in clean() method of Form
def clean(self):
cleaned_data = self.cleaned_data
some_field = cleaned_data.get("some_field")
if some_field == 'some_value':
self.fields['other_field'].required = False
return cleaned_data
but it doesn't work
See the Django documentation on Cleaning and validating fields that depend on each other. The standard practice would be to perform the following handling instead:
def clean(self):
cleaned_data = self.cleaned_data
some_field = cleaned_data.get("some_field")
if some_field == 'some_value':
# 'other_field' is conditionally required.
if not cleaned_data['other_field']:
raise forms.ValidationError("'Other_field' is required.")
return cleaned_data
You have the right idea but the problem is that the individual field validations have already run before the form clean. You have a couple options. You could make the field not required and handle the logic of when it is required in your form.clean. Or you could leave the field as required and remove the validation errors it might raise in the clean.
def clean(self):
cleaned_data = self.cleaned_data
some_field = cleaned_data.get("some_field")
if some_field == 'some_value':
if 'other_field' in self.errors:
del self.errors['other_field']
cleaned_data['other_field'] = None
return cleaned_data
This has some problems in that it removes all errors, not just missing/required errors. There is also a problem with the cleaned_data. You now have a required field which isn't in the cleaned_data which is why I've added it as None. The rest of your application will have to handle this case. It might seem odd to have a required field which doesn't have a value.
If you like to print error message for required field in common way, you can do this:
def clean(self):
cleaned_data = super(PasswordChangeForm, self).clean()
token = cleaned_data.get('token')
old_password = cleaned_data.get('old_password')
if not token and not old_password:
self._errors['old_password'] = self.error_class([self.fields['old_password'].error_messages['required']])
I found a possible solution.
You can use your condition before form.is_valid() and include your change in your field.
if form.data['tid_id'] is None: # my condition
form.fields['prs_razonsocial'].required = True
form.fields['prs_nombres'].required = False
form.fields['prs_apellidos'].required = False
if form.is_valid():
...

Validating delete on django-admin inline forms

I am trying to perform a validation such that you cannot delete a user if he's an admin. I'd therefore like to check and raise an error if there's a user who's an admin and has been marked for deletion.
This is my inline ModelForm
class UserGroupsForm(forms.ModelForm):
class Meta:
model = UserGroups
def clean(self):
delete_checked = self.fields['DELETE'].widget.value_from_datadict(
self.data, self.files, self.add_prefix('DELETE'))
if bool(delete_checked):
#if user is admin of group x
raise forms.ValidationError('You cannot delete a user that is the group administrator')
return self.cleaned_data
The if bool(delete_checked): condition returns true and stuff inside the if block gets executed but for some reason this validation error is never raised. Could someone please explain to me why?
Better yet if there's another better way to do this please let me know
The solution I found was to clean in the InlineFormSet instead of the ModelForm
class UserGroupsInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
delete_checked = False
for form in self.forms:
try:
if form.cleaned_data:
if form.cleaned_data['DELETE']:
delete_checked = True
except AttributeError:
pass
if delete_checked:
raise forms.ValidationError(u'You cannot delete a user that is the group administrator')
Although #domino's answer may work for now, the "kinda" recommended approach is to use formset's self._should_delete_form(form) function together with self.can_delete.
There's also the issue of calling super().clean() to perform standard builtin validation. So the final code may look like:
class UserGroupsInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
super().clean()
if any(self.errors):
return # Don't bother validating the formset unless each form is valid on its own
for form in self.forms:
if self.can_delete and self._should_delete_form(form):
if <...form.instance.is_admin...>:
raise ValidationError('...')
Adding to domino's Answer:
In some other scenarios, Sometimes user wants to delete and add object in the same time, so in this case delete should be fine!
Optimized version of code:
class RequiredImageInlineFormset(forms.models.BaseInlineFormSet):
""" Makes inline fields required """
def clean(self):
# get forms that actually have valid data
count = 0
delete_checked = 0
for form in self.forms:
try:
if form.cleaned_data:
count += 1
if form.cleaned_data['DELETE']:
delete_checked += 1
if not form.cleaned_data['DELETE']:
delete_checked -= 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
# Case no images uploaded
if count < 1:
raise forms.ValidationError(
'At least one image is required.')
# Case one image added and another deleted
if delete_checked > 0 and ProductImage.objects.filter(product=self.instance).count() == 1:
raise forms.ValidationError(
"At least one image is required.")