Josh Crompton

Dynamic form generation with class based generic views

Recently I had a situation where I wanted to use a form to gather some input, and use that input to generate a second form. Neither form needed to interact with the local database, since they were just talking to a 3rd party API behind the scenes. This is one of those situations that is not a primary use-case for Django (read as: near impossible to find an example of how to do it.).

Initially it looked like a good problem to solve with a FormWizard (which got re-written recently to take advantage of generic class-based views.) But the FormWizard code is still lacking documentation and it's pretty difficult to work with, even after consulting the source. I spent a significant chunk of time trying to get a solution working with a FormWizard (including a bunch of time trying to figure out how to unit test FormWizards), before I went back to the drawing board and got this alternate solution working in under an hour. The only real advantage to using a FormWizard that I can see is that the whole sequence of forms lives under a single URL.

The solution

What I wound up with is roughly the same as Jacob Kaplan-Moss's solution for dynamic form generation, but using generic class-based views rather than functional ones. (I haven't included a urls file here to help keep this brief.)

First, our SearchForm displays a first name, last name and email field for the user to fill out. When they submit the form, the SearchFormView adds the cleaned data to the session. (That's also where you'd place any custom validations you want to do.) Finally we redirect them to the SearchResultsForm.

When the redirect happens, the SearchResultsView gets the data that we just added to the session and passes it into the SearchResultsForm's __init__ method. We take the data and use it to get the values for the ChoiceField. I've left it at that for the sake of clarity, but you could chain as many of these together as you needed.

forms.py:

class SearchForm(forms.Form):
    """
    Search for users.
    """
    # Search fields.
    first_name = forms.CharField()
    last_name = forms.CharField()
    email = forms.CharField()


class SearchResultsForm(forms.Form):
    """
    Display a list of matching users as (in this case) radio buttons.
    """
    users = forms.ChoiceField(label='Search results', choices=(),
                              widget=forms.RadioSelect())

    def __init__(self, search_terms, *args, **kwargs):
        super(SearchResultsForm, self).__init__(*args, **kwargs)
        # Get the matching users and add them as choices to a radio button input.
        choices = api.fetch_users(first_name=first_name, last_name=last_name,
                                  email=email)
        self.fields['users'].choices = choices

views.py:

class SearchView(FormView):
    template_name = 'search.html'
    form_class = SearchForm

    def get_success_url(self):
        return reverse('search_results')

    def form_valid(self, form):
        # Add the inputs from the form to the session data.
        self.request.session['search_terms'] = {
            'first_name': form.cleaned_data.get('first_name'),
            'last_name': form.cleaned_data.get('last_name'),
            'email': form.cleaned_data.get('email'),
        }
        return HttpResponseRedirect(self.get_success_url())

class SearchResultsView(FormView):
    template_name = 'search_results.html'
    form_class = SearchResultsForm

    def get_success_url(self):
        reverse('search_complete')

    def get_form(self, form_class):
        search_terms = self.request.session.get('search_terms')
        return form_class(search_terms=search_terms,
                          **self.get_form_kwargs())