Django custom validation of multiple fields - django

for which I want to validate a number of fields in a custom clean method.
I have this so far:
class ProjectInfoForm(forms.Form):
module = forms.ModelChoiceField(
queryset=Module.objects.all(),
)
piece = forms.CharField(
widget=forms.Select(),
required=False,
)
span = forms.IntegerField(
max_value=100,
initial=48
)
max_span = forms.IntegerField(
max_value=100,
initial=0
)
def clean(self):
span = self.cleaned_data['span']
max_span = self.cleaned_data['max_span']
piece = self.cleaned_data.['piece']
# validate piece
try:
Piece.objects.get(pk=m)
except Piece.DoesNotExist:
raise forms.ValidationError(
'Illegal Piece selected!'
)
self._errors["piece"] = "Please enter a valid model"
# validate spans
if span > max_span:
raise forms.ValidationError(
'Span must be less than or equal to Maximum Span'
)
self._errors["span"] = "Please enter a valid span"
return self.cleaned_data
However, this only gives me one of the messages if both clauses invalidate. How can I get all the invalid messages. Also I do not get the field-specific messages - how do I include a message to be displayed for the specific field?
Any help much appreciated.

Store the errors and don't raise them until the end of the method:
def clean(self):
span = self.cleaned_data['span']
max_span = self.cleaned_data['max_span']
piece = self.cleaned_data.['piece']
error_messages = []
# validate piece
try:
Piece.objects.get(pk=m)
except Piece.DoesNotExist:
error_messages.append('Illegal Piece selected')
self._errors["piece"] = "Please enter a valid model"
# validate spans
if span > max_span:
error_messages.append('Span must be less than or equal to Maximum Span')
self._errors["span"] = "Please enter a valid span"
if len(error_messages):
raise forms.ValidationError(' & '.join(error_messages))
return self.cleaned_data

You should write a custom clean_FIELDNAME method in this case. That way, field centric validation errors can later be displayed as such when using {{form.errors}} in your template. The clean method o.t.h. is for validating logic that spans more than one field. Take a look through the link I posted above, everything you need to know about validating django forms is in there.

It happens because you are using raise.
Try replace it by these two line in your code:
del self.cleaned_data['piece']
and
del self.cleaned_data['span']

It appears this has changed in later versions of Django (this seems to work in 2.1 and later):
from django import forms
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super().clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject and "help" not in subject:
msg = "Must put 'help' in subject when cc'ing yourself."
self.add_error('cc_myself', msg)
self.add_error('subject', msg)
https://docs.djangoproject.com/en/dev/ref/forms/validation/#raising-multiple-errors has more details.

Related

Validation not running in django form

I want to run field validatin on my form, as in form and field validation- using validation in practice.
My form looks like this:
from kapsule.validators import name_zero_min_length, name_max_length
class NameUpdateForm(forms.Form):
name = forms.CharField(
validators=[
name_zero_min_length,
name_max_length
]
)
My validators:
from django.core.exceptions import ValidationError
def name_zero_min_length(name_field):
# Check minimum length
if not len(name_field) > 0:
print('firing zero length')
raise ValidationError(
"My custom error message name must be at least one character"
)
def name_max_length(name_field):
# Check that the name is under the max length
MAX_LENGTH = 200
if len(name_field) > MAX_LENGTH:
print('raising')
raise ValidationError(
"My custom error message name cannot be more than {} characters".format(MAX_LENGTH)
)
My view like this:
def edit_kapsule_name(request, kapsule_pk):
kapsule = Kapsule.objects.get(pk=kapsule_pk)
form = NameUpdateForm(request.POST)
response = {}
print('pre-validation')
if form.is_valid():
print('VALID')
name = form.data.get('name')
kapsule.name = name
kapsule.save(update_fields=['name'])
else:
print('INVALID') # INVALID
print('json') # json
errors = form._errors.as_json()
print(errors) # {"name": [{"message": "This field is required.", "code": "required"}]}
My output is commented in the above code (invalid, and giving a different error that that which I expected).
Why is my custom validation not running?
This seems to match with my model validation (working), and the second reponse here
Well in the comment from your code I can see that the form is invalid and it is complaining about a required field. That might be the cause your validators are not running, according to the docs:
The clean() method on a Field subclass is responsible for running to_python(), validate(), and run_validators() in the correct order and propagating their errors. If, at any time, any of the methods raise ValidationError, the validation stops and that error is raised. This method returns the clean data, which is then inserted into the cleaned_data dictionary of the form.
On the other hand, if the field is required, the validation not len(name_field) > 0 has no much sense.
Try calling your validators as part of the clean_name method in your form.

Django form.save() not taking updated form.cleaned_data

