I am trying to add an error class to fields of a each form in a formset if a custom clean method detects errors. This does look to do the trick, I load the page, and the field does have the error class in it. but when in the template I add a custom filter to add a form-control class, everything falls apart.
# in my inlineformset:
def clean(self, *args, **kwargs):
if any(self.errors):
errors = self.errors
return
## 1) Total amount
total_amount = 0
for form in self.forms:
if self.can_delete and self._should_delete_form(form):
continue
amount = form.cleaned_data.get('amount')
total_amount += amount
if total_amount> 100:
for form in self.forms:
form.fields['amount'].widget.attrs.update({'class': 'error special'})
raise ValidationError(_('Total amount cannot exceed 100%'))
And, here is my code for the custom filter:
#register.filter(name = 'add_class')
def add_class(the_field, class_name):
''' Adds class_name to the string of space-separated CSS classes for this field'''
initial_class_names = the_field.css_classes() ## This returns empty string, but it should return 'error special'
class_names = initial_class_names + ' ' + class_name if initial_class_names else class_name
return the_field.as_widget(attrs = {'class': class_names,})
And, in my template:
{# {{ the_field|add_class:"form-control"}} #} #<- This adds the form-control, but removes the other classes added in the clean method
{{ the_field }} {# This shows the two classes for the offending fields, 'error special' #}
I think the problem is with the .css_classes() method which does not bring in the classes defined on the form. Remember, these classes have been set on these fields and rendering {{ the_field }} shows the classes were correctly passed down to the template. So, then the question is whether I am using the correct method .css_classes() or if I should use another method?
I was able to add class error to the field using .add_error method of the form. Although this works around the problem, I would still appreciate if anyone can explain how come the the_field.css_classes() returned an empty string instead of what was set in the clean method:
form.fields['amount'].widget.attrs.update({'class': 'error special'})
The problem with add_error method is that it only adds the class error. But, what if I'd want to add another class special to the widget? So, the original problem still needs an answer. My solution here then is just a solution and not the solution:
# in my inlineformset:
def clean(self, *args, **kwargs):
if any(self.errors):
errors = self.errors
return
## 1) Total amount
total_amount = 0
for form in self.forms:
if self.can_delete and self._should_delete_form(form):
continue
amount = form.cleaned_data.get('amount')
total_amount += amount
if total_amount > 100:
msg = 'This field throw the amount over the 100% limit'
form.add_error('amount', msg)
if total_amount> 100:
raise ValidationError(_('Total amount cannot exceed 100%'))
Related
Django form trips me many times...
I gave initial value to a ChoiceField (init of Form class)
self.fields['thread_type'] = forms.ChoiceField(choices=choices,
widget=forms.Select,
initial=thread_type)
The form which is created with thread_type with the above code doesn't pass is_valid() because 'this field(thread_type) is required'.
-EDIT-
found the fix but it still perplexes me quite a bit.
I had a code in my template
{% if request.user.is_administrator() %}
<div class="select-post-type-div">
{{form.thread_type}}
</div>
{% endif %}
and when this form gets submitted, request.POST doesn't have 'thread_type' when user is not admin.
the view function creates the form with the following code:
form = forms.MyForm(request.POST, otherVar=otherVar)
I don't understand why giving initial value via the the following(the same as above) is not enough.
self.fields['thread_type'] = forms.ChoiceField(choices=choices,
widget=forms.Select,
initial=thread_type)
And, including the thread_type variable in request.POST allows the form to pass the is_valid() check.
The form class code looks like the following
class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
title = TitleField()
tags = TagNamesField()
#some more fields.. but removed for brevity, thread_type isn't defined here
def __init__(self, *args, **kwargs):
"""populate EditQuestionForm with initial data"""
self.question = kwargs.pop('question')
self.user = kwargs.pop('user')#preserve for superclass
thread_type = kwargs.pop('thread_type', self.question.thread.thread_type)
revision = kwargs.pop('revision')
super(EditQuestionForm, self).__init__(*args, **kwargs)
#it is important to add this field dynamically
self.fields['thread_type'] = forms.ChoiceField(choices=choices, widget=forms.Select, initial=thread_type)
Instead of adding this field dynamically, define it in the class appropriately:
class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
title = TitleField()
tags = TagNamesField()
thread_type = forms.ChoiceField(choices=choices, widget=forms.Select)
When creating the form instance set an intitial value if needed:
form = EditQuestionForm(initial={'tread_type': thread_type})
And if you dont need this field, just delete it:
class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
def __init__(self, *args, **kwargs):
super(EditQuestionForm, self).__init__(*args, **kwargs)
if some_condition:
del self.fields['thread_type']
When saving form, check:
thread_type = self.cleaned_data['thread_type'] if 'thread_type' in self.cleaned_data else None
This approach always works well for me.
In a form I am using a MultiValueField (MVF) with a MultiWidget that has several fields. If there is a validation error in one of the fields of the MVF, this gets handled (displayed) at the MVF level, rather than at the individual sub-fields, which can lead to:
* Ensure this value is greater than or equal to 1.
* Ensure this value is greater than or equal to -100.0.
Number of days: -1
...
...
Threshold: -200
Where the first error refers to the first field of the MVF and the second error the the last field of the MVF.
Is it possible to put those error messages "inside" the MVF at the field where they belong? (maybe in the format_output method of the MultiWidget?)
The following solution doesn't use a MultiValueField but instead:
dynamically replaces the original field with several ones on form's __init__
reconstruct valid data for the original field during the form validation on _post_clean
Here is some test code that needs to be adapted to each case:
class MyMultiField(CharField):
def split(self, form):
name = 'test'
form.fields_backup[name] = form.fields[name]
del form.fields[name]
# here is where you define your individual fields:
for i in range(3):
form.fields[name + '_' + str(i)] = CharField()
# you need to extract the initial data for these fields
form.initial[name + '_' + str(i)] = somefunction(form.initial[name])
form.fields['test_1'] = DecimalField() # because I only want numbers in the 2nd field
def restore(self, form):
# here is where you describe how to joins the individual fields:
value = ''.join([unicode(v) for k, v in form.cleaned_data.items() if 'test_' in k])
# extra step to validate the combined value against the original field:
try:
restored_data = form.cleaned_data.copy()
restored_data["test"] = form.fields_backup["test"].clean(value)
for k in form.cleaned_data:
if k.startswith("test_"):
del restored_data[k]
form.cleaned_data = restored_data
except Exception, e:
form._errors[NON_FIELD_ERRORS] = form.error_class(e)
class MyForm(Form):
test = MyMultiField()
def __init__(self, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
self.fields_backup = {}
self.fields['data'].split(self)
def _post_clean(self):
self.fields_backup['data'].restore(self)
return super(MyForm, self)._post_clean()
Before:
After (validating some input):
I'm not sure if it's possible to decouple this Field/Form code further using this approach. I'm also not quite satisfied with this code as the new field class needs to inherit from the original one.
Nonetheless, the basic idea is there and I successfully used it to individually validate form fields built from a dictionary stored in a single model field with PostgreSQL hstore.
I've got some <selects> that I need to populate with some choices that depend on the currently logged in user. I don't think this is possible (or easy) to do from inside the form class, so can I just leave the choices blank and set them in the view instead? Or what approach should I take?
Not sure if this is the best answer, but in the past I have set the choices of a choice field in the init of the form - you could potentially pass your choices to the constructor of your form...
You could build your form dynamically in you view (well, actually i would rather keep the code outside the view in it's own function and just call it in the view but that's just details)
I did it like this in one project:
user_choices = [(1, 'something'), (2, 'something_else')]
fields['choice'] = forms.ChoiceField(
choices=user_choices,
widget=forms.RadioSelect,
)
MyForm = type('SelectableForm', (forms.BaseForm,), { 'base_fields': fields })
form = MyForm()
Obviously, you will want to create the user_choices depending on current user and add whatever field you need along with the choices, but this is a basic principle, I'll leave the rest as the reader exercise.
Considering that you have included the user as a parameter, I would solve this using a custom tag.
In your app/templatetags/custom_tags.py something like this:
#register.simple_tag
def combo(user, another_param):
objects = get_objects(user, another_param)
str = '<select name="example" id="id_example">'
for object in objects:
str += '<option value="%s">%s</option>' % (object.id, object.name)
str += '</select>'
return mark_safe(str)
Then in your template:
{% load custom_tags %}
{% special_select user another_param %}
More about custom tags http://docs.djangoproject.com/en/dev/howto/custom-template-tags/
Django create dynamic forms - It works !!
Forms.py
class MyForm(forms.Form):
""" Initialize form values from views"""
select=forms.BooleanField(label='',required=False)
field_1=forms.CharField(label='',widget=forms.TextInput(attrs= \
{'size':'20','readonly':'readonly',}))
field_2=forms.ChoiceField(widget=forms.Select(), \
choices=((test.id,test.value) for test in test.objects.all()))
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
input=kwargs.get('initial',{})
get_field_1_initial_input_from_views=re.sub("\[|\]|u'|'","",str (input.values()))
# override field_2 choices based on field_1 input
try:
# filter choices
self.fields[‘field_2'].choices=((test.id,test.value) for test in test.objects.filter ( --------)))
except:
pass
Views.py
def views_function(request,val):
"""Dynamically generate input data for formset."""
Initial_data=[]
initial_data.append({'field_1':data.info})
#Initializing formset
MyFormSet=formset_factory(MyForm,extra=0)
formset=MyFormSet(initial=initial_data)
context={'formset':formset}
if request.method == 'POST':
formset=MyFormSet(request.POST,request.FILES)
if formset.is_valid():
# You can work with the formset dictionary elements in the views function (or) pass it to
#Forms.py script through an instance of MyForm
return HttpResponse(formset.cleaned_data)
return render_to_response(‘test.html', context,context_instance=RequestContext(request))
A form will be spitting out an unknown number of questions to be answered. each question contains a prompt, a value field, and a unit field. The form is built at runtime in the formclass's init method.
edit: each questions receives a unique prompt to be used as a label, as well as a unique list of units for the select element.
this seems a case perfect for iterable form fieldsets, which could be easily styled. but since fieldsets - such as those in django-form-utils are defined as tuples, they are immutable... and I can't find a way to define them at runtime. is this possible, or perhaps another solution?
Edit:
formsets with initial_data is not the answer - initial_data merely enables the setting of default values for the form fields in a formset. a list of items can't be sent to the choicefield constructor by way of initial_data.
...unless I'm wrong.
Check out formsets. You should be able to pass in the data for each of the N questions as initial data. Something along these lines:
question_data = []
for question in your_question_list:
question_data.append({'prompt': question.prompt,
'value': question.value,
'units': question.units})
QuestionFormSet = formset_factory(QuestionForm, extra=2)
formset = QuestionFormSet(initial=question_data)
Old question but I am running into a similar problem. The closest thing that I have found so far is this snippet based of a post that Malcom did a couple years ago now.
http://djangosnippets.org/snippets/1955/
The original snippet did not address the template side and splitting them up into fieldsets, but adding each form to its own fieldset should accomplish that.
forms.py
from django.forms.formsets import Form, BaseFormSet, formset_factory, \
ValidationError
class QuestionForm(Form):
"""Form for a single question on a quiz"""
def __init__(self, *args, **kwargs):
# CODE TRICK #1
# pass in a question from the formset
# use the question to build the form
# pop removes from dict, so we don't pass to the parent
self.question = kwargs.pop('question')
super(QuestionForm, self).__init__(*args, **kwargs)
# CODE TRICK #2
# add a non-declared field to fields
# use an order_by clause if you care about order
self.answers = self.question.answer_set.all(
).order_by('id')
self.fields['answers'] = forms.ModelChoiceField(
queryset=self.answers())
class BaseQuizFormSet(BaseFormSet):
def __init__(self, *args, **kwargs):
# CODE TRICK #3 - same as #1:
# pass in a valid quiz object from the view
# pop removes arg, so we don't pass to the parent
self.quiz = kwargs.pop('quiz')
# CODE TRICK #4
# set length of extras based on query
# each question will fill one 'extra' slot
# use an order_by clause if you care about order
self.questions = self.quiz.question_set.all().order_by('id')
self.extra = len(self.questions)
if not self.extra:
raise Http404('Badly configured quiz has no questions.')
# call the parent constructor to finish __init__
super(BaseQuizFormSet, self).__init__(*args, **kwargs)
def _construct_form(self, index, **kwargs):
# CODE TRICK #5
# know that _construct_form is where forms get added
# we can take advantage of this fact to add our forms
# add custom kwargs, using the index to retrieve a question
# kwargs will be passed to our form class
kwargs['question'] = self.questions[index]
return super(BaseQuizFormSet, self)._construct_form(index, **kwargs)
QuizFormSet = formset_factory(
QuestionForm, formset=BaseQuizDynamicFormSet)
views.py
from django.http import Http404
def quiz_form(request, quiz_id):
try:
quiz = Quiz.objects.get(pk=quiz_id)
except Quiz.DoesNotExist:
return Http404('Invalid quiz id.')
if request.method == 'POST':
formset = QuizFormSet(quiz=quiz, data=request.POST)
answers = []
if formset.is_valid():
for form in formset.forms:
answers.append(str(int(form.is_correct())))
return HttpResponseRedirect('%s?a=%s'
% (reverse('result-display',args=[quiz_id]), ''.join(answers)))
else:
formset = QuizFormSet(quiz=quiz)
return render_to_response('quiz.html', locals())
template
{% for form in formset.forms %}
<fieldset>{{ form }}</fieldset>
{% endfor %}
I used the trick below to create a dynamic formset. Call the create_dynamic_formset() function from your view.
def create_dynamic_formset(name_filter):
"""
-Need to create the classess dynamically since there is no other way to filter
"""
class FormWithFilteredField(forms.ModelForm):
type = forms.ModelChoiceField(queryset=SomeType.objects.filter(name__icontains=name_filter))
class Meta:
model=SomeModelClass
return modelformset_factory(SomeModelClass, form=FormWithFilteredField)
Here is what I used for a similar case (a variable set of fieldsets, each one containing a variable set of fields).
I used the type() function to build my Form Class, and BetterBaseForm class from django-form-utils.
def makeFurnitureForm():
"""makeFurnitureForm() function will generate a form with
QuantityFurnitureFields."""
furnitures = Furniture.objects.all()
fieldsets = {}
fields = {}
for obj in furnitures:
# I used a custom Form Field, but you can use whatever you want.
field = QuantityFurnitureField(name = obj.name)
fields[obj.name] = field
if not obj.room in fieldsets.keys():
fieldsets[obj.room] = [field,]
else:
fieldsets[obj.room].append(field)
# Here I use a double list comprehension to define my fieldsets
# and the fields within.
# First item of each tuple is the fieldset name.
# Second item of each tuple is a dictionnary containing :
# -The names of the fields. (I used a list comprehension for this)
# -The legend of the fieldset.
# You also can add other meta attributes, like "description" or "classes",
# see the documentation for further informations.
# I added an example of output to show what the dic variable
# I create may look like.
dic = [(name, {"fields": [field.name for field in fieldsets[name]], "legend" : name})
for name in fieldsets.keys()]
print(dic)
# Here I return a class object that is my form class.
# It inherits from both forms.BaseForm and forms_utils.forms.BetterBaseForm.
return (type("FurnitureForm",
(forms.BaseForm, form_utils.forms.BetterBaseForm,),
{"_fieldsets" : dic, "base_fields" : fields,
"_fieldset_collection" : None, '_row_attrs' : {}}))
Here is an example of how dic may look like :
[('fieldset name 1',
{'legend': 'fieldset legend 2',
'fields' ['field name 1-1']}),
('fieldset name 2',
{'legend': 'fieldset legend 2',
'fields' : ['field 1-1', 'field 1-2']})]
I used BetterBaseForm rather than BetterForm for the same reason this article suggests to use BaseForm rather than Form.
This article is interesting even if it's old, and explains how to do dynamic forms (with variable set of fields). It also gives other ways to achieve dynamic forms.
It doesn't explain how to do it with fieldsets though, but it inspired me to find how to do it, and the principle remains the same.
Using it in a view is pretty simple :
return (render(request,'main/form-template.html', {"form" : (makeFurnitureForm())()}))
and in a template :
<form method="POST" name="myform" action=".">
{% csrf_token %}
<div>
{% for fieldset in form.fieldsets %}
<fieldset>
<legend>{{ fieldset.legend }}</legend>
{% for field in fieldset %}
<div>
{% include "main/furniturefieldtemplate.html" with field=field %}
</div>
{% endfor %}
</fieldset>
{% endfor %}
</div>
<input type="submit" value="Submit"/>
</form>
I would like to make an entire inline formset within an admin change form compulsory. So in my current scenario when I hit save on an Invoice form (in Admin) the inline Order form is blank. I'd like to stop people creating invoices with no orders associated.
Anyone know an easy way to do that?
Normal validation like (required=True) on the model field doesn't appear to work in this instance.
The best way to do this is to define a custom formset, with a clean method that validates that at least one invoice order exists.
class InvoiceOrderInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data:
count += 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You must have at least one order')
class InvoiceOrderInline(admin.StackedInline):
formset = InvoiceOrderInlineFormset
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvoiceOrderInline]
Daniel's answer is excellent and it worked for me on one project, but then I realized due to the way Django forms work, if you are using can_delete and check the delete box while saving, it's possible to validate w/o any orders (in this case).
I spent a while trying to figure out how to prevent that from happening. The first situation was easy - don't include the forms that are going to get deleted in the count. The second situation was trickier...if all the delete boxes are checked, then clean wasn't being called.
The code isn't exactly straightforward, unfortunately. The clean method is called from full_clean which is called when the error property is accessed. This property is not accessed when a subform is being deleted, so full_clean is never called. I'm no Django expert, so this might be a terrible way of doing it, but it seems to work.
Here's the modified class:
class InvoiceOrderInlineFormset(forms.models.BaseInlineFormSet):
def is_valid(self):
return super(InvoiceOrderInlineFormset, self).is_valid() and \
not any([bool(e) for e in self.errors])
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data and not form.cleaned_data.get('DELETE', False):
count += 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You must have at least one order')
class MandatoryInlineFormSet(BaseInlineFormSet):
def is_valid(self):
return super(MandatoryInlineFormSet, self).is_valid() and \
not any([bool(e) for e in self.errors])
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data and not form.cleaned_data.get('DELETE', False):
count += 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You must have at least one of these.')
class MandatoryTabularInline(admin.TabularInline):
formset = MandatoryInlineFormSet
class MandatoryStackedInline(admin.StackedInline):
formset = MandatoryInlineFormSet
class CommentInlineFormSet( MandatoryInlineFormSet ):
def clean_rating(self,form):
"""
rating must be 0..5 by .5 increments
"""
rating = float( form.cleaned_data['rating'] )
if rating < 0 or rating > 5:
raise ValidationError("rating must be between 0-5")
if ( rating / 0.5 ) != int( rating / 0.5 ):
raise ValidationError("rating must have .0 or .5 decimal")
def clean( self ):
super(CommentInlineFormSet, self).clean()
for form in self.forms:
self.clean_rating(form)
class CommentInline( MandatoryTabularInline ):
formset = CommentInlineFormSet
model = Comment
extra = 1
#Daniel Roseman solution is fine but i have some modification with some less code to do this same.
class RequiredFormSet(forms.models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(RequiredFormSet, self).__init__(*args, **kwargs)
self.forms[0].empty_permitted = False
class InvoiceOrderInline(admin.StackedInline):
model = InvoiceOrder
formset = RequiredFormSet
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvoiceOrderInline]
try this it also works :)
The situation became a little bit better but still needs some work around. Django provides validate_min and min_num attributes nowadays, and if min_num is taken from Inline during formset instantiation, validate_min can be only passed as init formset argument. So my solution looks something like this:
class MinValidatedInlineMixIn:
validate_min = True
def get_formset(self, *args, **kwargs):
return super().get_formset(validate_min=self.validate_min, *args, **kwargs)
class InvoiceOrderInline(MinValidatedInlineMixIn, admin.StackedInline):
model = InvoiceOrder
min_num = 1
validate_min = True
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvoiceOrderInline]