Not exactly, not exactly tim the enchanter

By : Javed Khan

I've been putting off writing about my experience with django form wizard for some time now. I came across this blog post by Kenneth Love which finally compelled me to write this down.

About Django Form Wizard:

Form Wizard can be handy when you have a huge form, and you want to make it less intimidating to the user by splitting it into multiple steps.

If you have glued multiple forms together with sessions or whatnot and it turned out to be a mess, you now what I'm talking about :)

Think surveys, checkouts, lengthy registrations etc.

The class WizardView implements most of functionality while subclasses SessionWizardView, CookieWizardView etc. specify which backend to use.

Typically, the view takes a list of forms and presents them one by one. After each step, the form is validated and the raw form data is saved in the backend (session or cookie for POST data and file_storage for files). After the last step, each form is filled with raw data from the backend and validated again.

The list of fully-filled up form instances is then passed to the done handler where the forms can be processed.

I guess the key bit of info here is: Nothing is written to the database until all the steps are completed and the forms are saved manually.

I know that feel:

I'm not a fan of putting more stuff than necessary in urls either, and I think this can be easily remedied the way Kenneth wants it.

For example, to specify forms_list and url_name in the views instead of urls one could do:

from holygrail.forms import NameForm, QuestForm, ColorForm

class MerlinWizard(NamedUrlSessionWizardView):

    ...
    ...

    @classonlymethod
    def as_view(cls, *args, **kwargs):
        kwargs.update({
            'form_list': [
                NameForm,
                QuestForm,
                ColorForm,
            ],
            'url_name': 'merlin_wizard'
        })
        return super(MerlinWizard, cls).as_view(*args, **kwargs)

    ...
    ...

and voilà, you no longer need to pass any args to MerlinWizard

ModelForms/Formsets:

I think modelforms/formsets are perfectly fine for usage in the form wizard, although, you'll need to dive into a few details:

Save a modelform:

We can use the process_step method here. This methods takes the form instance and returns the data to be saved in the storage for the current step. By default, it returns the raw form data.

def process_step(self, form):
    """
    you can save the form here,
    since it's guaranteed to be valid
    """
    if self.steps.current == '2':
        instance = form.save()
    return super(MerlinWizard, self).process_step(form)

If you need to pass around the instance to another step, you can add it to the step's storage data e.g:

Let's say we need to save a model in step 2 and create a formset based on its instance in step 3:

def process_step(self, form):
    """
    you can save the current form here,
    since it's guaranteed to be valid
    """
    step_data = super(MerlinWizard, self).process_step(form).copy()
    if self.steps.current == '2':
        instance = form.save()
        step_data['instance_pk'] = instance.pk
    return step_data

then you can retrieve the instance_pk like this:

def get_form_kwargs(self, step=None):
    """
    in step 3, use the instance from step 2
    as a kwarg to the formset
    """
    kwargs = super(MerlinWizard, self).get_form_kwargs(step=step)
    if step == '3':
        step_data = self.storage.get_step_data('2')
        instance_pk = step_data['instance_pk']
        instance = MyModel.objects.get(pk=instance_pk)
        kwargs.update({
            'instance': instance
        })
    return kwargs

Defaults:

I agree with Kenneth that storages should just work™ like they work with forms/modelforms.

But, it's handy when you want to keep the wizard media away for the rest of the user media.

One workaround is to just set the file storage to the default storage engine:

from django.core.files.storage import default_storage

...
...

class MerlinWizard(NamedUrlSessionWizardView):

    file_storage = default_storage
    ...
    ...

Are we done, yet?:

You can handle all the forms you want to save to the database in done method.

AFAIK, there's no need to do:

del self.request.session["wizard_key_here"]

The wizard cleans up both when it's started and when it's done. Also, relying on request.session breaks the storage abstraction, so you're better off using self.storage if your really want to mess with the storage. This way, you can switch to a different wizard backend without worrying too much.

Conclusion:

Having spent a good deal of time with the form wizard over the past few weeks, I feel that form wizard does a very good job at handling the use case it was designed for.

Even though the documentation is not perfect, the implementation is clear, concise and flexible enough to handle a variety of scenarios.


Related Posts


Can we help you build amazing apps? Contact us today.

Topics : django forms wizard

Comments

Luke

The problem with putting stuff in urls.py can be solved much more simply - don't put stuff there. Just assign the result of the MerlinWizard.as_view() to a name in views.py:

merlin_wizard = MerlinWizard.as_view(form_list= [NameForm, QuestForm, ColorForm])

merlin_wizard is now a genuine view (a callable that takes a request and returns a response), just like if it had been defined as a function, and so belongs 100% in views.py. urls.py then refers to all views in the same way (i.e. normally as strings).

commmenttor
Javed Khan

Luke,

Yeah, that's the simplest way and works for all CBVs.

Kenneth wanted a way to define the form list inside the wizard class, so I tried this approach.

commmenttor
Robert Slotboom

Hi Javed,

Nice approach!
I think the condition_dict also should live within the class but I can’t figure out how to implement this.
Any ideas?

Cheers,
Rob

commmenttor
Javed Khan

Rob,

I guess you can just add a class attr called condition_dict.

Thanks,
Javed

commmenttor
© Agiliq, 2009-2012