I'm lost here and can't figure out what I'm missing, which is probably something stupid, but I need another set of eyes because as far as I can tell this should be working.
What I'm trying to do is allow my users to enter phone numbers in ways they are used to seeing, but then take that input and get a validated international phone number from Twilio and save it. By definition that means it will be in the following format - which is the format I need to have it in the database so that it interacts well with another part of the application:
+17085551212
I've debugged to the point there I know the values are coming in correctly, everything works right if I get an invalid number, etc. For some reason, the updated value is not being passed back to the form when I set form.cleaned_data['office_phone'] prior to the form.save(). So I am getting the original number (708) 555-1212 in the database.
forms.py
class ProfileForm(forms.ModelForm):
office_phone = forms.CharField(max_length=20, label="Office Phone")
views.py
if form.is_valid():
print (form.cleaned_data['office_phone'])
pn = form.cleaned_data['office_phone'].replace(" ","")
try:
response = validator.phone_numbers.get(str(pn))
form.cleaned_data['office_phone'] = str(response.phone_number)
print form.cleaned_data
form.save()
success_message = "Your changes have been saved"
except:
error_message = "The contact phone number you entered is invalid."
console.output
(708) 555-1212
+17085551212
+17085551212
{'office_phone': '+17085551212'}
<tr><th><label for="id_office_phone">Office Phone:</label></th>
<td><input id="id_office_phone" maxlength="20" name="office_phone" type="text" value="(708) 555-1212" /></td></tr>
What am I missing here?
Made an edit: I realise that instead of overriding save, we should instead clean/validate the phone number by using custom validation:
class ProfileForm(forms.ModelForm):
office_phone = forms.CharField(max_length=20, label="Office Phone")
def clean_office_phone(self):
value = self.cleaned_data.get("office_phone")
try:
value = value.replace(" ", "")
response = validator.phone_numbers.get(str(value))
except:
raise ValidationError("The contact phone number you entered is invalid")
return str(response.phone_number)
views.py:
if form.is_valid():
form.save()
success_message = "Your changes have been saved"

Django validate field based on value from another field

I have this django field called is_private indicating whether the posting done by the user is private or not. If the posting is private then a certain field called private_room must be mentioned otherwise a field called public_room is required.
In the clean_private_room and clean_public_room fields I'm doing a check to see the value of is_private. If the room is private then in the clean_public_room method I simply return an empty string "" and the same for clean_private_room otherwise I continue with the validation.
The problem is that checking with self.cleaned_data.get('is_private') is returning different results in those two methods. I tried debugging the code and printed the self.cleaned_data value to the terminal and in one of the methods cleaned data contains one form field and in the other method contains my full posted values.
Here's a part of my code, please read the comments in it to see where I print and what gets printed. I don't know why it's behaving this way.
class RoomForm( forms.ModelForm ):
...
def clean_is_private( self ):
if not 'is_private' in self.cleaned_data:
raise forms.ValidationError("please select the type of room (private/public)")
return self.cleaned_data.get("is_private")
def clean_public_room( self ):
print "<clean_public_room>"
# !!!!!!!!!
# when printing this one I only get one form value which is: public_room
print self.cleaned_data
if self.cleaned_data.get("is_private"):
return ""
# otherwise....
if not self.cleaned_data.get("public_room"):
raise forms.ValidationError(
'you need to mention a public room'
)
return self.cleaned_data[ 'public_room' ]
def clean_private_room( self ):
print "<clean_private_room>"
# !!!!!!!!!
# when printing this one I get all form values: public_room, private_room, is_private
print self.cleaned_data
if not self.cleaned_data.get("is_private"):
return ""
# otherwise....
if not self.cleaned_data.get("private_room"):
raise forms.ValidationError(
'you need to mention a private room'
)
return self.cleaned_data[ 'private_room' ]
Form fields are cleaned in the order they defined in the form. So you just need to put is_private field before the public_room in the fields list.

Django: Overriding the clean() method in forms - question about raising errors

