Reputation: 841
How do I get the submitted data to re-display in a formset after it encounters an error? Django book talked about this and I was able to make it work in a form but not in a formset.
Scenario: There are 2 required fields - First name and last name. If I only entered 'John' in the first name field leaving the last name field empty and click submit, I will get an error saying that both fields are required and the form cleared the submitted data with the error message. I want to see the name 'John' in the first name field along with the error messages.
Much appreciated!
**forms.py**
from django import forms
from django.forms.formsets import BaseFormSet
class NameForm (forms.Form):
first_name = forms.CharField (max_length = 20, required = False)
last_name = forms.CharField (max_length = 20, required = False)
class BaseNameFormSet (BaseFormSet):
def clean (self):
if any (self.errors):
return
firstnames = []
lastnames = []
for form in self.forms:
firstname = form.cleaned_data.get ('first_name')
lastname = form.cleaned_data.get ('last_name')
if (firstname in firstnames) or (lastname in lastnames):
raise forms.ValidationError ('First or last name must be unique')
if (firstname == '') or (lastname == ''):
raise forms.ValidationError ('Both first and last name must be filled out')
firstnames.append (firstname)
lastnames.append (lastname)
return self.cleaned_data
**views.py**
from django.shortcuts import render
from django.forms.formsets import formset_factory
from nameform.forms import NameForm
from nameform.forms import BaseNameFormSet
from nameform.addName import webform
# Create your views here.
def addname (request):
# Set maximum to avoid default of 1000 forms.
NameFormSet = formset_factory (NameForm, formset = BaseNameFormSet, extra = 2, max_num = 5)
if request.method == 'POST':
# Django will become valid even if an empty form is submitted. Adding initial data causes unbound form and
# trigger formset.errors
# formset = NameFormSet (request.POST, initial = [{'first_name': 'John', 'last_name': 'Doe'}])
formset = NameFormSet (request.POST)
if formset.is_valid ():
location = request.POST ['site']
data = formset.cleaned_data
for form in data:
# Retrieving the value in an empty dictionary will result key error as it doesn't exist
# firstname = form ['first_name']
# lastname = form ['last_name']
firstname = form.get ('first_name')
lastname = form.get ('last_name')
if firstname and lastname:
webform (firstname, lastname, location)
context = {'data': data, 'location': location}
return render (request, 'nameform/success.html', context)
else:
formset = NameFormSet ()
return render (request, 'nameform/addname.html', {
'formset': formset,
'first_name': request.POST.get ('first_name', ''),
'last_name': request.POST.get ('last_name', '')
})
**addname.html**
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Add Name</title>
</head>
<body>
<h1>Add Name</h1>
<form action="" method="POST">{% csrf_token %}
<h3>Location:</h3>
<p><input type="radio" id="radio1" name="site" value="Location_A" checked>
<label for="radio1">Location A</label>
<input type="radio" id="radio2" name="site" value="Location_B">
<label for="radio2">Location B</label></p>
{{ formset.management_form }}
<label for="id_form-0-first_name">First Name:</label>
<input id="id_form-0-first_name" maxlength="20" name="form-0-first_name" type="text" value="{{ first_name }}" />
<label for="id_form-0-last_name">Last Name:</label>
<input id="id_form-0-last_name" maxlength="20" name="form-0-last_name" type="text" value="{{ last_name }}" />
<p><label for="id_form-1-first_name">First Name:</label>
<input id="id_form-1-first_name" maxlength="20" name="form-1-first_name" type="text" value="{{ first_name }}" />
<label for="id_form-1-last_name">Last Name:</label>
<input id="id_form-1-last_name" maxlength="20" name="form-1-last_name" type="text" value="{{ last_name }}" /></p>
<p><input type="submit" value="Add Name"></p>
</form>
{% if formset.errors %}
<p style="color: red;">Please correct the error below:</p>
{{ formset.non_form_errors }}
{% endif %}
</body>
</html>
In Shell:
>>> partial_data = {
... 'form-TOTAL_FORMS': '2',
... 'form-INITIAL_FORMS': '0',
... 'form-MAX_NUM_FORMS': '3',
... 'form-0-first_name': 'John',
... 'form-0-last_name': 'Doe',
... 'form-1-first_name': 'James',
... 'form-1-last_name': ''
... }
>>>
>>> NameFormSet = formset_factory (NameForm, formset = BaseNameFormSet)
>>> formset = NameFormSet (partial_data)
>>>
>>> formset.is_valid ()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors ()
['Both first and last name must be filled out']
Upvotes: 2
Views: 1869
Reputation: 841
As it turned out, I need to make the changes below. The key here is to associate the "first_name_0" variable with the input field in html, "form-0-first_name". If you make both variable and the name of the html input box the same, you will get an error.
For example:
return render (request, 'nameform/addname.html', {
'formset': formset,
'form-0-first_name': request.POST.get ('form-0-first_name', '')})
Django doesn't like that format. Instead make it the same with what I have below to match which input field with the variable.
views.py
...
...
...
return render (request, 'nameform/addname.html', {
'formset': formset,
'first_name_0': request.POST.get ('form-0-first_name', ''),
'last_name_0': request.POST.get ('form-0-last_name', ''),
'first_name_1': request.POST.get ('form-1-first_name', ''),
'last_name_1': request.POST.get ('form-1-last_name', '')
addname.hmtl
...
...
...
{{ formset.management_form }}
<label for="id_form-0-first_name">First Name:</label>
<input id="id_form-0-first_name" maxlength="20" name="form-0-first_name" type="text" value="{{ first_name_0 }}" />
<label for="id_form-0-last_name">Last Name:</label>
<input id="id_form-0-last_name" maxlength="20" name="form-0-last_name" type="text" value="{{ last_name_0 }}" />
<p><label for="id_form-1-first_name">First Name:</label>
<input id="id_form-1-first_name" maxlength="20" name="form-1-first_name" type="text" value="{{ first_name_1 }}" />
<label for="id_form-1-last_name">Last Name:</label>
<input id="id_form-1-last_name" maxlength="20" name="form-1-last_name" type="text" value="{{ last_name_1 }}" /></p>
The desired result:
Now the user know what they did wrong or what was missing without refilling the whole form.
Upvotes: 1
Reputation: 2122
formsets are exactly the same as forms, just load it with the request.POST.
here is some code in one of my projects.
formset = modelformset_factory(PaymentTemplates, extra=extras, form=PaymentTemplateForm)
if request.POST:
formset = formset(request.POST)
in your template:
{% if formset.total_error_count %}
<ul class="errorList">
{% for dict in formset.errors %}
{% for error in dict.values %}
<li>{{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
i assume you also know that you need this:
<form action="..." method="POST">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
{{ form.as_table }}
{% endfor %}
</form>
look up how to render forms manually, or use something like crispy forms, or django-bootstrap3 which has template tags to handle all this.
Upvotes: 0