—————————
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:
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.