Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Need clarification on using Django 1.4 Form Wizards, specifically pre-filling and saving

We are building a wizard using Django 1.4's new form wizard functionality.
The docs on this are very terse and we can't find any advanced examples. We are using a named step wizard (needed to support a listview/datagrid we use) and a session backend. The wizard is meant to edit roles and linked rights and is built to provide both add and edit functionality. We do this by asking the user in the first step if he/she wants to add or edit.

The next step depends on that choice; If the user wants to edit, there is a search screen, followed by a listview/datagrid that displays results. The user can then select one of the results and goes to a details-screen, followed by a FilteredSelectMultiple page, allowing him/her to link rights to this role.

If the user wants to add a new role, the search and results screens are skipped and the user goes directly to the details screen, followed by the link-screen.
It all works pretty well, using a condition_dict in urls.py, but we are wondering a couple of things about the general functionality:

When a specific pre-existing role is selected, how can we fill the details and the link-screen with the corresponding data?

Do we instantiate a roles-object and pass it somehow to the two forms, if so, where do we instantiate it and do we need to do that for every form separately (which seems a bit over the top)?

When saving, is it common practice to create another instance of a role object, add the form data to it and save, or can we re-use the object used in the forms somehow?

We have tried overloading get_form_instance to return instances of roles, and we have looked at instance_dict in the docs, but it feels like the wrong approach and there are no examples to be found online, and we're not even sure these are used to pre-fill data or even if we're on the right track.

Logically, I would say in the step that selects an existing role, I need to fill the wizard-variables using an instance of the chosen object, and these get displayed in the forms. At the end of the wizard we reverse the process and get all data from the wizard-variables and add them to a newly instantiated roles-object and save it. Ideally this instance will determine itself if it needs to perform an INSERT or an UPDATE, depending on whether or not the promary key is filled.

If anyone can provide an example, or a nudge in the right direction, it would be very much appreciated.

The code of the wizardview class in views.py is below:

class RolesWizard(NamedUrlSessionWizardView):

def get_template_names(self):
    # get template for each step...
    if self.steps.current == 'choice':
        return 'clubassistant/wizard_neworeditrole.html'
    if self.steps.current == 'search':
        return 'clubassistant/wizard_searchrole.html'
    if self.steps.current == 'results':
        return 'clubassistant/wizard_pickrole.html'
    if self.steps.current == 'details':
        return 'clubassistant/wizard_detailsrole.html'
    elif self.steps.current == 'rights':
        return 'clubassistant/wizard_roles.html'

def get_context_data(self, form, **kwargs):
    # get context data to be passed to the respective templates
    context = super(RolesWizard, self).get_context_data(form=form, **kwargs)

    # add the listview in the results screen
    if self.steps.current == 'results':
        # get search text from previous step
        cleaned_data = self.get_cleaned_data_for_step('search')
        table = RolesTable(Roles.objects.filter(
            role_name__contains=cleaned_data['searchrole'])
        )
        RequestConfig(self.request, paginate={
            "per_page": 4,
            }).configure(table)
        # add the listview with results
        context.update({'table': table})

    # add a role instance based on the chosen primary key
    if self.steps.current == 'rights':
        cleaned_data = self.get_cleaned_data_for_step('results')
        role_id = cleaned_data['role_uuid']
        role = get_object_or_404(Roles, pk=role_id)
        context.update({'role': role})

    return context

def done(self, form_list, **kwargs):
    # this code is executed when the wizard needs to be completed

    # combine all forms into a single dictionary
    wizard = self.get_all_cleaned_data()

    if wizard.get("neworeditrole")=="add":
        role = Roles()
    else:
        role = get_object_or_404(Roles, pk=wizard.get("role_uuid"))

    # many-to-many rights/roles
    role.role_rights_new_style.clear()
    for each_right in wizard.get('role_rights_new_style'):
        RightsRoles.objects.create(role=role, right=each_right,)

    # other properties
    for field, value in self.get_cleaned_data_for_step('details'):
        setattr(role, field, value)

    role.save()

    # return to first page of wizard...
    return HttpResponseRedirect('/login/maintenance/roles/wizard/choice/')
like image 358
Erik Oosterwaal Avatar asked Oct 08 '22 04:10

Erik Oosterwaal


2 Answers

Let's see if I can help. I did a form wizard that adds steps depending on the answers. At each step I save all forms in a session variable, like so:

def process_step(self, request, form, step):
  request.session['form_list'] = self.form_list
  request.session['initial'] = self.initial

Then, each time that view is rendered, I instantiate a new form wizard with all the previous data:

def dynamic_wizard(request):
  if not request.session.get('form_list'):
        form = Wizard([Form1])
    else:
        form = Wizard(request.session.get('form_list'), initial = request.session['initial'])
    return form(context=RequestContext(request), request=request)
like image 25
daigorocub Avatar answered Oct 13 '22 12:10

daigorocub


For future googlers:

I had some success with using get_form() because it is called before a form is rendered. Start with a couple of ModelForms:

class Wizard1(models.ModelForm): 
    class Meta:
        model = MyModel
        fields = ('field0', 'model0')
class Wizard2(models.ModelForm): 
    class Meta:
        model = MyModel
        excludes = ('field0', 'model0')

Then, in your SessionWizardView:

class MyWizard(SessionWizardView):
    def get_form(self, step=None, data=None, files=None):
        form = super(ExtensionCreationWizard, self).get_form(step, data, files)

        if step is not None and data is not None:
            # get_form is called for validation by get_cleaned_data_for_step()
            return form

        if step == "0":
            # you can set initial values or tweak fields here

        elif step == "1":
            data = self.get_cleaned_data_for_step('0')
            if data is not None:
                form.fields['field1'].initial = data.get('field0')
                form.fields['field2'].widget.attrs['readonly'] = True
                form.fields['field3'].widget.attrs['disabled'] = True
                form.fields['model1'].queryset = Model1.objects.filter(name="foo")

        return form

The action is all in step 1. You request validated data from step 0 (which triggers another call to get_form() for step 0, so be careful) and then you can access any values that were set in step 0.

I threw in a couple of examples of settings you can change on the fields. You can update a queryset to limit the values in a ChoiceField, or re-display a value again but make it read-only. One caveat I noticed... readonly does not work on ChoiceField. You can make it disabled, but then the value is not propagated when you submit the form.

like image 157
Eli Burke Avatar answered Oct 13 '22 11:10

Eli Burke