Ticket #9200: wizard_patch.txt

File wizard_patch.txt, 16.7 KB (added by David Durham, 15 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