Skip to content

Commit f45851b

Browse files
committed
Fix infinite recursion error jazzband#220
This removes the undocumented `get_form_list` since the process of repeatedly and dynamically generating an OrderedDict of steps/forms pairs leads to infinite recursion. This is replaced with a new method `process_condition_dict` which directly modifies `form_list` in place, in a single step executed prior to dispatching to `get` or `post`. This eliminates infinite recursion since the `form_list` is calculated exactly once prior to any other attempts to access and process forms. Removed `test_form_condition_unstable` since this is an odd test (why would a condition_dict return value suddenly change in the middle of a request/response cycle?) that was attempting to deal with invalid steps (invalid steps are better handled through actual validation, see jazzband#224). For users who need to dynamically _add_ forms, this can be handled by overriding `process_condition_dict`.
1 parent 97441f1 commit f45851b

File tree

2 files changed

+28
-50
lines changed

2 files changed

+28
-50
lines changed

formtools/wizard/views.py

+18-26
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def __repr__(self):
4646
@property
4747
def all(self):
4848
"Returns the names of all steps/forms."
49-
return list(self._wizard.get_form_list())
49+
return list(self._wizard.form_list)
5050

5151
@property
5252
def count(self):
@@ -201,28 +201,21 @@ def get_prefix(self, request, *args, **kwargs):
201201
# TODO: Add some kind of unique id to prefix
202202
return normalize_name(self.__class__.__name__)
203203

204-
def get_form_list(self):
204+
def process_condition_dict(self):
205205
"""
206-
This method returns a form_list based on the initial form list but
207-
checks if there is a condition method/value in the condition_list.
208-
If an entry exists in the condition list, it will call/read the value
209-
and respect the result. (True means add the form, False means ignore
210-
the form)
211-
212-
The form_list is always generated on the fly because condition methods
213-
could use data from other (maybe previous forms).
206+
This method prunes `self.form_list` by checking if there is a condition method/value in `condition_list`.
207+
If an entry exists, it will call/read the value and respect the result. If the condition returns False, the
208+
form will be removed from `form_list`.
214209
"""
215-
form_list = OrderedDict()
216-
for form_key, form_class in self.form_list.items():
210+
for form_key in list(self.form_list.keys()):
217211
# try to fetch the value from condition list, by default, the form
218212
# gets passed to the new list.
219213
condition = self.condition_dict.get(form_key, True)
220214
if callable(condition):
221215
# call the value if needed, passes the current instance.
222216
condition = condition(self)
223-
if condition:
224-
form_list[form_key] = form_class
225-
return form_list
217+
if not condition:
218+
del self.form_list[form_key]
226219

227220
def dispatch(self, request, *args, **kwargs):
228221
"""
@@ -241,6 +234,7 @@ def dispatch(self, request, *args, **kwargs):
241234
getattr(self, 'file_storage', None),
242235
)
243236
self.steps = StepsHelper(self)
237+
self.process_condition_dict()
244238
response = super().dispatch(request, *args, **kwargs)
245239

246240
# update the response (e.g. adding cookies)
@@ -273,7 +267,7 @@ def post(self, *args, **kwargs):
273267
# contains a valid step name. If one was found, render the requested
274268
# form. (This makes stepping back a lot easier).
275269
wizard_goto_step = self.request.POST.get('wizard_goto_step', None)
276-
if wizard_goto_step and wizard_goto_step in self.get_form_list():
270+
if wizard_goto_step and wizard_goto_step in self.form_list:
277271
return self.render_goto_step(wizard_goto_step)
278272

279273
# Check if form was refreshed
@@ -342,7 +336,7 @@ def render_done(self, form, **kwargs):
342336
"""
343337
final_forms = OrderedDict()
344338
# walk through the form list and try to validate the data again.
345-
for form_key in self.get_form_list():
339+
for form_key in self.form_list.keys():
346340
form_obj = self.get_form(
347341
step=form_key,
348342
data=self.storage.get_step_data(form_key),
@@ -406,7 +400,7 @@ def get_form(self, step=None, data=None, files=None):
406400
"""
407401
if step is None:
408402
step = self.steps.current
409-
form_class = self.get_form_list()[step]
403+
form_class = self.form_list[step]
410404
# prepare the kwargs for the form instance.
411405
kwargs = self.get_form_kwargs(step)
412406
kwargs.update({
@@ -469,7 +463,7 @@ def get_all_cleaned_data(self):
469463
'formset-' and contain a list of the formset cleaned_data dictionaries.
470464
"""
471465
cleaned_data = {}
472-
for form_key in self.get_form_list():
466+
for form_key in self.form_list.keys():
473467
form_obj = self.get_form(
474468
step=form_key,
475469
data=self.storage.get_step_data(form_key),
@@ -510,8 +504,7 @@ def get_next_step(self, step=None):
510504
"""
511505
if step is None:
512506
step = self.steps.current
513-
form_list = self.get_form_list()
514-
keys = list(form_list.keys())
507+
keys = list(self.form_list.keys())
515508
if step not in keys:
516509
return self.steps.first
517510
key = keys.index(step) + 1
@@ -529,8 +522,7 @@ def get_prev_step(self, step=None):
529522
"""
530523
if step is None:
531524
step = self.steps.current
532-
form_list = self.get_form_list()
533-
keys = list(form_list.keys())
525+
keys = list(self.form_list.keys())
534526
if step not in keys:
535527
return None
536528
key = keys.index(step) - 1
@@ -547,7 +539,7 @@ def get_step_index(self, step=None):
547539
"""
548540
if step is None:
549541
step = self.steps.current
550-
keys = list(self.get_form_list().keys())
542+
keys = list(self.form_list.keys())
551543
if step in keys:
552544
return keys.index(step)
553545
return None
@@ -678,7 +670,7 @@ def get(self, *args, **kwargs):
678670
)
679671
return self.render(form, **kwargs)
680672

681-
elif step_url in self.get_form_list():
673+
elif step_url in self.form_list:
682674
self.storage.current_step = step_url
683675
return self.render(
684676
self.get_form(
@@ -699,7 +691,7 @@ def post(self, *args, **kwargs):
699691
is super'd from WizardView.
700692
"""
701693
wizard_goto_step = self.request.POST.get('wizard_goto_step', None)
702-
if wizard_goto_step and wizard_goto_step in self.get_form_list():
694+
if wizard_goto_step and wizard_goto_step in self.form_list:
703695
return self.render_goto_step(wizard_goto_step)
704696
return super().post(*args, **kwargs)
705697

tests/wizard/test_forms.py

+10-24
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,12 @@ def done(self, form_list, **kwargs):
9292

9393

9494
class TestWizardWithCustomGetFormList(TestWizard):
95+
form_list = [('start', Step1)]
9596

96-
form_list = [Step1]
97-
98-
def get_form_list(self):
99-
return {'start': Step1, 'step2': Step2}
97+
def process_condition_dict(self):
98+
super().process_condition_dict()
99+
# Modify the `form_list` using any criteria (e.g. whether the user is logged in, etc.) or none at all
100+
self.form_list['step2'] = Step2
100101

101102

102103
class FormTests(TestCase):
@@ -158,19 +159,6 @@ def test_form_condition(self):
158159
response, instance = testform(request)
159160
self.assertEqual(instance.get_next_step(), 'step2')
160161

161-
def test_form_condition_unstable(self):
162-
request = get_request()
163-
testform = TestWizard.as_view(
164-
[('start', Step1), ('step2', Step2), ('step3', Step3)],
165-
condition_dict={'step2': True}
166-
)
167-
response, instance = testform(request)
168-
self.assertEqual(instance.get_step_index('step2'), 1)
169-
self.assertEqual(instance.get_next_step('step2'), 'step3')
170-
instance.condition_dict['step2'] = False
171-
self.assertEqual(instance.get_step_index('step2'), None)
172-
self.assertEqual(instance.get_next_step('step2'), 'start')
173-
174162
def test_form_kwargs(self):
175163
request = get_request()
176164
testform = TestWizard.as_view([
@@ -265,23 +253,21 @@ def test_form_list_type(self):
265253
response, instance = testform(request)
266254
self.assertEqual(response.status_code, 200)
267255

268-
def test_get_form_list_default(self):
256+
def test_form_list_default(self):
269257
request = get_request()
270258
testform = TestWizard.as_view([('start', Step1)])
271259
response, instance = testform(request)
272260

273-
form_list = instance.get_form_list()
274-
self.assertEqual(form_list, {'start': Step1})
261+
self.assertEqual(instance.form_list, {'start': Step1})
275262
with self.assertRaises(KeyError):
276263
instance.get_form('step2')
277264

278-
def test_get_form_list_custom(self):
265+
def test_form_list_custom(self):
279266
request = get_request()
280-
testform = TestWizardWithCustomGetFormList.as_view([('start', Step1)])
267+
testform = TestWizardWithCustomGetFormList.as_view()
281268
response, instance = testform(request)
282269

283-
form_list = instance.get_form_list()
284-
self.assertEqual(form_list, {'start': Step1, 'step2': Step2})
270+
self.assertEqual(instance.form_list, {'start': Step1, 'step2': Step2})
285271
self.assertIsInstance(instance.get_form('step2'), Step2)
286272

287273

0 commit comments

Comments
 (0)