Skip to content

Create Data Once in a FormWizard

May 6th, 2014 - Software(1 min)

At one of the sites I’m building I need to walk users through a registration process:

  1. Select a supported tool
  2. Generate an ID
  3. Create a configuration based on the previous two steps

To do this, I chose to use a FormWizard with three steps, one for each of the above. The problem then becomes to generate the ID once per wizard iteration.

Initial Approach(es)

Various online solutions (and even in the FormWizard documentation) suggest using get_form_kwargs() or get_form_initial() to build step-relevant data coupled with the get_cleaned_data_from_step().

The get_form_initial() method is designed to provide initial data for a particular form. It’s quite useful and a sample code can look like this:

def get_form_initial(self, step):
    """
    Get the form initial data for a certain step
    :param step: the step in question
    :return: the initial (dict)
    """
    initial = self.initial_dict.get(step, {})
    if step == STEP_UID:
        initial.update({'id': 'My Identifier here'})

    return initial

A solution based on the get_form_kwargs() is similar, but the form needs to process the generated kwargs. Thus, we need to build code both in the wizard:

def get_form_kwargs(self, step=None):
    result = {}
    if step == STEP_UID:
        result.update({'id': 'My unique ID'})
        logger.debug('Provide ID: [%s]', result['id'])
    return result

and in the form:

class IDForm(forms.Form):
    id = forms.CharField(max_length=50, required=False, label='')

    def __init__(self, *args, **kwargs):
        # vvvv--- HERE
        initial_id = kwargs.pop('id', None)

        super(IDForm, self).__init__(*args, **kwargs)

        # vvvv--- AND HERE
        self.fields['id'].initial = initial_id
        self.fields['id'].widget.attrs['readonly'] = True

Both solutions can be used to provide initial data to a form. However, if you use get_form_initial() with get_cleaned_data_for_step(), you can end up with recursion issues as the latter will call the former with the given step parameter.

In my case, the STEP_UID step would generate a new UID which the wizard would use to build a configuration. Given the above approach, the code generating the UID would get called every time the validation for the ID form would be called. This would result in extra UIDs being created, although the FormWizard will persist your correct value.

My Solution

In order to avoid proliferation of IDs, I need a way to persist the information around. I’ve tried the context variable, but access to it is difficult from within e.g. the get_form_initial() method (read: I wasn’t able to find a way).

The next best thing is to do a session-based binding. The instance_dict object field is bound to the instance and I can use it to store my ID for future use. The new, adjusted function is:

def get_form_initial(self, step):
    initial = self.initial_dict.get(step, {})
    if step == STEP_UID:
        if 'id' not in self.instance_dict.keys():
            uid = UID.objects.create()
            self.instance_dict['id'] = uid.key

            logger.debug('Create new ID [%s] for session [%s]',
                          self.instance_dict['id'],
                          self.request.session.session_key)

        initial.update({'id': self.instance_dict['id']})
    return initial

This way, we generate the ID only if it’s not in the instance_dict.

Note: We need to clean up the dictionary at the end:

def done(self, form_list, **kwargs):
    # Clear the id from the instance dict so we allow
    # generation of a new one
    self.instance_dict.clear()

    return HttpResponseRedirect("/ok/")

HTH,

Share on
Reddit
Linked in
Whatsapp

A little experiment: