views:

162

answers:

4

Hi there

For my project I need many "workflow" forms. I explain myself:

The user selects a value in the first field, validates the form and new fields appear depending on the first field value. Then, depending on the others fields, new fields can appear...

How can I implement that in a generic way ?

A: 

It sounds like you want an AJAXy type solution. Checkout the Taconite plugin for jQuery. I use this for populating pulldowns, etc. on forms. Works very nicely.

As for being "generic" ... you might have standard methods on your container classes that return lists of children and then have a template fragmen t that knows how to format that in some 'standard' way.

Peter Rowell
in fact, i would like to avoid javascript for that process. What I would like is the form to "morph" when it is submitted and really submit only on the last step of the workflow
Ghislain Leveque
+2  A: 

I think the solution you are looking for is django form wizard

Basically you define separate forms for different pages and customize the next ones based on input in previous screens, at the end, you get all form's data together.

Specifically look at the process step advanced option on the form wizard.

FormWizard.process_step()
"""
Hook for modifying the wizard's internal state, given a fully validated Form object. The Form is guaranteed to have clean, valid data.
This method should not modify any of that data. Rather, it might want to set self.extra_context or dynamically alter self.form_list, based on previously submitted forms.
Note that this method is called every time a page is rendered for all submitted steps.
The function signature:
"""

def process_step(self, request, form, step):
    # ...

If you need to only modify the dropdown values based on other dropdowns within the same form, you should have a look at the implemented dajaxproject

Lakshman Prasad
Is it possible with form wizard to keep the first forms displayed above the latter ones ?
Ghislain Leveque
You must use ajax for that. It's not straight forward.
Lakshman Prasad
I think if you customize the FormWizard internals you can probably include all of the forms in each step.
Orange Box
A: 

Ok, I've found a solution that does not use ajax at all and seems nice enough to me :

Create as many forms as needed and make them subclass each other. Put an Integer Hidden Field into the first one :

class Form1(forms.Form):
    _nextstep = forms.IntegerField(initial = 0, widget = forms.HiddenInput())
    foo11 = forms.IntegerField(label = u'First field of the first form')
    foo12 = forms.IntegerField(label = u'Second field of the first form')

class Form2(Form1):
    foo21 = forms.CharField(label = u'First field of the second form')

class Form3(Form2):
    foo31 = forms.ChoiceField([],
        label=u'A choice field which choices will be completed\
            depending on the previous forms')
    foo32 = forms.IntegerField(label = u'A last one')

    # You can alter your fields depending on the data.
    # Example follows for the foo31 choice field
    def __init__(self, *args, **kwargs):
        if self.data and self.data.has_key('foo12'):
            self.fields['foo31'].choices = ['make','a','nice','list',
                'and you can','use your models']

Ok, that was for the forms now here is the view :

def myview(request):
    errors = []
    # define the forms used :
    steps = [Form1,Form2,Form3]
    if request.method != 'POST':
        # The first call will use the first form :
        form = steps[0]()
    else:
        step = 0
        if request.POST.has_key('_nextstep'):
            step = int(request.POST['_nextstep'])
        # Fetch the form class corresponding to this step
        # and instantiate the form
        klass = steps[step]
        form = klass(request.POST)
        if form.is_valid():
            # If the form is valid, increment the step
            # and use the new class to create the form
            # that will be displayed
            data = form.cleaned_data
            data['_nextstep'] = min(step + 1, len(steps) - 1)
            klass = steps[data['_nextstep']]
            form = klass(data)
        else:
            errors.append(form.errors)
    return render_to_response(
        'template.html',
        {'form':form,'errors':errors},
        context_instance = RequestContext(request))

The only problem I saw is that if you use {{form}} in your template, it calls form.errors and so automagically validates the new form (Form2 for example) with the data of the previous one (Form1). So what I do is iterate over the items in the form and only use {{item.id}}, {{item.label}} and {{item}}. As I've already fetched the errors of the previous form in the view and passed this to the template, I add a div to display them on top of the page.

Ghislain Leveque
A: 

I think it depends on the scale of the problem.

You could write some generic JavaScript that shows and hides the form fields (then in the form itself you apply these css classes). This would work well for a relatively small number showing and hiding fields.

If you want to go further than that you will need to think about developing dynamic forms in Django. I would suggest you don't modify the ['field'] in the class like Ghislain suggested. There is a good post here about dynamic forms and it shows you a few approaches.

I would imagine that a good solution might be combining the dynamic forms in the post above with the django FormWizard. The FormWizard will take you through various different Forms and then allow you to save the overall data at the end.

It had a few gotchas though as you can't easily go back a step without loosing the data of the step your on. Also displaying all the forms will require a bit of a customization of the FormWizard. Some of the API isn't documented or considered public (so be wary of it changing in future versions of Django) but if you look at the source you can extend and override parts of the form wizard fairly easily to do what you need.

Finally a simpler FormWizard approach would be to have say 5 static forms and then customize the form selection in the wizard and change what forms are next and only show the relevant forms. This again would work well but it depends how much the forms change on previous choices.

Hope that helps, ask any questions if have any!

Orange Box
You can see ma solution in this post but I have a question : why do you suggest not to modify the self.fields['field_name'] ??
Ghislain Leveque
It doesn't scale up well - if using type() to create forms dynamically is a much tidier and pythonic solution. Modifying self.fields is a bit of a hack and you end up making a mess of the __init__ and it requires changing the API for using the form. I'm actually writing a blog about this and will add the link here when I'm done as it explains in more. It comes down to personal choice in the end I suppose.
Orange Box