I recently started working on another new project here at Imaginary Landscape, and this one looked rather enticing as it threw some stuff my way that I haven't had a chance to play with much recently. First on that chopping block was a multi-page registration form application. Immediately I remembered reading about Django's Form Wizard module and thought it'd be a great way to handle this small application. At least I thought so until I finished reading the specifications for the project and realized that the user must be able to move both backwards and forwards through the form. Oh, and as a bonus, I was dealing with sensitive information, so I had to be careful about what I stashed in sessions.
So unfortunately Form Wizard wasn't going to be able to handle my needs, as it doesn't really support going backwards in the forms. Also, it's built for use with django.forms.Form, not django.forms.ModelForm like I wanted to use since I have to store all this information in a database anyway. In the end I went with storing object IDs in the user's session. This would allow me to grab the data that was submitted based off the ID, avoid storing sensitive information in sessions, and provide me with all the functionality that I was looking to achieve. This actually ended up being more straightforward than I had initially expected. First you need some forms:
class UserInfoForm(forms.ModelForm):
class Meta:
model = models.UserInfo
class AddressForm(forms.ModelForm):
class Meta:
model = models.Address
Let's just pretend these are connected to very basic models that fit their namesake. Now we'll need to create some views:
def user_information(request):
if request.method == "POST":
form = forms.UserInfoForm(request.POST)
if form.is_valid():
user_info = form.save()
request.session['user_info_id'] = user_info.id
return HttpResponseRedirect(
reverse("address_form"))
else:
if 'user_info_id' in request.session:
try:
user_info_obj = models.UserInfo.objects.get(
id=request.session['user_info_id']
)
form = forms.UserInfoForm(instance=user_info_obj)
except ObjectDoesNotExist:
del request.session['user_info_id']
form = forms.UserInfoForm()
else:
form = forms.UserInfoForm()
return render_to_response(
"app/user_info.html",
locals(), context_instance=RequestContext(request))
As you can see this is fairly straightforward. It's just how you'd handle a normal form, but after we save our form we're injecting the "user_info.id" into the session. We'll be needing this if we decide to go backwards from our next form. If we're not posting, we check to see if that ID is in the session. If it is, we load that object, and initialize the form it. If that object doesn't exist, we clear the session item and create a blank form for the user to fill out. Now we have our second form:
def address(request):
if 'user_info_id' not in request.session:
return HttpResponseRedirect(
reverse("user_info_form"))
if request.method == "POST":
user_info_obj = models.UserInfo.objects.get(
id=request.session['user_info_id']
)
new_address = models.Address(patient=user_info_obj)
form = forms.AddressForm(request.POST,
instance=new_address)
if form.is_valid():
address_info = form.save()
request.session['address_info_id'] = address_info.id
## Send them to a thank you page
else:
if 'address_info_id' in request.session:
try:
user_info_obj = models.UserInfo.objects.get(
id=request.session['user_info_id']
)
except ObjectDoesNotExist:
return HttpResponseRedirect(
reverse("user_info_form"))
try:
address_obj = models.Address.objects.get(
id=request.session['address_info_id']
)
form = forms.AddressForm(instance=address_obj)
except ObjectDoesNotExist:
user_info_obj = models.UserInfo.objects.get(
id=request.session['user_info_id']
)
form = forms.AddressForm(
initial={'user': user_info_obj.id})
else:
user_info_obj = models.UserInfo.objects.get(
id=request.session['user_info_id']
)
form = forms.AddressForm(
initial={'user': user_info_obj.id})
return render_to_response(
"app/address.html",
locals(), context_instance=RequestContext(request))
This follows the same basic principle as the first form. As an added treat, it also uses some data from the first form. If we don't have that data, that means the user didn't fill out the first form. In that case, we send them back to the first form to properly fill it out. We also stash the ID of the address object in the sessions as well, which if this is the last form (which it is in this example) isn't totally necessary. You can choose to clear all the items from the session that you put in there if you prefer. However, if the user uses their browser's back button to go back to the first form and submit it, then the second form will still be initialized with the data they previously put in there.
Surprisingly, that pretty much sums up creating a multi-page form in Django. Naturally there are many ways to accomplish a multi-page form. If you're using simple forms and don't need to enable users to browse backwards, then I highly recommend FormWizard for ease of use. It's really easy to use, and handles most use cases. If you need something a bit more complex, the code above should work out for you. Though I must warn you, this code was adapted to this blog post from an existing project. As such, there might be some minor issues I missed in adaptation. Please let me know in the comments if you see any such issues.
Updated 12/14/09 @ 11:13AM CST by markr
Categories: Django



Comments
Brian Moloney
I spent a lot of time at the Bingo wheel to come up with the term "bi-lateral navigation." And here you are describing it as "browse backwards and forwards." Shame on you for being so clear and jargon-free.
Mike Fotinakis
Just wanted to mention that there is also a Django app called django-dataforms that can do this dynamically using the (currently undocumented) "sections" feature. I'll add some code to our example application to make it clear how sections would be used. If you're interested: http://code.google.com/p/django-dataforms/
Frank Wiles
Overall this is great, but I just wanted to point out that there isn't any real security concern with storing "sensitive" data in a Django session. Any more than storing it in your database or cache system at least. Django only puts the session key in the cookie, not the data itself.
joecasper03
Is there anyway I could see the model for this? I am trying to adapt this to a 6 step form and I am getting a disconnect somewhere and I am suspecting its in the models! Any help would be appreciated. Thanks.
Mark Rogers
joecasper03: The models are actually really basic. UserInfo is just a collection of user information fields (name, phone number, etc). Address information is just a collection of typical address fields (street, city, zip). There really isn't much in them unfortunately.
joecasper03
Is there a relationship between the Address and UserInfo models?
joecasper03
I guess more specifically my question boils down to: What does this line mean? new_address = models.Address(patient=user_info_obj)
Mark Rogers
There is a foreign key relationship between the two, yes. Patient is the field name in Address that FKs to the UserInfo object. That line you mention I'm creating the start of an object to feed to the model form, because there's some information that the user won't need to enter, so I've made those fields hidden.