Django: problems with urlfields validation when overriding form validation - django

I have some Form with two urlfields, both not required. The form is used to set the value of JSONField in a Model from these two urlfields (for user convenience), everything works fine. If a user enters something except an URL into url1 or url2, django shows validation error at the form "Enter a valid URL".
Now I want to make a user to input URL in ANY of these urlfields. I'm overriding clean method for that:
class MyForm(forms.ModelForm):
url1 = forms.URLField(required=False)
url2 = forms.URLField(required=False)
def clean(self):
cleaned_data = super(MyForm, self).clean()
if not cleaned_data['url1'] and not cleaned_data['url2']:
raise ValidationError(
_("You should enter at least one URL"),
code='no_urls'
)
return cleaned_data
It works, BUT there is a problem: if user enters some "non-URL" data into url1 or url2 and submits the form, Django raises KeyError with Exception Value: 'url1' (or 'url2') instead of showing a validation error at the form
What's wrong? Thanks!

As documented in quite a few places - notably the part about cross validation -, cleaned_data only contains valid data - the fields that didn't validate wont show up here. You have to account for this one way or another - by testing for key existence or, as shown in the cross-validation example snippet, using dict.get():
def clean(self):
cleaned_data = super(MyForm, self).clean()
# boolean algebra 101: "not A and not B" => "not (A or B)"
if not (cleaned_data.get('url1') or cleaned_data.get('url2')):
raise ValidationError(
_("You should enter at least one URL"),
code='no_urls'
)
return cleaned_data

Related

How do you only run a validator on a form field at the end after no validation errors have been raised?

I would like to only run a specific checksum validation if things like required, min and max validations as well as a custom is_digit() validation is run.
The reason is I do not want to show the error message for the checksum validation if some other validation is failing.
I've tried:
id_number = ZaIdField(
required=False,
max_length=13,
min_length=13,
validators=[validate_numeric, ]
)
then I have the checksum validator after others run in super():
class ZaIdField(forms.CharField):
'''
Field for validating ZA Id Numbers
'''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def validate(self, value):
"""Check if id is valid"""
# Use the parent's handling of required fields, etc.
super().validate(value)
validate_sa_id(value)
Update:
In other words, my final validation is dependent on the length being correct and all digits.
So I just want to ensure that is correct before running it.
Check the django docs for more information. It's pretty simple actually.
def clean_id_number(self):
data = self.cleaned_data['id_number']
if checksum:
raise forms.ValidationError("Checksum error!")
return data
This has probably been answered somewhere before but it looks like the right palce to do this is in the form's clean():
def clean(self):
cleaned_data = super().clean()
id_num = cleaned_data.get('id_number')
if id_num:
validate_sa_id(id_num)
return cleaned_data
The key part of the docs is:
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 just check if the field has survived, if it has then it has passed prior validations
If you want to mess with the order of the validators, I would override the ZaIdFieldsrun_validators method.
Note that the fields validate method that you're overriding will always be called before.
Example (untested):
class ZaIdField(forms.CharField):
'''
Field for validating ZA Id Numbers
'''
def run_validators(self, value):
super().run_validators(value) # will potentially throw ValidationError, exiting
validate_sa_id(value) # your late validator, will throw its own ValidationError

Programmatically set field value after POST in Django Forms

I have a user registration form, and I want the initial password to be the same as the social security number. For some reason, I need to keep the actual password input, so I only hid it.
Now I'm struggling with setting the value of the password before it gets validated. I was under the impression that clean() calls the validation stuff, so naturally I wrote this:
def clean(self):
self.data['password1'] = self.data['password2'] = self.data['personal_number']
return super(SomeForm, self).clean()
This is however not working, because the field apparently gets validated before I can populate it. Help?
def clean_password1(self):
return self.cleaned_data['personal_number']
def clean_password2(self):
return self.cleaned_data['personal_number']

Assigning a custom form error to a field in a modelform in django

