jazzband / django-formtools

A set of high-level abstractions for Django forms
https://django-formtools.readthedocs.io
BSD 3-Clause "New" or "Revised" License
815 stars 136 forks source link

Allow dynamic form classes with WizardView #16

Open gchp opened 10 years ago

gchp commented 10 years ago

This issue was originally submitted on Trac (#21667). I've added some snippets from the original ticket below, see the link above for the full conversation.


The WizardView does not currently support dynamic form classes without overriding the entire get_form method.

My mixin below demonstrates an easy way to make this convenient. I needed this functionality to support using modelform_factory at runtime to accommodate logic that varies depending on choices made previously in the wizard. A simple use case is to support dynamic "required" fields.

class WizardDynamicFormClassMixin(object):
    def get_form_class(self, step):
        return self.form_list[step]

    def get_form(self, step=None, data=None, files=None):
        """
        This method was copied from the base Django 1.6 wizard class in order to
        support a callable `get_form_class` method which allows dynamic modelforms.

        Constructs the form for a given `step`. If no `step` is defined, the
        current step will be determined automatically.

        The form will be initialized using the `data` argument to prefill the
        new form. If needed, instance or queryset (for `ModelForm` or
        `ModelFormSet`) will be added too.
        """
        if step is None:
            step = self.steps.current
        # prepare the kwargs for the form instance.
        kwargs = self.get_form_kwargs(step)
        kwargs.update({
            'data': data,
            'files': files,
            'prefix': self.get_form_prefix(step, self.form_list[step]),
            'initial': self.get_form_initial(step),
        })
        if issubclass(self.form_list[step], forms.ModelForm):
            # If the form is based on ModelForm, add instance if available
            # and not previously set.
            kwargs.setdefault('instance', self.get_form_instance(step))
        elif issubclass(self.form_list[step], forms.models.BaseModelFormSet):
            # If the form is based on ModelFormSet, add queryset if available
            # and not previous set.
            kwargs.setdefault('queryset', self.get_form_instance(step))
        return self.get_form_class(step)(**kwargs)

This is a simple demonstration of usage:

def get_form_class(self, step):
    if step == STEP_FOO:
        try:
            choice_foo = self.get_cleaned_data_for_step(STEP_FOO)["choice_foo"]
        except KeyError:
            pass
        else:
            # get_wizard_form_class() would return a model form with fields
            # that depend on the value of "choice"
            return ModelFoo(choice=choice_foo).get_wizard_form_class()
    return super(WizardFoo, self).get_form_class(step)
thenewguy commented 9 years ago

Should forms that are swapped out dynamically be provided in the initial wizard definition as None? This makes sense to me, but it will require some other changes. For example, https://github.com/django/django-formtools/blob/master/formtools/wizard/views.py#L178 errors out if None is passed for the form.

Something like:

WIZARD_FORMS = (
    ...
    ("foo", None),
    ...
)
wizard_view = WizardView.as_view(WIZARD_FORMS)

If we allow None as a placeholder, then we can't guarantee this check is thrown: https://github.com/django/django-formtools/blob/master/formtools/wizard/views.py#L187

But with swappable form classes it really isn't guaranteed anyways.