Opened 3 years ago

Closed 2 years ago

#17850 closed New feature (wontfix)

Passing FormWizard.as_view(form_list) a dynamic form_list

Reported by: Jeffrey Horn <jrhorn424@…> Owned by: nobody
Component: contrib.formtools Version: 1.4-beta-1
Severity: Normal Keywords: formwizard
Cc: kristoffer.snabb@… Triage Stage: Design decision needed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The following technique fails to provide the expected behavior. The error returned is:

TypeError: issubclass() arg 1 must be a class
# happens in
/django/contrib/formtools/wizard/views.py", line 166, in get_initkwargs

urls.py:

from django.conf.urls import patterns, url

# from views import survey_wizard
from views import SurveyWizard
from forms import get_survey_page_list

# I'd rather do this dynamically as well, but this *should* work
survey_page_list = get_survey_page_list('274a442b-67ec-11e1-b010-78ca39b1b597')

urlpatterns = patterns('',
    url(r'^(?P<survey>[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})$', SurveyWizard.as_view(form_list=survey_page_list)),
)

forms.py:

def get_survey_page_list(survey):
    """
    Build a list of survey pages for the form wizard to use.

    args:
        survey - a unicode string passed in from a URL parameter
    """
    survey_page_list = []

    # Since we're receiving a captured URL parameter, we have to use that
    # string to acutally fetch the survey object.
    s = Survey.objects.get(pk=survey)
    for page in s.page_set.all().order_by('number'):
        step_name = unicode(page.number)
        form_class = make_survey_page_form(survey, page.number)
        survey_page_list.append((step_name, form_class))

    return SortedDict(survey_page_list)

Looking into the Werkzeug debugger reveals that the functions called in forms.py are grabbing and returning a SortedDict of forms:

>>> dir()
['args', 'cls', 'condition_dict', 'form', 'form_list', 'i', 'init_form_list', 'initial_dict',   ]
>>> form_list
django.utils.datastructures.SortedDict({u'1': <class 'mysite.survey.forms._SurveyPageForm'>, u'2': <class 'mysite.survey.forms._SurveyPageForm'>, u'3': <class 'mysite.survey.forms._SurveyPageForm'>})
>>> initial_dict
>>> init_form_list
django.utils.datastructures.SortedDict({u'0': u'1', u'1': u'2', u'2': u'3'})

Change History (8)

comment:1 follow-up: Changed 3 years ago by jrhorn424

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset

Using a slightly different technique yields a different error.

TypeError: get_initkwargs() takes at least 2 arguments (1 given)
# in 
"/django/django/contrib/formtools/wizard/views.py", line 118, in as_view

#urls.py

from django.conf.urls import patterns, url

from views import survey_wizard

urlpatterns = patterns('',
    url(r'^(?P<survey>[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})$', survey_wizard),
)

views.py:

class SurveyWizard(CookieWizardView):

    def get_form_list(self):
        form_list = get_survey_page_list(self.kwargs['survey'])
        return form_list

    ### This fails, too ###
    # @property
    # def form_list(self):
    #     form_list = get_survey_page_list(self.kwargs['survey'])
    #     return form_list

survey_wizard = SurveyWizard.as_view()

I suspect this is because SurveyWizard.as_view() is expecting an argument to pass along to get_initkwargs. Werkzeug:

>>> dir()
['args', 'cls', 'kwargs']
>>> args
()
>>> kwargs
{}
>>> cls
<class 'mysite.survey.views.SurveyWizard'>

However, I'm at a loss as to how else to get a dynamically generated list of forms into form_list.

comment:2 Changed 3 years ago by kristoffer.snabb@…

The problem is that the WizardView takes a form_list of classes and not object instances. I cannot say if this is a bug or a design choice, maybe a performance issue? Anyway I would also appreciate a possibility to create forms more dynamically.

Taken from WizardView:

if issubclass(form, formsets.BaseFormSet):
    # if the element is based on BaseFormSet (FormSet/ModelFormSet)
    # we need to override the form variable.
    form = form.form

comment:3 Changed 3 years ago by ksnabb

  • Cc kristoffer.snabb@… added

comment:4 Changed 3 years ago by jrhorn424

Yes, no matter what I did, WizardView was always expecting some kind of list. :)

I'm also not sure if this a bug, but I think dynamic forms is a feature worth having.

Here's what I went with, but the form wizard is still acting weird, not giving me a next button when there are multiple steps:

class SurveyWizard(CookieWizardView):

    def dispatch(self, request, *args, **kwargs):
        survey_page_list = get_survey_page_list(kwargs['survey'])
        self.form_list = self.get_initkwargs(survey_page_list)['form_list']
        return super(SurveyWizard, self).dispatch(request, *args, **kwargs)

    def done(self, form_list, **kwargs):
        # do something

Also, I passed SurveyWizard.as_view() a list filled with a bogus form to avoid the wrong number of arguments error. The form list is overridden by dispatch above, anyway.

Thanks to SmileyChris on #django for walking me through the source for a few hours to figure this out.

comment:5 Changed 3 years ago by jezdez

  • Triage Stage changed from Unreviewed to Design decision needed
  • Type changed from Bug to New feature

This isn't really a bug, to be honest. You're expecting a behavior that the form wizard wasn't written for. Marking as design decision needed.

comment:6 Changed 2 years ago by anonymous

Hi there, why don't you just create your URLConf dynamically?

comment:7 in reply to: ↑ 1 Changed 2 years ago by steph

Replying to jrhorn424:

Using a slightly different technique yields a different error.

TypeError: get_initkwargs() takes at least 2 arguments (1 given)
# in 
"/django/django/contrib/formtools/wizard/views.py", line 118, in as_view


This happens because you're currently forced to provide a form_list.

views.py:

class SurveyWizard(CookieWizardView):

    def get_form_list(self):
        form_list = get_survey_page_list(self.kwargs['survey'])
        return form_list

survey_wizard = SurveyWizard.as_view()

I suspect this is because SurveyWizard.as_view() is expecting an argument to pass along to get_initkwargs.


Yes, you have to provide a form_list, but - as a quick fix - you could provide a dummy form list with one form. You could then override the get_form_list method as you already did and it should work.

What we need is a way to tell the wizard that the empty form_list is ok. For example with an attribute in a subclass "allow_empty_form_list = True".

comment:8 Changed 2 years ago by jacob

  • Resolution set to wontfix
  • Status changed from new to closed

As jezdez says, this isn't what the wizard is designed for, sorry.

Note: See TracTickets for help on using tickets.
Back to Top