Ticket #9200: wizard_patch.txt

File wizard_patch.txt, 16.7 KB (added by David Durham, 16 years ago)
Line 
1Index: /home/david/work/django/django-trunk/django/contrib/formtools/wizard.py
2===================================================================
3--- /home/david/work/django/django-trunk/django/contrib/formtools/wizard.py (revision 9084)
4+++ /home/david/work/django/django-trunk/django/contrib/formtools/wizard.py (working copy)
5@@ -1,21 +1,22 @@
6-"""
7-FormWizard class -- implements a multi-page form, validating between each
8-step and storing the form's state as HTML hidden fields so that no state is
9-stored on the server side.
10-"""
11-
12 import cPickle as pickle
13
14 from django import forms
15 from django.conf import settings
16 from django.http import Http404
17+from django.http import HttpResponseRedirect
18 from django.shortcuts import render_to_response
19 from django.template.context import RequestContext
20 from django.utils.hashcompat import md5_constructor
21 from django.utils.translation import ugettext_lazy as _
22 from django.contrib.formtools.utils import security_hash
23
24+
25 class FormWizard(object):
26+ """
27+ FormWizard class -- implements a multi-page form, validating between each
28+ step and storing the form's state as HTML hidden fields so that no state is
29+ stored on the server side.
30+ """
31 # Dictionary of extra template context variables.
32 extra_context = {}
33
34@@ -239,3 +240,399 @@
35 data.
36 """
37 raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
38+
39+
40+class SessionWizard(object):
41+ """
42+ SessionWizard class -- implements multi-page forms with the following
43+ characteristics:
44+
45+ 1) easily supports navigation to arbitrary pages in the wizard
46+ 2) uses GETs to display forms (caveat validation errors) and POSTs for
47+ form submissions
48+
49+ Pros are support for back-button and arbitrary navigation within pages
50+ (including the oddity of someone clicking on the refresh button)
51+
52+ The major Con is use of the session scope. In particular, zero
53+ consideration has been given to multipart form data.
54+ """
55+
56+ # keys used to store wizard data in sessions
57+ __form_list_key = 'form_list'
58+ __cleaned_data_key = 'cleaned_data'
59+ __POST_data_key = 'POST_data'
60+ __page_infos_key = 'page_infos'
61+
62+ def __init__(self, form_list):
63+ """form_list should be a list of Form classes (not instances)."""
64+ self.base_form_list = form_list[:]
65+
66+ def _init_form_list(self, request):
67+ """
68+ Copy self.base_form_list to the session scope so that subclasses can
69+ manipulate the form_list for individual users.
70+ """
71+ session_key = self.get_wizard_data_key(request)
72+ if session_key not in request.session:
73+ request.session[session_key] = {
74+ self.__form_list_key : self.base_form_list[:],
75+ self.__cleaned_data_key : [],
76+ self.__POST_data_key : [],
77+ self.__page_infos_key : [],
78+ }
79+
80+ def __call__(self, request, *args, **kwargs):
81+ """
82+ Initialize the form_list for a session if needed and call GET or
83+ POST depending on the http method.
84+ """
85+ self._init_form_list(request)
86+ page0 = int(kwargs['page0'])
87+
88+ if request.method == 'POST':
89+ return self.POST(request)
90+ else:
91+ return self.GET(request, page0)
92+
93+
94+ def GET(self, request, page0):
95+ """
96+ Display the form/page for the page identified by page0
97+ """
98+ page_data = self._get_cleaned_data(request.session, page0)
99+ if page_data is None:
100+ form = self._get_form_list(request.session)[page0]()
101+ else:
102+ form_class = self._get_form_list(request.session)[page0]
103+ if issubclass(form_class, forms.ModelForm):
104+ form = form_class(instance=form_class.Meta.model(**page_data))
105+ else:
106+ form = form_class(initial=page_data)
107+ return self._show_form(request, page0, form)
108+
109+ def POST(self, request):
110+ """
111+ Validate form submission, and redirect to GET the next form or return
112+ the response from self.done().
113+ """
114+ page0 = int(request.POST['page0'])
115+ url_base = self.get_URL_base(request, page0)
116+ self._set_POST_data(request.session, request.POST, page0)
117+ form_list = self._get_form_list(request.session)
118+ form = form_list[page0](request.POST)
119+ new_page0 = self.preprocess_submit_form(request, page0, form)
120+
121+ if new_page0 is not None:
122+ return HttpResponseRedirect(url_base + str(new_page0))
123+ else:
124+ if form.is_valid():
125+ self._set_cleaned_data(request.session, page0,
126+ form.cleaned_data)
127+ self._set_page_info(request.session, page0, True)
128+ is_done = self.process_submit_form(request, page0, form)
129+ if (is_done is None or is_done == False) and \
130+ len(form_list) > page0 + 1:
131+ return HttpResponseRedirect(url_base + str(page0 + 1))
132+ else:
133+ first_broken_page, form = \
134+ self._validate_all_forms(request.session)
135+ if first_broken_page is not None:
136+ return self._show_form(request, first_broken_page,
137+ form)
138+ else:
139+ return self.done(request)
140+ else:
141+ self._set_page_info(request.session, page0, False)
142+
143+ return self._show_form(request, page0, form)
144+
145+
146+ # form util methods #
147+ def _validate_all_forms(self, session):
148+ """
149+ Iterate through the session form list and validate based on the POST
150+ data stored in the session for this wizard. Return the page index and
151+ the form of the first invalid form or None, None if all forms are valid.
152+ """
153+ i = 0
154+ for form_class in self._get_form_list(session):
155+ form = form_class(self._get_POST_data(session, i))
156+ if not form.is_valid():
157+ return i, form
158+ else:
159+ i = i + 1
160+ return None, None
161+
162+ def _show_form(self, request, page0, form):
163+ """
164+ Show the form associated with indicated page index.
165+ """
166+ url_base = self.get_URL_base(request, page0)
167+ extra_context = self.process_show_form(request, page0, form)
168+ self._set_current_page(request.session, page0)
169+ page_infos = self._get_page_infos(request.session)
170+ return render_to_response(self.get_template(page0),
171+ {'page0' : page0,
172+ 'page' : page0 + 1,
173+ 'form' : form,
174+ 'page_infos' : page_infos,
175+ 'url_base' : url_base,
176+ 'extra_context' : extra_context
177+ }, RequestContext(request))
178+
179+ def _get_form_list(self, session):
180+ """
181+ Return the list of form classes stored in the provided session.
182+ """
183+ return session[self.get_wizard_data_key(session)][self.__form_list_key]
184+
185+ def _insert_form(self, session, page0, form_class):
186+ """
187+ Insert a form class into the provided session's form list at index
188+ page0.
189+ """
190+ form_list = self._get_form_list(session)
191+ form_list.insert(page0, form_class)
192+ self._insert_wizard_data(session, self.__form_list_key, form_list)
193+
194+ def _remove_form(self, session, page0):
195+ """
196+ Remove the form at index page0 from the provided sessions form list.
197+ """
198+ self._del_wizard_data(session, self.__form_list_key, page0)
199+ # end form util methods #
200+
201+
202+ # Form data methods #
203+ def _get_POST_data(self, session, page0):
204+ """
205+ Return the POST data for a given page index page0, stored in the
206+ provided session.
207+ """
208+ post_data = self._get_all_POST_data(session)
209+ if len(post_data) > page0:
210+ return post_data[page0]
211+ else:
212+ return {}
213+
214+ def _set_POST_data(self, session, data, page0, force_insert=False):
215+ """
216+ Set the POST data for a given page index and session to the 'data'
217+ provided. If force_insert is True then the data assignment is forced
218+ as an list.insert(page0, data) call.
219+ """
220+ post_data = self._get_all_POST_data(session)
221+ if force_insert or len(post_data) <= page0:
222+ post_data.insert(page0, data)
223+ else:
224+ post_data[page0] = data
225+ self._insert_wizard_data(session, self.__POST_data_key, post_data)
226+
227+ def _remove_POST_data(self, session, page0):
228+ """
229+ Remove the POST data stored in the session at index page0.
230+ """
231+ self._del_wizard_data(session, self.__POST_data_key, page0)
232+
233+ def _get_all_POST_data(self, session):
234+ """
235+ Return the list of all POST data for this wizard from the provided
236+ session.
237+ """
238+ return session[self.get_wizard_data_key(session)][self.__POST_data_key]
239+
240+ def _get_cleaned_data(self, session, page0):
241+ """
242+ Return all cleaned data for this wizard from the provided session.
243+ """
244+ cleaned_data = self._get_all_cleaned_data(session)
245+ if len(cleaned_data) > page0:
246+ return cleaned_data[page0]
247+ else:
248+ return {}
249+
250+ def _set_cleaned_data(self, session, page0, data, force_insert=False):
251+ """
252+ Assign the cleaned data for this wizard in the session at index page0,
253+ optionally forcing a call a list insert call based on the
254+ 'force_insert' argument.
255+ """
256+ cleaned_data = self._get_all_cleaned_data(session)
257+ if force_insert or len(cleaned_data) <= page0:
258+ cleaned_data.insert(page0, data)
259+ else:
260+ cleaned_data[page0] = data
261+ self._insert_wizard_data(session, self.__cleaned_data_key, cleaned_data)
262+
263+
264+ def _get_all_cleaned_data(self, session):
265+ """
266+ Return a list of all the cleaned data in the session for this wizard.
267+ """
268+ wizard_data = session[self.get_wizard_data_key(session)]
269+ return wizard_data[self.__cleaned_data_key]
270+
271+ def _remove_cleaned_data(self, session, page0):
272+ """
273+ Remove the cleaned data at index page0 for this wizard from the
274+ provided session.
275+ """
276+ self._del_wizard_data(session, self.__cleaned_data_key, page0)
277+ # end Form data methods #
278+
279+
280+ # page methods #
281+ def _set_current_page(self, session, page0):
282+ """
283+ Iterate through the page info dicts in the session and set
284+ 'current_page' to True for the page_info corresponding to page0 and
285+ False for all others.
286+ """
287+ page_infos = self._get_page_infos(session)
288+ for i in range(len(page_infos)):
289+ if i == page0:
290+ page_infos[i]['current_page'] = True
291+ else:
292+ page_infos[i]['current_page'] = False
293+
294+ def _get_page_infos(self, session):
295+ """
296+ Return the list of page info dicts stored in the provided session for
297+ this wizard.
298+ """
299+ return session[self.get_wizard_data_key(session)][self.__page_infos_key]
300+
301+ def _remove_page(self, session, page0):
302+ """
303+ Remove the page for this wizard indicated by the page0 argument from
304+ the provided session.
305+ """
306+ self._remove_form(session, page0)
307+ self._remove_page_info(session, page0)
308+ self._remove_cleaned_data(session, page0)
309+ self._remove_POST_data(session, page0)
310+
311+ def _remove_page_info(self, session, page0):
312+ """
313+ Remove the page info dict for this wizard stored at the page0 index
314+ from the provided session.
315+ """
316+ self._del_wizard_data(session, self.__page_infos_key, page0)
317+
318+ def _insert_page(self, session, page0, form_class):
319+ """
320+ Insert a page into this wizard, storing required session structures.
321+ """
322+ self._insert_form(session, page0, form_class)
323+ self._set_page_info(session, page0, False, True)
324+ self._set_cleaned_data(session, page0, {}, True)
325+ self._set_POST_data(session, {}, page0, True)
326+
327+ def _set_page_info(self, session, page0, valid, force_insert=False):
328+ """
329+ Set the page info in this wizard for a page at index page0 and stored
330+ in the provided session.
331+ """
332+ page_info = {
333+ 'valid' : valid,
334+ 'title' : self.get_page_title(session, page0)
335+ }
336+ page_infos = self._get_page_infos(session)
337+ if force_insert or len(page_infos) <= page0:
338+ page_infos.insert(page0, page_info)
339+ else:
340+ page_infos[page0] = page_info
341+ self._insert_wizard_data(session, self.__page_infos_key, page_infos)
342+ # end page methods #
343+
344+ # start wizard data utils #
345+ def _clear_wizard_data_from_session(self, session):
346+ """
347+ Clear the session data used by this wizard from the provided session.
348+ """
349+ del session[self.get_wizard_data_key(session)]
350+
351+ def _insert_wizard_data(self, session, key, data):
352+ """
353+ Inserts wizard data into the provided session at the provided key.
354+ """
355+ wizard_data = session[self.get_wizard_data_key(session)]
356+ wizard_data[key] = data
357+ session[self.get_wizard_data_key(session)] = wizard_data
358+
359+ def _del_wizard_data(self, session, key, page0):
360+ """
361+ Deletes wizard data from the provided session at the key and page0
362+ index.
363+ """
364+ wizard_data = session[self.get_wizard_data_key(session)]
365+ sub_set = wizard_data[key]
366+ if len(sub_set) > page0:
367+ del sub_set[page0]
368+ wizard_data[key] = sub_set
369+ session[self.get_wizard_data_key(session)] = wizard_data
370+
371+ # end wizard data utils #
372+
373+ # typically overriden methods #
374+ def get_wizard_data_key(self, session):
375+ """
376+ Return a session key for this wizard. The provided session could be
377+ used to prevent overlapping keys in the case that someone needs
378+ multiple instances of this wizard at one time.
379+ """
380+ return 'session_wizard_data'
381+
382+ def get_URL_base(self, request, page0):
383+ """
384+ Return the URL to this wizard minus the "page0" parto of the URL. This
385+ value is passed to template as url_base.
386+ """
387+ return request.path.replace("/" + str(page0), "/")
388+
389+ def get_page_title(self, session, page0):
390+ """
391+ Return a user friendly title for the page at index page0.
392+ """
393+ return 'Page %s' % str(page0 + 1)
394+
395+ def process_show_form(self, request, page0, form):
396+ """
397+ Called before rendering a form either from a GET or when a form submit
398+ is invalid.
399+ """
400+
401+ def preprocess_submit_form(self, request, page0, form):
402+ """
403+ Called when a form is POSTed, but before form is validated. If this
404+ function returns None then form submission continues, else it should
405+ return a new page index that will be redirected to as a GET.
406+ """
407+
408+ def process_submit_form(self, request, page0, form):
409+ """
410+ Called when a form is POSTed. This is only called if the form data is
411+ valid. If this method returns True, the done() method is called,
412+ otherwise the wizard continues. Note that it is possible that this
413+ method would not return True, and done() would still be called because
414+ there are no more forms left in the form_list.
415+ """
416+
417+ def get_template(self, page0):
418+ """
419+ Hook for specifying the name of the template to use for a given page.
420+ Note that this can return a tuple of template names if you'd like to
421+ use the template system's select_template() hook.
422+ """
423+ return 'forms/session_wizard.html'
424+
425+ def done(self, request):
426+ """
427+ Hook for doing something with the validated data. This is responsible
428+ for the final processing including clearing the session scope of items
429+ created by this wizard.
430+ """
431+ raise NotImplementedError("Your %s class has not defined a done() " + \
432+ "method, which is required." \
433+ % self.__class__.__name__)
Back to Top