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

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():
...

Related

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

Duplication check before saving the form data in Django

I got a form as following:
class CourseAddForm(forms.ModelForm):
"""Add a new course"""
name = forms.CharField(label=_("Course Name"), max_length=100)
description = forms.Textarea()
course_no = forms.CharField(label=_("course Number"), max_length=15)
#Attach a form helper to this class
helper = FormHelper()
helper.form_id = "addcourse"
helper.form_class = "course"
#Add in a submit and reset button
submit = Submit("Add", "Add New Record")
helper.add_input(submit)
reset = Reset("Reset", "Reset")
helper.add_input(reset)
def clean(self):
"""
Override the default clean method to check whether this course has been already inputted.
"""
cleaned_data = self.cleaned_data
name = cleaned_data.get('name')
hic = cleaned_data.get('course_no')
try:
course=Course.objects.get(name=name)
except Course.DoesNotExist:
course=None
if course:
msg = u"Course name: %s has already exist." % name
self._errors['name'] = self.error_class([msg])
del cleaned_data['name']
return cleaned_data
else:
return self.cleaned_data
class Meta:
model = Course
As you can see I overwrote the clean method to check whether this course has already existed in the database when the user is trying to add it. This works fine for me.
However, when I want to add the same check for the form for editing, the problem happened. Because it is editing, so the record with same course name has already exist in the DB. Thus, the same check would throw error the course name has already exist. But I need to check the duplication in order to avoid the user updating the course name to another already existed course name.
I am thinking of checking the value of the course name to see if it is changed. If it has been changed, than I can do the same check as above. If it has not been changed, I don't need to do the check. But I don't know how can I obtain the origin data for editing.
Does anyone know how to do this in Django?
My view looks as following:
#login_required
#csrf_protect
#never_cache
#custom_permission_required('records.change_course', 'course')
def edit_course(request,course_id):
# See if the family exists:
try:
course = Course.objects.get(id=course_id)
except Course.DoesNotExist:
course = None
if course:
if request.method == 'GET':
form = CourseEditForm(instance=course)
return render_to_response('records/add.html',
{'form': form},
context_instance=RequestContext(request)
)
elif request.method == 'POST':
form = CourseEditForm(request.POST, instance=course)
if form.is_valid():
form.save()
return HttpResponseRedirect('/records/')
# form is not valid:
else:
error_message = "Please correct all values marked in red."
return render_to_response('records/edit.html',
{'form': form, 'error_message': error_message},
context_instance=RequestContext(request)
)
else:
error = "Course %s does not exist. Press the 'BACK' button on your browser." % (course)
return HttpResponseRedirect(reverse('DigitalRecords.views.error', args=(error,)))
Thank you.
I think you should just set unique=True on the Course.name field and let the framework handle that validation for you.
Update:
Since unique=True is not the right answer for your case, you can check this way:
def clean(self):
"""
Override the default clean method to check whether this course has
been already inputted.
"""
cleaned_data = self.cleaned_data
name = cleaned_data.get('name')
matching_courses = Course.objects.filter(name=name)
if self.instance:
matching_courses = matching_courses.exclude(pk=self.instance.pk)
if matching_courses.exists():
msg = u"Course name: %s has already exist." % name
raise ValidationError(msg)
else:
return self.cleaned_data
class Meta:
model = Course
As a side note, I've also changed your custom error handling to use a more standard ValidationError.
I believe excluding the current instance id from the results would solve the problem:
from django.db.models import Q
try:
qs = Course.objects.filter(name=self.cleaned_data.get('name'))
if self.instance.pk is not None:
qs = qs.filter(~Q(pk=self.instance.pk))
course = qs.get()
except Course.DoesNotExist:
course = None
However as dokkaebi pointed out, unique is really the better way to go with this, as this solution is vulnerable to race conditions. I'm not sure what your datamodel looks like but I suspect defining
class Meta:
unique_together = ('department', 'name')
should accomplish what you want.

Django forms raise general validation error if all fields empty

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()

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