39

I would like to create a mutli-step form in Django that only submits the data for processing at the end of all the steps. Each step needs to be able to access and display data that we entered in previous step(s).

Is there a way to do this with Django? Django's Form-Wizard can't handle this basic functionality.

Saqib Ali
  • 11,931
  • 41
  • 133
  • 272

2 Answers2

41

Of course there's a way to do this in Django.

One way is to hold your values in session until you submit them at the end. You can populate your forms using values held in session if you return to previous step.

With some searching, you may find an app that someone has already written that will do what you want, but doing what you need isn't hard to do with Django, or any other framework.

Example, ignoring import statements:

#models/forms

class Person(models.Model):
    fn = models.CharField(max_length=40)

class Pet(models.Model):
    owner = models.ForeignKey(Person)
    name = models.CharField(max_length=40)

class PersonForm(forms.ModelForm):
    class Meta:
        model = Person

class PetForm(forms.ModelForm):
    class Meta:
        model = Pet
        exclude = ('owner',)

#views
def step1(request):
    initial={'fn': request.session.get('fn', None)}
    form = PersonForm(request.POST or None, initial=initial)
    if request.method == 'POST':
        if form.is_valid():
            request.session['fn'] = form.cleaned_data['fn']
            return HttpResponseRedirect(reverse('step2'))
    return render(request, 'step1.html', {'form': form})

def step2(request):
    form = PetForm(request.POST or None)
    if request.method == 'POST':
        if form.is_valid():
            pet = form.save(commit=False)
            person = Person.objects.create(fn=request.session['fn'])
            pet.owner = person
            pet.save()
            return HttpResponseRedirect(reverse('finished'))
    return render(request, 'step2.html', {'form': form})

We'll assume that step2.html has a link to go back to step1.html.

You'll notice in the step1 view I'm pulling the value for fn from session that was set when the form was saved. You would need to persist the values from all previous steps into the session. At the end of the steps, grab the values, create your objects and redirect to a finished view, whatever that might be.

None of this code has been tested, but it should get you going.

Tony
  • 9,672
  • 3
  • 47
  • 75