I am trying to assign a custom form error to a field in a modelform in django so that it appears where a 'standard' error such as the field being left blank, with the same formatting (which is handled by crispy forms).
My model form clean method looks like this:
def clean(self):
cleaned_data = super(CreatorForm, self).clean()
try:
if cleaned_data['email'] != cleaned_data['re_email']:
raise forms.ValidationError({'email': "Your emails don't match!"})
except KeyError:
pass
return cleaned_data
And in my template I display the form/re-submitted form like this:
{{creator_form|crispy}}
I would like the error to appear below the re_email field if possible (though currently I thought I'd have better luck getting it below the email field. At the moment it appears at the top of the form, unformatted.
For the re_email field, despite not being part of the model, the error displayed for leaving it blank appears below the re_email field. How do I 'attach' errors to fields so they are displayed beneath/near them?
All help appreciated thanks
To get the error to display on a specific field you need to explicitly define what field the error goes on since you're overriding .clean(). Here is a sample taken from the Django docs:
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super(ContactForm, self).clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject and "help" not in subject:
# We know these are not in self._errors now (see discussion
# below).
msg = u"Must put 'help' in subject when cc'ing yourself."
self._errors["cc_myself"] = self.error_class([msg])
self._errors["subject"] = self.error_class([msg])
# These fields are no longer valid. Remove them from the
# cleaned data.
del cleaned_data["cc_myself"]
del cleaned_data["subject"]
# Always return the full collection of cleaned data.
return cleaned_data

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.")

Can I count on the order of field validation in a Django form?

I have a Django form with a username and email field. I want to check the email isn't already in use by a user:
def clean_email(self):
email = self.cleaned_data["email"]
if User.objects.filter(email=email).count() != 0:
raise forms.ValidationError(_("Email not available."))
return email
This works, but raises some false negatives because the email might already be in the database for the user named in the form. I want to change to this:
def clean_email(self):
email = self.cleaned_data["email"]
username = self.cleaned_data["username"]
if User.objects.filter(email=email, username__ne=username).count() != 0:
raise forms.ValidationError(_("Email not available."))
return email
The Django docs say that all the validation for one field is done before moving onto the next field. If email is cleaned before username, then cleaned_data["username"] won't be available in clean_email. But the docs are unclear as to what order the fields are cleaned in. I declare username before email in the form, does that mean I'm safe in assuming that username is cleaned before email?
I could read the code, but I'm more interested in what the Django API is promising, and knowing that I'm safe even in future versions of Django.
Update
.keyOrder no longer works. I believe this should work instead:
from collections import OrderedDict
class MyForm(forms.ModelForm):
…
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
field_order = ['has_custom_name', 'name']
reordered_fields = OrderedDict()
for fld in field_order:
reordered_fields[fld] = self.fields[fld]
for fld, value in self.fields.items():
if fld not in reordered_fields:
reordered_fields[fld] = value
self.fields = reordered_fields
Previous Answer
There are things that can alter form order regardless of how you declare them in the form definition. One of them is if you're using a ModelForm, in which case unless you have both fields declared in fields under class Meta they are going to be in an unpredictable order.
Fortunately, there is a reliable solution.
You can control the field order in a form by setting self.fields.keyOrder.
Here's some sample code you can use:
class MyForm(forms.ModelForm):
has_custom_name = forms.BooleanField(label="Should it have a custom name?")
name = forms.CharField(required=False, label="Custom name")
class Meta:
model = Widget
fields = ['name', 'description', 'stretchiness', 'egginess']
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
ordered_fields = ['has_custom_name', 'name']
self.fields.keyOrder = ordered_fields + [k for k in self.fields.keys() if k not in ordered_fields]
def clean_name(self):
data = self.cleaned_data
if data.get('has_custom_name') and not data.get('name'):
raise forms.ValidationError("You must enter a custom name.")
return data.get('name')
With keyOrder set, has_custom_name will be validated (and therefore present in self.cleaned_data) before name is validated.
The Django docs claim that it's in order of the field definition.
But I've found that it doesn't always hold up to that promise.
Source: http://docs.djangoproject.com/en/dev/ref/forms/validation/
These methods are run in the order
given above, one field at a time. That
is, for each field in the form (in the
order they are declared in the form
definition), the Field.clean() method
(or its override) is run, then
clean_(). Finally, once
those two methods are run for every
field, the Form.clean() method, or its
override, is executed.
There's no promise that the fields are processed in any particular order. The official recommendation is that any validation that depends on more than one field should be done in the form's clean() method, rather than the field-specific clean_foo() methods.
The Form subclass’s clean() method. This method can perform any
validation that requires access to multiple fields from the form at
once. This is where you might put in things to check that if field A
is supplied, field B must contain a valid email address and the like.
The data that this method returns is the final cleaned_data attribute
for the form, so don’t forget to return the full list of cleaned data
if you override this method (by default, Form.clean() just returns
self.cleaned_data).
Copy-paste from https://docs.djangoproject.com/en/dev/ref/forms/validation/#using-validators
This means that if you want to check things like the value of the email and the parent_email are not the same you should do it inside that function. i.e:
from django import forms
from myapp.models import User
class UserForm(forms.ModelForm):
parent_email = forms.EmailField(required = True)
class Meta:
model = User
fields = ('email',)
def clean_email(self):
# Do whatever validation you want to apply to this field.
email = self.cleaned_data['email']
#... validate and raise a forms.ValidationError Exception if there is any error
return email
def clean_parent_email(self):
# Do the all the validations and operations that you want to apply to the
# the parent email. i.e: Check that the parent email has not been used
# by another user before.
parent_email = self.cleaned_data['parent_email']
if User.objects.filter(parent_email).count() > 0:
raise forms.ValidationError('Another user is already using this parent email')
return parent_email
def clean(self):
# Here I recommend to user self.cleaned_data.get(...) to get the values
# instead of self.cleaned_data[...] because if the clean_email, or
# clean_parent_email raise and Exception this value is not going to be
# inside the self.cleaned_data dictionary.
email = self.cleaned_data.get('email', '')
parent_email = self.cleaned_data.get('parent_email', '')
if email and parent_email and email == parent_email:
raise forms.ValidationError('Email and parent email can not be the same')
return self.cleaned_data