Very important update: DO NOT USE THE FORMWIZARD for dynamic steps as described below. Django stores the FormWizard as a Singleton so concurrent sessions produce crazy behavior on the wizard. Thanks to my friends at Galotecnia.com because they found the problem. Use the FormWizard just as described in Django documentation and always stateless.

—————————
Django FormWizard is really useful. But when it comes to variable steps (I mean, depending on the choosen answer the user gets different forms) you can’t just initialize the FormWizard with a bunch of Forms as you don’t know which ones. Using polymorphism makes it possible to save lots of code lines by letting each form decide which one is next.

Instead of define your form extending Form, use this base class:

class WizardStepForm(forms.Form):
    step_order = 0
    confirmQuestion = "Proceed?"
    
    def process_dynamic_step(self, form_list):
        raise NotImplementedError("Please implement this")
        
    def get_field_name(self):
        raise NotImplementedError("Please implement this")
    
    def get_field_name_for_summary(self):
        fieldname = self.get_field_name()
        firstUpper = fieldname.upper()[0] + fieldname[1:]
        return firstUpper + ":"
        
    def append_end(self, form_list):
        return "", form_list

Now we assume our FormWizard will work with WizardStepForms instead of regular Forms:

class MyWizard(FormWizard):                
    def get_summary(self, form_list):
        """
         To show the user the summary at the last step
        """
        summary = ""
        for i in form_list:
            if hasattr(i, "cleaned_data"):
               if isinstance(i, WizardStepForm):
                  model = i.cleaned_data[i.cleaned_data.keys()[0]]
                  append_text = i.get_field_name_for_summary()
                  append_text = append_text + " " + str(model)
                  summary = summary + append_text + "
" return summary def get_hidden_summary(self, form_list): """ Collect the information from all the steps which is in the input hidden field """ summary = "" for i in form_list: if hasattr(i, "cleaned_data"): model = i.cleaned_data[i.cleaned_data.keys()[0]] if hasattr(model, "id"): summary = summary + str(model.id) + ":" else: summary = summary + str(model) + ":" summary = summary[:-1] return summary def done(self, request, form_list): summary = self.get_summary(form_list) hidden_summary = self.get_hidden_summary(form_list) return render_to_response('confirm.html', { 'summary': summary, 'hidden_summary': hidden_summary, 'form': ConfirmationForm(), # a regular Form }, RequestContext(request)) def get_template(self, step): return 'wizard.html' def process_step(self, request, form, step): self.extra_context = {'info': ''} if hasattr(form, "cleaned_data"): info, self.form_list = form.process_dynamic_step(self.form_list) self.extra_context = {'info': info}

Now two forms for the wizard (two steps):

class SearchWhateverForm(WizardStepForm):
    step_order = 1
    
    someChoice = forms.ChoiceField(choices=WHATEVER_CHOICES)

    @classmethod
    def get_field_name(self):
        return "whatever"

    def process_dynamic_step(self, form_list):
        form_list = form_list[:self.step_order]
        form_list.append(WhatToGenerateForm)
        return "How many items do you need to generate?", form_list

class WhatToGenerateForm(WizardStepForm):
    step_order = 2
    generate = forms.ChoiceField(choices=GENERATION_CHOICES)

    @classmethod
    def get_field_name(self):
        return "generate"
    
    def process_dynamic_step(self, form_list):
        form_list = form_list[:self.step_order]
        generate = self.cleaned_data['generate']
        if generate == GENERATE_EXACT or generate == GENERATE_EXACT_90:
           form_list.append(HowManyToGenerateForm)
           return "Please set number", form_list
        else:
           return self.append_end(form_list)

File urls.py would be like this:

  url(r'^wizard/$', MyWizard([SearchWhateverForm])

Only one template is needed for all the steps:

{{ form }}
{{ previous_fields|safe }}

So, How could you test drive the wizard?

As you don’t need views or templates to run the sequence, it is easy:

def test_StepsAreOk(self):
        form_list = []
        form_list.append(SearchWhateverForm)
        # define somedata
        form_list[-1].cleaned_data = {SearchWhateverForm.get_field_name(): somedata}
        info, form_list = form_list[-1]().process_dynamic_step(form_list)
        self.assertEqual(form_list[-1].get_field_name(), WhatToGenerateForm.get_field_name())

How can I populate the choices of one form dynamically?

The forms_list of the wizard is not populated with instances but data types so you’ve got to dynamically create a form type once you know which choices will it have available. One solution is to extend from one of your forms:

class SomeWizardStepForm(WizardStepForm):
    step_order = 1
 
    #field is not defined
 
    @classmethod
    def get_field_name(self):
        return "whatever"
 
    def process_dynamic_step(self, form_list):
        form_list = form_list[:self.step_order]
        form_list.append(TheNextForm)
        return "whatever", form_list


def createDynamicForm(self, someInput):
        class SomeDynamicForm(SomeWizardStepForm):
             field = forms.ModelChoiceField(queryset=someInput.get_whatever())
        return SomeDynamicForm  # returning a datatype not an instance.

Method createDynamicForm can be inserted in MyWizard class or in any WizardStepForm, in its process_dynamic_step method, so once the datatype is returned the form_list can hold the new form.

What if I need to populate the first step form with an incoming variable?

Initialize the FormWizard with an empty list:

url(r'^wizard/(?P\d+)/$', MyWizard([]),

Add logic to MyWizard to populate and place the first form:

class MyWizard(FormWizard):
   
    someInput = None   # this is a model in my actual code
    
    def _initialize(self, id):
        self.someInput = SomeModel.objects.get(id=id)
        self.extra_context = {'someInput': self.someInput}
        self.form_list = []
        self.form_list.append(self.createDynamicForm(self.someInput))
        
    def parse_params(self, request, *args, **kwargs):
        """
        Get the parameters sent through the URL
        """
        if self.someInput is None:  # first time
           self._initialize(kwargs['someInput'])

    ....

It the code snippets are not clear enough let me know.