Brandon Taylor
  • 33,823
  • 15
  • 104
  • 144
  • 1
    Do you know why this solution using Form Wizard doesn't work? http://stackoverflow.com/questions/14860392/django-formwizards-how-to-painlessly-pass-user-entered-data-between-forms – Saqib Ali Feb 15 '13 at 19:14
  • I've actually never used the form wizard in Django, and I've been working with it since 0.96, but apparently the OP on that question never found the answer either. – Brandon Taylor Feb 15 '13 at 19:30
  • 1
    Haha! Sorry, I completely overlooked the username. – Brandon Taylor Feb 15 '13 at 19:41
  • Do you have any sample code how to do it? If so, I would love to see your view & form functions so that I can generally copy the methodology. How do you hold these values in the session? – Saqib Ali Feb 16 '13 at 06:23
  • Your code makes sense. Thanks a lot! I'll give it a shot. The only question I have is what is does this function "reverse" do? I have never seen it before except list.reverse(). But I don't think that's what you are doing here. – Saqib Ali Feb 18 '13 at 08:01
  • Reverse is a method by which you can look up a URL pattern by name: https://docs.djangoproject.com/en/1.4/topics/http/urls/#reverse – Brandon Taylor Feb 18 '13 at 12:48
  • I'm sorry I'm so slow on the uptake, This line: form = PersonForm(request.POST or None, data=data) produces this error: __init__() got multiple values for keyword argument 'data' Any suggestions how to fix? – Saqib Ali Feb 19 '13 at 04:58
  • using inspect.getargspec() I was able to understand what parameters PersonForm.__init__() was expecting. I found that the first listed argument it expects is "data", so giving another argument explicitly named data confuses it. Brandon, I edited the solution you posted. I think that's what you meant. – Saqib Ali Feb 19 '13 at 08:46
  • Hi everyone. My fault, I intended `data` to be `initial`. I've edited the answer to reflect that. Thanks for catching my mistake. – Brandon Taylor Feb 19 '13 at 12:59
  • Would it be possible to show me the templates for step1 and step2 from
    to
    ? I am having trouble with moving back and forth between the forms. I'm not sure what to put in the "action" attribute or underneath the form buttons. Here is my template for step #1:
    {% csrf_token %} {{ form.as_p }}
    When I do this, no matter what I type into the FN box, form.is_valid() always remains false.
    – Saqib Ali Feb 19 '13 at 19:02
  • Hmm. It might be later this afternoon or evening, but I can work up some working sample code for you. I typically have my forms post back to the same view that loaded them, which means that the action parameter is set to ".". The view code should handle moving from action to action, not the form. Sounds like there is a different problem with validation. – Brandon Taylor Feb 19 '13 at 19:04
  • Furthermore, I'm trying to create separate namespaces in the session for the elements of each form (because their names are not unique). So instead of request.session['fn'], I'm saving request.session["step1"]["fn"]. So instead of passing in initial=initial to the PersonForm constructor, I pass in initial=initial["step1"] – Saqib Ali Feb 19 '13 at 19:05
  • To get something more accurate, please send me a Pastie, etc, of your models. – Brandon Taylor Feb 19 '13 at 19:07
  • Here are my three model classes: http://pastie.org/private/v5v2weeukiwmmydvbhclw Step 1: Create Prediction Step 2: Create PredictionReference Step 3: Create PredictionRecording Step 4: Confirm values from previous 3 steps Step 5: Congratulations! You created new Database Objects. – Saqib Ali Feb 19 '13 at 21:43
  • Haven't forgotten about you. I'm working on sample code as I type this. – Brandon Taylor Feb 20 '13 at 14:18
  • I added a link to a fully fleshed-out example project using your models in my answer. – Brandon Taylor Feb 20 '13 at 16:44
  • Wow. It looks really beautiful. Thank you. I will dig through it in great detail. What a great example for me to learn from. – Saqib Ali Feb 20 '13 at 18:35
  • Thanks, and you're welcome. I may have sent that code a little early; I see an issue with the form instantiation that isn't getting the correct values in place. Some unit tests will help figure that out. Nevertheless, the workflow in the views should give you a pretty good idea of what's going on. – Brandon Taylor Feb 20 '13 at 19:12
  • Yes, I'm noticing that once you put in values that get validated on any of the forms, you cannot change them. – Saqib Ali Feb 20 '13 at 23:34
  • Here's another line I need some explanation about since I can't find a good explanation online: form = PredictionReferenceForm(request.session.get('prediction_reference', request.POST or None)) I'm not sure, but I guess this works because PredictionReferenceForm is a model form closely tied to a Model. I assume passing in an object of type of the model to the form's constructor fills those values on the form. But what if PredictionReferenceForm is not a model form? In that case what is this line supposed to look like? The example I gave u is a bit simplified. Only the first is a modelform. – Saqib Ali Feb 20 '13 at 23:53
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/24858/discussion-between-brandon-and-user1742777) – Brandon Taylor Feb 21 '13 at 00:03
  • what happened to the fully-fledged example? I want to see it please >_ – Aymon Fournier Aug 23 '16 at 13:39
  • this is a late question but, how can I delete the session if the user navigates away from the form? – frei Sep 30 '20 at 05:48
  • @frei it depends on what session storage you're using – Brandon Taylor Sep 30 '20 at 12:10
  • @Brandon django's request.session, but I guess I could store it in a context variable instead. basically i want the session to store relevant information while on any of the steps but if they user navigates away from any of the steps in the form, the info is deleted (i don't want to get session hijacked or face security issues with important user credentials being stored unsafely on the server) – frei Sep 30 '20 at 15:01
  • This answer is so basic, yet it saved me from an endless sleepless night. Your answer is clear, elegant and simple. Many others answer online made me think I had to make some complicated things to get this going. I should have thinked about it myself, but i'm trying to modify an existing project so my brain tried to overcomplicate things (And I need a break). Anyone trying to do this, give this answer a shot, you've got your answer here. – T.Nel Jun 25 '21 at 22:35
24

You can easily do this with the form wizard of django-formtools. A simple example would be something like the following.

forms.py

from django import forms

class ContactForm1(forms.Form):
    subject = forms.CharField(max_length=100)
    sender = forms.EmailField()

class ContactForm2(forms.Form):
    message = forms.CharField(widget=forms.Textarea)

views.py

from django.shortcuts import redirect
from formtools.wizard.views import SessionWizardView

class ContactWizard(SessionWizardView):
    def done(self, form_list, **kwargs):
        do_something_with_the_form_data(form_list)
        return redirect('/page-to-redirect-to-when-done/')

urls.py

from django.conf.urls import url

from forms import ContactForm1, ContactForm2
from views import ContactWizard

urlpatterns = [
    url(r'^contact/$', ContactWizard.as_view([ContactForm1, ContactForm2])),
]
Jonathan Potter
  • 2,981
  • 1
  • 24
  • 19