I've been doing things like this in the clean method:
if self.cleaned_data['type'].organized_by != self.cleaned_data['organized_by']:
raise forms.ValidationError('The type and organization do not match.')
if self.cleaned_data['start'] > self.cleaned_data['end']:
raise forms.ValidationError('The start date cannot be later than the end date.')
But then that means that the form can only raise one of these errors at a time. Is there a way for the form to raise both of these errors?
EDIT #1:
Any solutions for the above are great, but would love something that would also work in a scenario like:
if self.cleaned_data['type'].organized_by != self.cleaned_data['organized_by']:
raise forms.ValidationError('The type and organization do not match.')
if self.cleaned_data['start'] > self.cleaned_data['end']:
raise forms.ValidationError('The start date cannot be later than the end date.')
super(FooAddForm, self).clean()
Where FooAddForm is a ModelForm and has unique constraints that might also cause errors. If anyone knows of something like that, that would be great...
From the docs:
https://docs.djangoproject.com/en/1.7/ref/forms/validation/#cleaning-and-validating-fields-that-depend-on-each-other
from django.forms.util import ErrorList
def clean(self):
if self.cleaned_data['type'].organized_by != self.cleaned_data['organized_by']:
msg = 'The type and organization do not match.'
self._errors['type'] = ErrorList([msg])
del self.cleaned_data['type']
if self.cleaned_data['start'] > self.cleaned_data['end']:
msg = 'The start date cannot be later than the end date.'
self._errors['start'] = ErrorList([msg])
del self.cleaned_data['start']
return self.cleaned_data
errors = []
if self.cleaned_data['type'].organized_by != self.cleaned_data['organized_by']:
errors.append('The type and organization do not match.')
if self.cleaned_data['start'] > self.cleaned_data['end']:
errors.append('The start date cannot be later than the end date.')
if errors:
raise forms.ValidationError(errors)
Although its old post, if you want less code you can use add_error() method to add error messages. I am extending the #kemar's answer to show the used case:
add_error() automatically removes the field from cleaned_data dictionary, you dont have to delete it manually.
Also you dont have to import anything to use this.
documentation is here
def clean(self):
if self.cleaned_data['type'].organized_by != self.cleaned_data['organized_by']:
msg = 'The type and organization do not match.'
self.add_error('type', msg)
if self.cleaned_data['start'] > self.cleaned_data['end']:
msg = 'The start date cannot be later than the end date.'
self.add_error('start', msg)
return self.cleaned_data
If you'd prefer that the error messages be attached to the form rather than to specific fields, you can use the key "__all__" like this:
msg = 'The type and organization do not match.'
self._errors['__all__'] = ErrorList([msg])
Also, as the Django docs explain: "if you want to add a new error to a particular field, you should check whether the key already exists in self._errors or not. If not, create a new entry for the given key, holding an empty ErrorList instance. In either case, you can then append your error message to the list for the field name in question and it will be displayed when the form is displayed."

Django custom form validation best practices?

I have a form that contains 5 pairs of locations and descriptions. I have three sets of validations that need to be done
you need to enter at least one location
for the first location, you must have a description
for each remaining pair of locations and description
After reading the Django documentation, I came up with the following code to do these custom validations
def clean(self):
cleaned_data = self.cleaned_data
location1 = cleaned_data.get('location1')
location2 = cleaned_data.get('location2')
location3 = cleaned_data.get('location3')
location4 = cleaned_data.get('location4')
location5 = cleaned_data.get('location5')
description1 = cleaned_data.get('description1')
description2 = cleaned_data.get('description2')
description3 = cleaned_data.get('description3')
description4 = cleaned_data.get('description4')
description5 = cleaned_data.get('description5')
invalid_pairs_msg = u"You must specify a location and description"
# We need to make sure that we have pairs of locations and descriptions
if not location1:
self._errors['location1'] = ErrorList([u"At least one location is required"])
if location1 and not description1:
self._errors['description1'] = ErrorList([u"Description for this location required"])
if (description2 and not location2) or (location2 and not description2):
self._errors['description2'] = ErrorList([invalid_pairs_msg])
if (description3 and not location3) or (location3 and not description3):
self._errors['description3'] = ErrorList([invalid_pairs_msg])
if (description4 and not location4) or (location4 and not description4):
self._errors['description4'] = ErrorList([invalid_pairs_msg])
if (description5 and not location5) or (location5 and not description5):
self._errors['description5'] = ErrorList([invalid_pairs_msg])
return cleaned_data
Now, it works but it looks really ugly. I'm looking for a more "Pythonic" and "Djangoist"(?) way to do this. Thanks in advance.
First thing you can do is simplify your testing for those cases where you want to see if only one of the two fields is populated. You can implement logical xor this way:
if bool(description2) != bool(location2):
or this way:
if bool(description2) ^ bool(location2):
I also think this would be more clear if you implemented a clean method for each field separately, as explained in the docs. This makes sure the error will show up on the right field and lets you just raise a forms.ValidationError rather than accessing the _errors object directly.
For example:
def _require_together(self, field1, field2):
a = self.cleaned_data.get(field1)
b = self.cleaned_data.get(field2)
if bool(a) ^ bool(b):
raise forms.ValidationError(u'You must specify a location and description')
return a
# use clean_description1 rather than clean_location1 since
# we want the error to be on description1
def clean_description1(self):
return _require_together('description1', 'location1')
def clean_description2(self):
return _require_together('description2', 'location2')
def clean_description3(self):
return _require_together('description3', 'location3')
def clean_description4(self):
return _require_together('description4', 'location4')
def clean_description5(self):
return _require_together('description5', 'location5')
In order to get the behavior where location1 is required, just use required=True for that field and it'll be handled automatically.
At least you can reduce some code. Have 'location1' and 'description1' Required=True (as TM and stefanw pointed out). Then,
def clean(self):
n=5
data = self.cleaned_data
l_d = [(data.get('location'+i),data.get('description'+i)) for i in xrange(1,n+1)]
invalid_pairs_msg = u"You must specify a location and description"
for i in xrange(1,n+1):
if (l_d[i][1] and not l_d[i][0]) or (l_d[i][0] and not l_d[i][1]):
self._errors['description'+i] = ErrorList([invalid_pairs_msg])
return data
still ugly though...