dreamzboy
dreamzboy

Reputation: 841

Redisplay Form with Previously Submitted Data on Error in Django

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>

Form

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

Answers (2)

dreamzboy
dreamzboy

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.

Add_Name

Upvotes: 1

warath-coder
warath-coder

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

Related Questions