| 1 | Index: django-trunk/django/contrib/formtools/wizard.py
|
|---|
| 2 | ===================================================================
|
|---|
| 3 | --- django-trunk/django/contrib/formtools/wizard.py (revision 9084)
|
|---|
| 4 | +++ django-trunk/django/contrib/formtools/wizard.py (working copy)
|
|---|
| 5 | @@ -1,21 +1,23 @@
|
|---|
| 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 | +from django.utils.datastructures import SortedDict
|
|---|
| 24 |
|
|---|
| 25 | +
|
|---|
| 26 | class FormWizard(object):
|
|---|
| 27 | + """
|
|---|
| 28 | + FormWizard class -- implements a multi-page form, validating between each
|
|---|
| 29 | + step and storing the form's state as HTML hidden fields so that no state is
|
|---|
| 30 | + stored on the server side.
|
|---|
| 31 | + """
|
|---|
| 32 | # Dictionary of extra template context variables.
|
|---|
| 33 | extra_context = {}
|
|---|
| 34 |
|
|---|
| 35 | @@ -239,3 +241,423 @@
|
|---|
| 36 | data.
|
|---|
| 37 | """
|
|---|
| 38 | raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
|
|---|
| 39 | +
|
|---|
| 40 | +
|
|---|
| 41 | +class SessionWizard(object):
|
|---|
| 42 | + """
|
|---|
| 43 | + SessionWizard class -- implements multi-page forms with the following
|
|---|
| 44 | + characteristics:
|
|---|
| 45 | +
|
|---|
| 46 | + 1) easily supports navigation to arbitrary pages in the wizard
|
|---|
| 47 | + 2) uses GETs to display forms (caveat validation errors) and POSTs for
|
|---|
| 48 | + form submissions
|
|---|
| 49 | +
|
|---|
| 50 | + Pros are support for back-button and arbitrary navigation within pages
|
|---|
| 51 | + (including the oddity of someone clicking on the refresh button)
|
|---|
| 52 | +
|
|---|
| 53 | + The major Con is use of the session scope. In particular, zero
|
|---|
| 54 | + consideration has been given to multipart form data.
|
|---|
| 55 | + """
|
|---|
| 56 | +
|
|---|
| 57 | + # keys used to store wizard data in sessions
|
|---|
| 58 | + __form_classes_key = 'form_classes'
|
|---|
| 59 | + __cleaned_data_key = 'cleaned_data'
|
|---|
| 60 | + __POST_data_key = 'POST_data'
|
|---|
| 61 | + __pages_key = 'pages'
|
|---|
| 62 | +
|
|---|
| 63 | + def __init__(self, forms):
|
|---|
| 64 | + """
|
|---|
| 65 | + A form_classes can be a list of form classes or a list of 2-Tuples in
|
|---|
| 66 | + the form (page_key, form_class).
|
|---|
| 67 | + """
|
|---|
| 68 | + self.base_forms = SortedDict()
|
|---|
| 69 | + if forms:
|
|---|
| 70 | + if type(forms[0]) == tuple:
|
|---|
| 71 | + self.contains_named_pages = True
|
|---|
| 72 | + for page_key, form_class in forms:
|
|---|
| 73 | + self.base_forms[page_key] = form_class
|
|---|
| 74 | + else:
|
|---|
| 75 | + i = 0
|
|---|
| 76 | + for form_class in forms:
|
|---|
| 77 | + self.base_forms[str(i)] = form_class
|
|---|
| 78 | + i = i + 1
|
|---|
| 79 | +
|
|---|
| 80 | + def _init_wizard_data(self, request):
|
|---|
| 81 | + """
|
|---|
| 82 | + Copy self.base_forms to the session scope so that subclasses can
|
|---|
| 83 | + manipulate the form_classes for individual users and initialize
|
|---|
| 84 | + the pages dict.
|
|---|
| 85 | + """
|
|---|
| 86 | + session_key = self.get_wizard_data_key(request)
|
|---|
| 87 | + if session_key not in request.session:
|
|---|
| 88 | + pages = {}
|
|---|
| 89 | + for page_key in self.base_forms.keys():
|
|---|
| 90 | + pages[page_key] = {
|
|---|
| 91 | + 'valid' : False,
|
|---|
| 92 | + 'visited' : False,
|
|---|
| 93 | + 'title' : self.get_page_title(request.session, page_key)
|
|---|
| 94 | + }
|
|---|
| 95 | + request.session[session_key] = {
|
|---|
| 96 | + self.__form_classes_key : self.base_forms.copy(),
|
|---|
| 97 | + self.__cleaned_data_key : {},
|
|---|
| 98 | + self.__POST_data_key : {},
|
|---|
| 99 | + self.__pages_key : pages,
|
|---|
| 100 | + }
|
|---|
| 101 | +
|
|---|
| 102 | + def __call__(self, request, *args, **kwargs):
|
|---|
| 103 | + """
|
|---|
| 104 | + Initialize the form_classes for a session if needed and call GET or
|
|---|
| 105 | + POST depending on the provided request's method.
|
|---|
| 106 | + """
|
|---|
| 107 | + self._init_wizard_data(request)
|
|---|
| 108 | +
|
|---|
| 109 | + if request.method == 'POST':
|
|---|
| 110 | + return self.POST(request)
|
|---|
| 111 | + else:
|
|---|
| 112 | + return self.GET(request, kwargs['page_key'])
|
|---|
| 113 | +
|
|---|
| 114 | + def GET(self, request, page_key):
|
|---|
| 115 | + """
|
|---|
| 116 | + Initialize a form if necessary, and display the form/page identified by
|
|---|
| 117 | + page_key.
|
|---|
| 118 | + """
|
|---|
| 119 | + page_data = self._get_cleaned_data(request.session, page_key)
|
|---|
| 120 | + if page_data is None:
|
|---|
| 121 | + form = self._get_form_classes(request.session)[page_key]()
|
|---|
| 122 | + else:
|
|---|
| 123 | + form_class = self._get_form_classes(request.session)[page_key]
|
|---|
| 124 | + if issubclass(form_class, forms.ModelForm):
|
|---|
| 125 | + form = form_class(instance=form_class.Meta.model(**page_data))
|
|---|
| 126 | + else:
|
|---|
| 127 | + form = form_class(initial=page_data)
|
|---|
| 128 | +
|
|---|
| 129 | + return self._show_form(request, page_key, form)
|
|---|
| 130 | +
|
|---|
| 131 | + def POST(self, request):
|
|---|
| 132 | + """
|
|---|
| 133 | + Validate form submission, and redirect to GET the next form or return
|
|---|
| 134 | + the response from self.done(). Note that the request.POST data must
|
|---|
| 135 | + contain a value for the key 'page_key', and this value must reference
|
|---|
| 136 | + a form in the form_classes collection for this wizard.
|
|---|
| 137 | + """
|
|---|
| 138 | + form_classes = self._get_form_classes(request.session)
|
|---|
| 139 | + page_key = request.POST['page_key']
|
|---|
| 140 | + page0 = form_classes.keys().index(page_key)
|
|---|
| 141 | + URL_base = self.get_URL_base(request, page_key)
|
|---|
| 142 | + self._set_POST_data(request.session, page_key, request.POST)
|
|---|
| 143 | + form = form_classes[page_key](request.POST)
|
|---|
| 144 | + new_page_key = self.preprocess_submit_form(request, page_key, form)
|
|---|
| 145 | +
|
|---|
| 146 | + if new_page_key is not None:
|
|---|
| 147 | + return HttpResponseRedirect(URL_base + new_page_key)
|
|---|
| 148 | + else:
|
|---|
| 149 | + if form.is_valid():
|
|---|
| 150 | + self._set_cleaned_data(request.session, page_key,
|
|---|
| 151 | + form.cleaned_data)
|
|---|
| 152 | + self._set_page(request.session, page_key, True, True)
|
|---|
| 153 | + is_done = self.process_submit_form(request, page_key, form)
|
|---|
| 154 | + if (is_done is None or is_done == False) and \
|
|---|
| 155 | + len(form_classes) > page0 + 1:
|
|---|
| 156 | + return HttpResponseRedirect(URL_base +
|
|---|
| 157 | + self._get_next_page_key(request.session, page_key))
|
|---|
| 158 | + else:
|
|---|
| 159 | + first_broken_page, form = \
|
|---|
| 160 | + self._validate_all_forms(request.session)
|
|---|
| 161 | + if first_broken_page is not None:
|
|---|
| 162 | + return self._show_form(request, first_broken_page,
|
|---|
| 163 | + form)
|
|---|
| 164 | + else:
|
|---|
| 165 | + return self.done(request)
|
|---|
| 166 | + else:
|
|---|
| 167 | + self._set_page(request.session, page_key, False)
|
|---|
| 168 | +
|
|---|
| 169 | + return self._show_form(request, page_key, form)
|
|---|
| 170 | +
|
|---|
| 171 | +
|
|---|
| 172 | + # form util methods #
|
|---|
| 173 | + def _validate_all_forms(self, session):
|
|---|
| 174 | + """
|
|---|
| 175 | + Iterate through the session form list and validate based on 1) the
|
|---|
| 176 | + 'valid' attribute of the page data and 2) the POST data stored in the
|
|---|
| 177 | + session for this wizard. Return the page key and the form of the first
|
|---|
| 178 | + invalid form or None, None if all forms are valid.
|
|---|
| 179 | + """
|
|---|
| 180 | + for page_key, form_class in self._get_form_classes(session).iteritems():
|
|---|
| 181 | + if not self._get_pages(session)[page_key]['valid']:
|
|---|
| 182 | + form = form_class(self._get_POST_data(session, page_key))
|
|---|
| 183 | + if not form.is_valid():
|
|---|
| 184 | + return page_key, form
|
|---|
| 185 | + return None, None
|
|---|
| 186 | +
|
|---|
| 187 | + def _show_form(self, request, page_key, form):
|
|---|
| 188 | + """
|
|---|
| 189 | + Show the form associated with indicated page index.
|
|---|
| 190 | + """
|
|---|
| 191 | + URL_base = self.get_URL_base(request, page_key)
|
|---|
| 192 | + extra_context = self.process_show_form(request, page_key, form)
|
|---|
| 193 | + self._set_current_page(request.session, page_key)
|
|---|
| 194 | + pages = self._get_pages(request.session)
|
|---|
| 195 | + context = {'page_key' : page_key,
|
|---|
| 196 | + 'form' : form,
|
|---|
| 197 | + 'pages' : pages,
|
|---|
| 198 | + 'URL_base' : URL_base,
|
|---|
| 199 | + 'extra_context' : extra_context }
|
|---|
| 200 | + return render_to_response(self.get_template(page_key), context,
|
|---|
| 201 | + RequestContext(request))
|
|---|
| 202 | +
|
|---|
| 203 | + def _get_form_classes(self, session):
|
|---|
| 204 | + """
|
|---|
| 205 | + Return the collection of form classes stored in the provided session.
|
|---|
| 206 | + """
|
|---|
| 207 | + return session[self.get_wizard_data_key(session)][self.__form_classes_key]
|
|---|
| 208 | +
|
|---|
| 209 | + def _insert_form(self, session, index, page_key, form_class):
|
|---|
| 210 | + """
|
|---|
| 211 | + Insert a form class into the provided session's form list at the
|
|---|
| 212 | + provided index.
|
|---|
| 213 | + """
|
|---|
| 214 | + form_classes = self._get_form_classes(session)
|
|---|
| 215 | + form_classes.insert(index, page_key, form_class)
|
|---|
| 216 | + self._insert_wizard_data(session, self.__form_classes_key, form_classes)
|
|---|
| 217 | +
|
|---|
| 218 | + def _remove_form(self, session, page_key):
|
|---|
| 219 | + """
|
|---|
| 220 | + Remove the form at index page_key from the provided sessions form list.
|
|---|
| 221 | + """
|
|---|
| 222 | + self._del_wizard_data(session, self.__form_classes_key, page_key)
|
|---|
| 223 | + # end form util methods #
|
|---|
| 224 | +
|
|---|
| 225 | +
|
|---|
| 226 | + # Form data methods #
|
|---|
| 227 | + def _get_POST_data(self, session, page_key):
|
|---|
| 228 | + """
|
|---|
| 229 | + Return the POST data for a page_key stored in the provided session.
|
|---|
| 230 | + """
|
|---|
| 231 | + post_data = self._get_all_POST_data(session)
|
|---|
| 232 | + if page_key in post_data:
|
|---|
| 233 | + return post_data[page_key]
|
|---|
| 234 | + else:
|
|---|
| 235 | + return {}
|
|---|
| 236 | +
|
|---|
| 237 | + def _set_POST_data(self, session, page_key, data):
|
|---|
| 238 | + """
|
|---|
| 239 | + Set the POST data for a given page_key and session to the 'data'
|
|---|
| 240 | + provided.
|
|---|
| 241 | + """
|
|---|
| 242 | + post_data = self._get_all_POST_data(session)
|
|---|
| 243 | + post_data[page_key] = data
|
|---|
| 244 | + self._insert_wizard_data(session, self.__POST_data_key, post_data)
|
|---|
| 245 | +
|
|---|
| 246 | + def _remove_POST_data(self, session, page_key):
|
|---|
| 247 | + """
|
|---|
| 248 | + Remove the POST data stored in the session at index page_key.
|
|---|
| 249 | + """
|
|---|
| 250 | + self._del_wizard_data(session, self.__POST_data_key, page_key)
|
|---|
| 251 | +
|
|---|
| 252 | + def _get_all_POST_data(self, session):
|
|---|
| 253 | + """
|
|---|
| 254 | + Return the dict of all POST data for this wizard from the provided
|
|---|
| 255 | + session.
|
|---|
| 256 | + """
|
|---|
| 257 | + return session[self.get_wizard_data_key(session)][self.__POST_data_key]
|
|---|
| 258 | +
|
|---|
| 259 | + def _get_cleaned_data(self, session, page_key):
|
|---|
| 260 | + """
|
|---|
| 261 | + Return the cleaned data from the provided session for this wizard based
|
|---|
| 262 | + on the provided page_key.
|
|---|
| 263 | + """
|
|---|
| 264 | + cleaned_data = self._get_all_cleaned_data(session)
|
|---|
| 265 | + if page_key in cleaned_data:
|
|---|
| 266 | + return cleaned_data[page_key]
|
|---|
| 267 | + else:
|
|---|
| 268 | + return {}
|
|---|
| 269 | +
|
|---|
| 270 | + def _set_cleaned_data(self, session, page_key, data):
|
|---|
| 271 | + """
|
|---|
| 272 | + Assign the cleaned data for this wizard in the session at index
|
|---|
| 273 | + page_key.
|
|---|
| 274 | + """
|
|---|
| 275 | + cleaned_data = self._get_all_cleaned_data(session)
|
|---|
| 276 | + cleaned_data[page_key] = data
|
|---|
| 277 | + self._insert_wizard_data(session, self.__cleaned_data_key, cleaned_data)
|
|---|
| 278 | +
|
|---|
| 279 | + def _get_all_cleaned_data(self, session):
|
|---|
| 280 | + """
|
|---|
| 281 | + Return a list of all the cleaned data in the session for this wizard.
|
|---|
| 282 | + """
|
|---|
| 283 | + wizard_data = session[self.get_wizard_data_key(session)]
|
|---|
| 284 | + return wizard_data[self.__cleaned_data_key]
|
|---|
| 285 | +
|
|---|
| 286 | + def _remove_cleaned_data(self, session, page_key):
|
|---|
| 287 | + """
|
|---|
| 288 | + Remove the cleaned data at index page_key for this wizard from the
|
|---|
| 289 | + provided session.
|
|---|
| 290 | + """
|
|---|
| 291 | + self._del_wizard_data(session, self.__cleaned_data_key, page_key)
|
|---|
| 292 | + # end Form data methods #
|
|---|
| 293 | +
|
|---|
| 294 | +
|
|---|
| 295 | + # page methods #
|
|---|
| 296 | + def _get_next_page_key(self, session, page_key):
|
|---|
| 297 | + """
|
|---|
| 298 | + Return the next page_key after the provided page_key in the sequence of
|
|---|
| 299 | + pages. If this is a named pages wizard, this method iterates
|
|---|
| 300 | + through keys. Otherwise it will simply iterate the page_key.
|
|---|
| 301 | + This method must return a String.
|
|---|
| 302 | + """
|
|---|
| 303 | + form_classes_keys = self._get_form_classes(session).keys()
|
|---|
| 304 | + return form_classes_keys[form_classes_keys.index(page_key) + 1]
|
|---|
| 305 | +
|
|---|
| 306 | + def _set_current_page(self, session, page_key):
|
|---|
| 307 | + """
|
|---|
| 308 | + Iterate through the page dicts in the session and set 'current_page' to
|
|---|
| 309 | + True for the page corresponding to page_key and False for all others.
|
|---|
| 310 | + """
|
|---|
| 311 | + for key, page in self._get_pages(session).iteritems():
|
|---|
| 312 | + if key == page_key:
|
|---|
| 313 | + page['current_page'] = True
|
|---|
| 314 | + else:
|
|---|
| 315 | + page['current_page'] = False
|
|---|
| 316 | +
|
|---|
| 317 | + def _get_pages(self, session):
|
|---|
| 318 | + """
|
|---|
| 319 | + Return the list of page info dicts stored in the provided session for
|
|---|
| 320 | + this wizard.
|
|---|
| 321 | + """
|
|---|
| 322 | + return session[self.get_wizard_data_key(session)][self.__pages_key]
|
|---|
| 323 | +
|
|---|
| 324 | + def _remove_page_data(self, session, page_key):
|
|---|
| 325 | + """
|
|---|
| 326 | + Remove page data from the provided session for this wizard based on a
|
|---|
| 327 | + given page_key. This removes page information, form_class and form
|
|---|
| 328 | + data.
|
|---|
| 329 | + """
|
|---|
| 330 | + self._remove_form(session, page_key)
|
|---|
| 331 | + self._remove_page(session, page_key)
|
|---|
| 332 | + self._remove_cleaned_data(session, page_key)
|
|---|
| 333 | + self._remove_POST_data(session, page_key)
|
|---|
| 334 | +
|
|---|
| 335 | + def _remove_page(self, session, page_key):
|
|---|
| 336 | + """
|
|---|
| 337 | + Remove the page info dict for this wizard stored at a given page_key
|
|---|
| 338 | + from the provided session.
|
|---|
| 339 | + """
|
|---|
| 340 | + self._del_wizard_data(session, self.__pages_key, page_key)
|
|---|
| 341 | +
|
|---|
| 342 | + def _insert_page(self, session, index, page_key, form_class):
|
|---|
| 343 | + """
|
|---|
| 344 | + Insert a page into this wizard at the provided form_class index, storing
|
|---|
| 345 | + required associated data.
|
|---|
| 346 | + """
|
|---|
| 347 | + self._insert_form(session, index, page_key, form_class)
|
|---|
| 348 | + self._set_page(session, page_key, False)
|
|---|
| 349 | + self._set_cleaned_data(session, page_key, {})
|
|---|
| 350 | + self._set_POST_data(session, page_key, {})
|
|---|
| 351 | +
|
|---|
| 352 | + def _set_page(self, session, page_key, valid=False, visited=False):
|
|---|
| 353 | + """
|
|---|
| 354 | + Set the page info in this wizard for a page at index page_key and stored
|
|---|
| 355 | + in the provided session.
|
|---|
| 356 | + """
|
|---|
| 357 | + page_info = {
|
|---|
| 358 | + 'valid' : valid,
|
|---|
| 359 | + 'visited' : visited,
|
|---|
| 360 | + 'title' : self.get_page_title(session, page_key)
|
|---|
| 361 | + }
|
|---|
| 362 | + pages = self._get_pages(session)
|
|---|
| 363 | + pages[page_key] = page_info
|
|---|
| 364 | + self._insert_wizard_data(session, self.__pages_key, pages)
|
|---|
| 365 | + # end page methods #
|
|---|
| 366 | +
|
|---|
| 367 | + # start wizard data utils #
|
|---|
| 368 | + def _clear_wizard_data_from_session(self, session):
|
|---|
| 369 | + """
|
|---|
| 370 | + Clear the session data used by this wizard from the provided session.
|
|---|
| 371 | + """
|
|---|
| 372 | + del session[self.get_wizard_data_key(session)]
|
|---|
| 373 | +
|
|---|
| 374 | + def _insert_wizard_data(self, session, key, data):
|
|---|
| 375 | + """
|
|---|
| 376 | + Inserts wizard data into the provided session at the provided key.
|
|---|
| 377 | + """
|
|---|
| 378 | + wizard_data = session[self.get_wizard_data_key(session)]
|
|---|
| 379 | + wizard_data[key] = data
|
|---|
| 380 | + session[self.get_wizard_data_key(session)] = wizard_data
|
|---|
| 381 | +
|
|---|
| 382 | + def _del_wizard_data(self, session, key, page_key):
|
|---|
| 383 | + """
|
|---|
| 384 | + Deletes wizard data from the provided session based on a page_key.
|
|---|
| 385 | + """
|
|---|
| 386 | + wizard_data = session[self.get_wizard_data_key(session)]
|
|---|
| 387 | + sub_set = wizard_data[key]
|
|---|
| 388 | + if page_key in sub_set:
|
|---|
| 389 | + del sub_set[page_key]
|
|---|
| 390 | + wizard_data[key] = sub_set
|
|---|
| 391 | + session[self.get_wizard_data_key(session)] = wizard_data
|
|---|
| 392 | +
|
|---|
| 393 | + # end wizard data utils #
|
|---|
| 394 | +
|
|---|
| 395 | + # typically overriden methods #
|
|---|
| 396 | + def get_wizard_data_key(self, request):
|
|---|
| 397 | + """
|
|---|
| 398 | + Return a session key for this wizard. The provided request could be
|
|---|
| 399 | + used to prevent overlapping keys in the case that someone needs
|
|---|
| 400 | + multiple instances of this wizard at one time.
|
|---|
| 401 | + """
|
|---|
| 402 | + return 'session_wizard_data'
|
|---|
| 403 | +
|
|---|
| 404 | + def get_URL_base(self, request, page_key):
|
|---|
| 405 | + """
|
|---|
| 406 | + Return the URL to this wizard minus the "page_key" part of the URL.
|
|---|
| 407 | + This value is passed to template as URL_base.
|
|---|
| 408 | + """
|
|---|
| 409 | + return request.path.replace("/" + page_key, "/")
|
|---|
| 410 | +
|
|---|
| 411 | + def get_page_title(self, session, page_key):
|
|---|
| 412 | + """
|
|---|
| 413 | + Return a user friendly title for the page at index page_key.
|
|---|
| 414 | + """
|
|---|
| 415 | + if self.contains_named_pages:
|
|---|
| 416 | + return page_key.replace("_", " ").title()
|
|---|
| 417 | + else:
|
|---|
| 418 | + return 'Page %s' % str(int(page_key) + 1)
|
|---|
| 419 | +
|
|---|
| 420 | + def process_show_form(self, request, page_key, form):
|
|---|
| 421 | + """
|
|---|
| 422 | + Called before rendering a form either from a GET or when a form submit
|
|---|
| 423 | + is invalid.
|
|---|
| 424 | + """
|
|---|
| 425 | +
|
|---|
| 426 | + def preprocess_submit_form(self, request, page_key, form):
|
|---|
| 427 | + """
|
|---|
| 428 | + Called when a form is POSTed, but before the form is validated. If this
|
|---|
| 429 | + function returns None then form submission continues, else it should
|
|---|
| 430 | + return a new page index that will be redirected to as a GET.
|
|---|
| 431 | + """
|
|---|
| 432 | +
|
|---|
| 433 | + def process_submit_form(self, request, page_key, form):
|
|---|
| 434 | + """
|
|---|
| 435 | + Called when a form is POSTed. This is only called if the form data is
|
|---|
| 436 | + valid. If this method returns True, the done() method is called,
|
|---|
| 437 | + otherwise the wizard continues. Note that it is possible that this
|
|---|
| 438 | + method would not return True, and done() would still be called because
|
|---|
| 439 | + there are no more forms left in the form_classes.
|
|---|
| 440 | + """
|
|---|
| 441 | +
|
|---|
| 442 | + def get_template(self, page_key):
|
|---|
| 443 | + """
|
|---|
| 444 | + Hook for specifying the name of the template to use for a given page.
|
|---|
| 445 | + Note that this can return a tuple of template names if you'd like to
|
|---|
| 446 | + use the template system's select_template() hook.
|
|---|
| 447 | + """
|
|---|
| 448 | + return 'forms/session_wizard.html'
|
|---|
| 449 | +
|
|---|
| 450 | + def done(self, request):
|
|---|
| 451 | + """
|
|---|
| 452 | + Hook for doing something with the validated data. This is responsible
|
|---|
| 453 | + for the final processing including clearing the session scope of items
|
|---|
| 454 | + created by this wizard.
|
|---|
| 455 | + """
|
|---|
| 456 | + raise NotImplementedError("Your %s class has not defined a done() " + \
|
|---|
| 457 | + "method, which is required." \
|
|---|
| 458 | + % self.__class__.__name__)
|
|---|
| 459 | Index: django-trunk/django/contrib/formtools/tests.py
|
|---|
| 460 | ===================================================================
|
|---|
| 461 | --- django-trunk/django/contrib/formtools/tests.py (revision 9084)
|
|---|
| 462 | +++ django-trunk/django/contrib/formtools/tests.py (working copy)
|
|---|
| 463 | @@ -1,8 +1,11 @@
|
|---|
| 464 | from django import forms
|
|---|
| 465 | +from django.db import models
|
|---|
| 466 | from django.contrib.formtools import preview, wizard
|
|---|
| 467 | from django import http
|
|---|
| 468 | from django.test import TestCase
|
|---|
| 469 | +from django.test.client import Client
|
|---|
| 470 |
|
|---|
| 471 | +
|
|---|
| 472 | success_string = "Done was called!"
|
|---|
| 473 |
|
|---|
| 474 | class TestFormPreview(preview.FormPreview):
|
|---|
| 475 | @@ -141,4 +144,168 @@
|
|---|
| 476 | request = DummyRequest(POST={"0-field":"test", "wizard_step":"0"})
|
|---|
| 477 | response = wizard(request)
|
|---|
| 478 | self.assertEquals(1, wizard.step)
|
|---|
| 479 | +
|
|---|
| 480 |
|
|---|
| 481 | +#
|
|---|
| 482 | +# SessionWizard tests
|
|---|
| 483 | +#
|
|---|
| 484 | +class SessionWizardModel(models.Model):
|
|---|
| 485 | + field = models.CharField(max_length=10)
|
|---|
| 486 | +
|
|---|
| 487 | +class SessionWizardPageOneForm(forms.Form):
|
|---|
| 488 | + field = forms.CharField(required=True)
|
|---|
| 489 | +
|
|---|
| 490 | +class SessionWizardPageTwoForm(forms.ModelForm):
|
|---|
| 491 | + class Meta:
|
|---|
| 492 | + model = SessionWizardModel
|
|---|
| 493 | +
|
|---|
| 494 | +class SessionWizardPageThreeForm(forms.Form):
|
|---|
| 495 | + field = forms.CharField()
|
|---|
| 496 | +
|
|---|
| 497 | +class SessionWizardDynamicPageForm(forms.Form):
|
|---|
| 498 | + field = forms.CharField()
|
|---|
| 499 | +
|
|---|
| 500 | +class SessionWizardClass(wizard.SessionWizard):
|
|---|
| 501 | + def get_page_title(self, session, page_key):
|
|---|
| 502 | + try:
|
|---|
| 503 | + return "Custom Page Title: %s" % str(int(page_key) + 1)
|
|---|
| 504 | + except ValueError:
|
|---|
| 505 | + return super(SessionWizardClass, self).get_page_title(session,
|
|---|
| 506 | + page_key)
|
|---|
| 507 | +
|
|---|
| 508 | + def process_show_form(self, request, page_key, form):
|
|---|
| 509 | + try:
|
|---|
| 510 | + return {'form_title' : 'Form %s' % str(int(page_key) + 1)}
|
|---|
| 511 | + except ValueError:
|
|---|
| 512 | + return super(SessionWizardClass, self).process_show_form(request,
|
|---|
| 513 | + page_key, form)
|
|---|
| 514 | +
|
|---|
| 515 | + def preprocess_submit_form(self, request, page_key, form):
|
|---|
| 516 | + if page_key == "1" and request.POST['field'] == "":
|
|---|
| 517 | + self._remove_page(request.session, page_key)
|
|---|
| 518 | + return str(int(page_key) - 1)
|
|---|
| 519 | +
|
|---|
| 520 | + def process_submit_form(self, request, page_key, form):
|
|---|
| 521 | + if page_key == '2':
|
|---|
| 522 | + self._insert_page(request.session, 3, str(int(page_key) + 1),
|
|---|
| 523 | + SessionWizardDynamicPageForm)
|
|---|
| 524 | +
|
|---|
| 525 | + def get_template(self, page_key):
|
|---|
| 526 | + return "formtools/form.html"
|
|---|
| 527 | +
|
|---|
| 528 | + def done(self, request):
|
|---|
| 529 | + return http.HttpResponse(success_string)
|
|---|
| 530 | +
|
|---|
| 531 | +class SessionWizardTests(TestCase):
|
|---|
| 532 | + urls = 'django.contrib.formtools.test_urls'
|
|---|
| 533 | +
|
|---|
| 534 | + def test_named_pages_wizard_get(self):
|
|---|
| 535 | + """
|
|---|
| 536 | + Tests that a wizard is created properly based on it's initialization
|
|---|
| 537 | + argument, which could be a sequence or dictionary-like object.
|
|---|
| 538 | + """
|
|---|
| 539 | + response = self.client.get('/named_pages_wizard/first_page_form')
|
|---|
| 540 | + self.assertEquals(200, response.status_code)
|
|---|
| 541 | + self.assertEquals('First Page Form',
|
|---|
| 542 | + response.context[0]['pages']['first_page_form']['title'])
|
|---|
| 543 | +
|
|---|
| 544 | +
|
|---|
| 545 | + def test_valid_POST(self):
|
|---|
| 546 | + """
|
|---|
| 547 | + Tests that a post containing valid data will set session values
|
|---|
| 548 | + correctly and redirect to the next page.
|
|---|
| 549 | + """
|
|---|
| 550 | + response = self.client.post('/sessionwizard/', {"page_key":"0",
|
|---|
| 551 | + "field":"test"})
|
|---|
| 552 | + self.assertEquals(302, response.status_code)
|
|---|
| 553 | + self.assertEquals("http://testserver/sessionwizard/1",
|
|---|
| 554 | + response['Location'])
|
|---|
| 555 | + session = self.client.session
|
|---|
| 556 | + cleaned_data = session['session_wizard_data']['cleaned_data']
|
|---|
| 557 | + post_data = session['session_wizard_data']['POST_data']
|
|---|
| 558 | + self.assertEquals('test', cleaned_data['0']['field'])
|
|---|
| 559 | + self.assertEquals('test', post_data['0']['field'])
|
|---|
| 560 | +
|
|---|
| 561 | + def test_invalid_POST(self):
|
|---|
| 562 | + """
|
|---|
| 563 | + Tests that a post containing invalid data will set session values
|
|---|
| 564 | + correctly and redisplay the form.
|
|---|
| 565 | + """
|
|---|
| 566 | + response = self.client.post('/sessionwizard/', {"page_key":"0",
|
|---|
| 567 | + "field":""})
|
|---|
| 568 | + self.assertEquals(200, response.status_code)
|
|---|
| 569 | + session = self.client.session
|
|---|
| 570 | + post_data = session['session_wizard_data']['POST_data']
|
|---|
| 571 | + self.assertEquals('', post_data['0']['field'])
|
|---|
| 572 | +
|
|---|
| 573 | + def test_GET(self):
|
|---|
| 574 | + """
|
|---|
| 575 | + Tests that a get will display a page properly.
|
|---|
| 576 | + """
|
|---|
| 577 | + response = self.client.get('/sessionwizard/0')
|
|---|
| 578 | + self.assertEquals(200, response.status_code)
|
|---|
| 579 | +
|
|---|
| 580 | + def test_preprocess_submit_form(self):
|
|---|
| 581 | + """
|
|---|
| 582 | + Tests the preprocess_submit_form hook of SessionWizard POSTs.
|
|---|
| 583 | + The SessionWizardClass is coded to short-circuit a POST for page index 1
|
|---|
| 584 | + when form.cleaned_data['field'] == '' by returning a reference to page_key
|
|---|
| 585 | + index 0.
|
|---|
| 586 | + """
|
|---|
| 587 | + response = self.client.post('/sessionwizard/', {"page_key":"1",
|
|---|
| 588 | + "field":""})
|
|---|
| 589 | + self.assertEquals(302, response.status_code)
|
|---|
| 590 | + self.assertEquals("http://testserver/sessionwizard/0",
|
|---|
| 591 | + response['Location'])
|
|---|
| 592 | +
|
|---|
| 593 | + def test_process_submit_form(self):
|
|---|
| 594 | + """
|
|---|
| 595 | + Tests the process_submit_form hook of SessionWizard POSTs.
|
|---|
| 596 | + The SessionWizardClass is coded to insert a new page at index 3 on a
|
|---|
| 597 | + POST for page index 2.
|
|---|
| 598 | + """
|
|---|
| 599 | + response = self.client.post('/sessionwizard/', {"page_key":"2",
|
|---|
| 600 | + "field":"test"})
|
|---|
| 601 | + self.assertEquals(302, response.status_code)
|
|---|
| 602 | + self.assertEquals("http://testserver/sessionwizard/3",
|
|---|
| 603 | + response['Location'])
|
|---|
| 604 | + self.assertEquals({"0":SessionWizardPageOneForm,
|
|---|
| 605 | + "1":SessionWizardPageTwoForm,
|
|---|
| 606 | + "2":SessionWizardPageThreeForm,
|
|---|
| 607 | + "3":SessionWizardDynamicPageForm},
|
|---|
| 608 | + self.client.session['session_wizard_data']['form_classes'])
|
|---|
| 609 | +
|
|---|
| 610 | + def test_process_show_form(self):
|
|---|
| 611 | + """
|
|---|
| 612 | + Tests the process_show_form hook. SessionWizardClass is coded to
|
|---|
| 613 | + return a extra_context having a specific 'form_title' attribute.
|
|---|
| 614 | + """
|
|---|
| 615 | + response = self.client.get('/sessionwizard/0')
|
|---|
| 616 | + self.assertEquals(200, response.status_code)
|
|---|
| 617 | + self.assertEquals("Form 1",
|
|---|
| 618 | + response.context[0]['extra_context']['form_title'])
|
|---|
| 619 | +
|
|---|
| 620 | + def test_validate_all(self):
|
|---|
| 621 | + """
|
|---|
| 622 | + Submits all forms, with one of them being invalid, and tests that
|
|---|
| 623 | + submitting the last form will catch an invalid form earlier in the
|
|---|
| 624 | + workflow and redisplay it.
|
|---|
| 625 | + """
|
|---|
| 626 | + response = self.client.post('/sessionwizard/', {"page_key":"0", "field":""})
|
|---|
| 627 | + self.client.post('/sessionwizard/', {"page_key":"1", "field":"test1"})
|
|---|
| 628 | + self.client.post('/sessionwizard/', {"page_key":"2", "field":"test2"})
|
|---|
| 629 | + response = self.client.post('/sessionwizard/3',
|
|---|
| 630 | + {"page_key":"3", "field":"test3"})
|
|---|
| 631 | + self.assertEquals(True, response.context[0]['pages']['1']['visited'])
|
|---|
| 632 | + self.assertEquals(True, response.context[0]['pages']['1']['valid'])
|
|---|
| 633 | +
|
|---|
| 634 | + self.assertEquals("Form 1",
|
|---|
| 635 | + response.context[0]['extra_context']['form_title'])
|
|---|
| 636 | +
|
|---|
| 637 | + def test_done(self):
|
|---|
| 638 | + self.client.post('/sessionwizard/', {"page_key":"0", "field":"test0"})
|
|---|
| 639 | + self.client.post('/sessionwizard/', {"page_key":"1", "field":"test1"})
|
|---|
| 640 | + self.client.post('/sessionwizard/', {"page_key":"2", "field":"test2"})
|
|---|
| 641 | + response = self.client.post('/sessionwizard/',
|
|---|
| 642 | + {"page_key":"3", "field":"test3"})
|
|---|
| 643 | + self.assertEqual(response.content, success_string)
|
|---|
| 644 | Index: django-trunk/django/contrib/formtools/test_urls.py
|
|---|
| 645 | ===================================================================
|
|---|
| 646 | --- django-trunk/django/contrib/formtools/test_urls.py (revision 9084)
|
|---|
| 647 | +++ django-trunk/django/contrib/formtools/test_urls.py (working copy)
|
|---|
| 648 | @@ -9,4 +9,13 @@
|
|---|
| 649 |
|
|---|
| 650 | urlpatterns = patterns('',
|
|---|
| 651 | (r'^test1/', TestFormPreview(TestForm)),
|
|---|
| 652 | + (r'^sessionwizard/(?P<page_key>\d*)$',
|
|---|
| 653 | + SessionWizardClass([SessionWizardPageOneForm,
|
|---|
| 654 | + SessionWizardPageTwoForm,
|
|---|
| 655 | + SessionWizardPageThreeForm])),
|
|---|
| 656 | + (r'^named_pages_wizard/(?P<page_key>\w*)$',
|
|---|
| 657 | + SessionWizardClass((
|
|---|
| 658 | + ('first_page_form', SessionWizardPageOneForm),
|
|---|
| 659 | + ('page2', SessionWizardPageTwoForm),
|
|---|
| 660 | + ('page3', SessionWizardPageThreeForm),)))
|
|---|
| 661 | )
|
|---|
| 662 | Index: django-trunk/docs/ref/contrib/formtools/session-wizard.txt
|
|---|
| 663 | ===================================================================
|
|---|
| 664 | --- django-trunk/docs/ref/contrib/formtools/session-wizard.txt (revision 0)
|
|---|
| 665 | +++ django-trunk/docs/ref/contrib/formtools/session-wizard.txt (revision 0)
|
|---|
| 666 | @@ -0,0 +1,345 @@
|
|---|
| 667 | +.. _ref-contrib-formtools-session-wizard:
|
|---|
| 668 | +
|
|---|
| 669 | +==============
|
|---|
| 670 | +Session wizard
|
|---|
| 671 | +==============
|
|---|
| 672 | +
|
|---|
| 673 | +.. module:: django.contrib.formtools.wizard
|
|---|
| 674 | + :synopsis: Splits forms across multiple Web pages using users' sessions to store form and page data.
|
|---|
| 675 | +
|
|---|
| 676 | +.. versionadded:: 1.x
|
|---|
| 677 | +
|
|---|
| 678 | +Django comes with an optional "session wizard" application that splits
|
|---|
| 679 | +:ref:`forms <topics-forms-index>` across multiple Web pages. It maintains
|
|---|
| 680 | +state in users' sessions incurring additional resource costs on a server
|
|---|
| 681 | +but also creating a smoother workflow for users.
|
|---|
| 682 | +
|
|---|
| 683 | +Note that SessionWizard is similar to :ref:`FormWizard <ref-contrib-formtools-form-wizard>`
|
|---|
| 684 | +and some of these examples and documentation mirror FormWizard examples and
|
|---|
| 685 | +documentation exactly.
|
|---|
| 686 | +
|
|---|
| 687 | +You might want to use this if you have a workflow or lengthy form and want to
|
|---|
| 688 | +provide navigation to various pages in the wizard.
|
|---|
| 689 | +
|
|---|
| 690 | +The term "wizard," in this context, is `explained on Wikipedia`_.
|
|---|
| 691 | +
|
|---|
| 692 | +.. _explained on Wikipedia: http://en.wikipedia.org/wiki/Wizard_%28software%29
|
|---|
| 693 | +.. _forms: ../forms/
|
|---|
| 694 | +
|
|---|
| 695 | +How it works
|
|---|
| 696 | +============
|
|---|
| 697 | +
|
|---|
| 698 | +Here's the basic workflow for how a user would use a wizard:
|
|---|
| 699 | +
|
|---|
| 700 | + 1. The user visits the first page of the wizard, fills in the form and
|
|---|
| 701 | + submits it.
|
|---|
| 702 | + 2. The server validates the data. If it's invalid, the form is displayed
|
|---|
| 703 | + again, with error messages. If it's valid, the server stores this data
|
|---|
| 704 | + in a user's session and sends an HTTP redirect to GET the next page.
|
|---|
| 705 | + 3. Step 1 and 2 repeat, for every subsequent form in the wizard.
|
|---|
| 706 | + 4. Once the user has submitted all the forms and all the data has been
|
|---|
| 707 | + validated, the wizard processes the data -- saving it to the database,
|
|---|
| 708 | + sending an e-mail, or whatever the application needs to do.
|
|---|
| 709 | +
|
|---|
| 710 | +Usage
|
|---|
| 711 | +=====
|
|---|
| 712 | +
|
|---|
| 713 | +This application handles as much machinery for you as possible. Generally, you
|
|---|
| 714 | +just have to do these things:
|
|---|
| 715 | +
|
|---|
| 716 | + 1. Define a number of :mod:`django.forms`
|
|---|
| 717 | + :class:`~django.forms.forms.Form` classes -- one per wizard page.
|
|---|
| 718 | +
|
|---|
| 719 | + 2. Create a :class:`~django.contrib.formtools.wizard.SessionWizard` class
|
|---|
| 720 | + that specifies what to do once all of your forms have been submitted
|
|---|
| 721 | + and validated. This also lets you override some of the wizard's behavior.
|
|---|
| 722 | +
|
|---|
| 723 | + 3. Create some templates that render the forms. You can define a single,
|
|---|
| 724 | + generic template to handle every one of the forms, or you can define a
|
|---|
| 725 | + specific template for each form.
|
|---|
| 726 | +
|
|---|
| 727 | + 4. Point your URLconf at your
|
|---|
| 728 | + :class:`~django.contrib.formtools.wizard.SessionWizard` class.
|
|---|
| 729 | +
|
|---|
| 730 | +Defining ``Form`` classes
|
|---|
| 731 | +=========================
|
|---|
| 732 | +
|
|---|
| 733 | +The first step in creating a form wizard is to create the
|
|---|
| 734 | +:class:`~django.forms.forms.Form` classes. These should be standard
|
|---|
| 735 | +:mod:`django.forms` :class:`~django.forms.forms.Form` classes, covered in the
|
|---|
| 736 | +:ref:`forms documentation <topics-forms-index>`.
|
|---|
| 737 | +
|
|---|
| 738 | +These classes can live anywhere in your codebase, but convention is to put them
|
|---|
| 739 | +in a file called :file:`forms.py` in your application.
|
|---|
| 740 | +
|
|---|
| 741 | +For example, let's write a "contact form" wizard, where the first page's form
|
|---|
| 742 | +collects the sender's e-mail address and subject, and the second page collects
|
|---|
| 743 | +the message itself. Here's what the :file:`forms.py` might look like::
|
|---|
| 744 | +
|
|---|
| 745 | + from django import forms
|
|---|
| 746 | +
|
|---|
| 747 | + class ContactForm1(forms.Form):
|
|---|
| 748 | + subject = forms.CharField(max_length=100)
|
|---|
| 749 | + sender = forms.EmailField()
|
|---|
| 750 | +
|
|---|
| 751 | + class ContactForm2(forms.Form):
|
|---|
| 752 | + message = forms.CharField(widget=forms.Textarea)
|
|---|
| 753 | +
|
|---|
| 754 | +**Important limitation:** Because the wizard uses users' sessions to store
|
|---|
| 755 | +data between pages, you should seriously consider whether or not it
|
|---|
| 756 | +makes sense to include :class:`~django.forms.fields.FileField` in any forms.
|
|---|
| 757 | +
|
|---|
| 758 | +Creating a ``SessionWizard`` class
|
|---|
| 759 | +==================================
|
|---|
| 760 | +
|
|---|
| 761 | +The next step is to create a :class:`~django.contrib.formtools.wizard.SessionWizard`
|
|---|
| 762 | +class, which should be a subclass of ``django.contrib.formtools.wizard.SessionWizard``.
|
|---|
| 763 | +
|
|---|
| 764 | +As your :class:`~django.forms.forms.Form` classes, this
|
|---|
| 765 | +:class:`~django.contrib.formtools.wizard.SessionWizard` class can live anywhere
|
|---|
| 766 | +in your codebase, but convention is to put it in :file:`forms.py`.
|
|---|
| 767 | +
|
|---|
| 768 | +The only requirement on this subclass is that it implement a
|
|---|
| 769 | +:meth:`~django.contrib.formtools.wizard.SessionWizard.done()` method,
|
|---|
| 770 | +which specifies what should happen when the data for *every* form is submitted
|
|---|
| 771 | +and validated. This method is passed one argument:
|
|---|
| 772 | +
|
|---|
| 773 | + * ``request`` -- an :class:`~django.http.HttpRequest` object
|
|---|
| 774 | +
|
|---|
| 775 | +In this simplistic example, rather than perform any database operation, the
|
|---|
| 776 | +method simply renders a template of the validated data::
|
|---|
| 777 | +
|
|---|
| 778 | + from django.shortcuts import render_to_response
|
|---|
| 779 | + from django.contrib.formtools.wizard import SessionWizard
|
|---|
| 780 | +
|
|---|
| 781 | + class ContactWizard(SessionWizard):
|
|---|
| 782 | + def done(self, request):
|
|---|
| 783 | + form_data = self._get_all_cleaned_data(request.session)
|
|---|
| 784 | + self._clear_wizard_data_from_session(request.session)
|
|---|
| 785 | + return render_to_response('done.html', {
|
|---|
| 786 | + 'form_data': form_data,
|
|---|
| 787 | + })
|
|---|
| 788 | +
|
|---|
| 789 | +Note that this method will be called via ``POST``, so it really ought to be a
|
|---|
| 790 | +good Web citizen and redirect after processing the data. Here's another
|
|---|
| 791 | +example::
|
|---|
| 792 | +
|
|---|
| 793 | + from django.http import HttpResponseRedirect
|
|---|
| 794 | + from django.contrib.formtools.wizard import SessionWizard
|
|---|
| 795 | +
|
|---|
| 796 | + class ContactWizard(SessionWizard):
|
|---|
| 797 | + def done(self, request):
|
|---|
| 798 | + form_data = self._get_all_cleaned_data(request.session)
|
|---|
| 799 | + self._clear_wizard_data_from_session(request.session)
|
|---|
| 800 | + do_something_with_the_form_data(form_data)
|
|---|
| 801 | + return HttpResponseRedirect('/page-to-redirect-to-when-done/')
|
|---|
| 802 | +
|
|---|
| 803 | +See the section `Advanced SessionWizard methods`_ below to learn about more
|
|---|
| 804 | +:class:`~django.contrib.formtools.wizard.SessionWizard` hooks.
|
|---|
| 805 | +
|
|---|
| 806 | +Creating templates for the forms
|
|---|
| 807 | +================================
|
|---|
| 808 | +
|
|---|
| 809 | +Next, you'll need to create a template that renders the wizard's forms. By
|
|---|
| 810 | +default, every form uses a template called :file:`forms/session_wizard.html`.
|
|---|
| 811 | +(You can change this template name by overriding
|
|---|
| 812 | +:meth:`~django.contrib.formtools.wizard..get_template()`, which is documented
|
|---|
| 813 | +below. This hook also allows you to use a different template for each form.)
|
|---|
| 814 | +
|
|---|
| 815 | +This template expects the following context:
|
|---|
| 816 | +
|
|---|
| 817 | + * ``page_key`` -- A string representation of the current page in this
|
|---|
| 818 | + wizard. Depending on how a wizard is created, this could be a page name
|
|---|
| 819 | + or a zero-based page index.
|
|---|
| 820 | + * ``form`` -- The :class:`~django.forms.forms.Form` instance for the
|
|---|
| 821 | + current page (empty, populated or populated with errors).
|
|---|
| 822 | + * ``pages`` -- The current list of pages for this wizard. This is a dict of
|
|---|
| 823 | + dict objects in the form::
|
|---|
| 824 | +
|
|---|
| 825 | + {'page_key1' : {'title' : 'page1',
|
|---|
| 826 | + 'visited': True,
|
|---|
| 827 | + 'valid' : True,
|
|---|
| 828 | + 'current_page' : False
|
|---|
| 829 | + },
|
|---|
| 830 | + 'page_key2' : {'title' : 'page2',
|
|---|
| 831 | + 'visited': False,
|
|---|
| 832 | + 'valid' : False,
|
|---|
| 833 | + 'current_page' : True
|
|---|
| 834 | + },
|
|---|
| 835 | + ..
|
|---|
| 836 | + }
|
|---|
| 837 | + * ``URL_base`` -- The base URL used to generate links to pages in this
|
|---|
| 838 | + wizard. By default, it is the request.path value minus the ``page_key``.
|
|---|
| 839 | + * 'extra_context' -- A dict returned from the
|
|---|
| 840 | + :meth:`~django.contrib.formtools.wizard.SessionWizard.process_show_form()`
|
|---|
| 841 | + hook.
|
|---|
| 842 | +
|
|---|
| 843 | +Here's a full example template:
|
|---|
| 844 | +
|
|---|
| 845 | +.. code-block:: html+django
|
|---|
| 846 | +
|
|---|
| 847 | + {% extends "base.html" %}
|
|---|
| 848 | +
|
|---|
| 849 | + {% block content %}
|
|---|
| 850 | + <ul>
|
|---|
| 851 | + {% for page_key,page in pages.items %}
|
|---|
| 852 | + <li class="{% if page.valid %}valid{% endif %}
|
|---|
| 853 | + {% if page.current_page %}current{% endif %}">
|
|---|
| 854 | + {% if page.visited %}visited{% endif %}
|
|---|
| 855 | + <a href="{{ URL_base }}{{ page_key }}">{{ page.title }}</a>
|
|---|
| 856 | + {% else %}
|
|---|
| 857 | + {{ page.title }}
|
|---|
| 858 | + {% end if %}
|
|---|
| 859 | + </li>
|
|---|
| 860 | + {% endfor %}
|
|---|
| 861 | + </ul>
|
|---|
| 862 | + <form action="." method="post">
|
|---|
| 863 | + <table>
|
|---|
| 864 | + {{ form }}
|
|---|
| 865 | + </table>
|
|---|
| 866 | + <input type="hidden" name="page_key" value="{{ page_key }}"/>
|
|---|
| 867 | + <input type="submit">
|
|---|
| 868 | + </form>
|
|---|
| 869 | + {% endblock %}
|
|---|
| 870 | +
|
|---|
| 871 | +Note that ``page_key`` is required for the wizard to work properly.
|
|---|
| 872 | +
|
|---|
| 873 | +Hooking the wizard into a URLconf
|
|---|
| 874 | +=================================
|
|---|
| 875 | +
|
|---|
| 876 | +Finally, give your new :class:`~django.contrib.formtools.wizard.SessionWizard`
|
|---|
| 877 | +object a URL in ``urls.py``. The wizard has two types of initialization. The
|
|---|
| 878 | +first takes a list of your form objects as arguments, and the seconds takes a
|
|---|
| 879 | +sequence of 2-tuples in the form (page_key, form_class). The two types are
|
|---|
| 880 | +illustrated below::
|
|---|
| 881 | +
|
|---|
| 882 | + from django.conf.urls.defaults import *
|
|---|
| 883 | + from mysite.testapp.forms import ContactForm1, ContactForm2, ContactWizard
|
|---|
| 884 | +
|
|---|
| 885 | + urlpatterns = patterns('',
|
|---|
| 886 | + ## First form - a list of form classes
|
|---|
| 887 | + (r'^contact/(?P<page_key>\d*)$', ContactWizard([ContactForm1, ContactForm2])),
|
|---|
| 888 | +
|
|---|
| 889 | + ## Second form - a sequence of 2-tuples
|
|---|
| 890 | + (r'^contact/(?P<page_key>\w*)$', ContactWizard((("subject_and_sender", ContactForm1),
|
|---|
| 891 | + ("message", ContactForm2)))),
|
|---|
| 892 | + )
|
|---|
| 893 | +
|
|---|
| 894 | +In the first type of SessionWizard initialization, a list of form classes is
|
|---|
| 895 | +provided. The ``page_key`` values matched from a URL are auto-generated zero-based
|
|---|
| 896 | +digits. Note these values are stored as strings not integers, which is
|
|---|
| 897 | +something to keep in mind while referencing ``page_key`` values in any SessionWizard
|
|---|
| 898 | +hooks described below.
|
|---|
| 899 | +
|
|---|
| 900 | +In the second style of initialization, the ``page_key`` values from a URL are
|
|---|
| 901 | +matched exactly with ``page_key`` values provided in a sequence of 2-tuples.
|
|---|
| 902 | +
|
|---|
| 903 | +Advanced SessionWizard methods
|
|---|
| 904 | +==============================
|
|---|
| 905 | +
|
|---|
| 906 | +.. class:: SessionWizard
|
|---|
| 907 | +
|
|---|
| 908 | + Aside from the :meth:`~django.contrib.formtools.wizard.SessionWizard.done()`
|
|---|
| 909 | + method, :class:`~django.contrib.formtools.wizard.SessionWizard` offers a few
|
|---|
| 910 | + advanced method hooks that let you customize how your wizard works.
|
|---|
| 911 | +
|
|---|
| 912 | + Some of these methods take an argument ``page_key``, which is a string
|
|---|
| 913 | + representing the current page. As noted above, if a wizard is created
|
|---|
| 914 | + from a list of form classes, then this string is a zero-based auto-incremented
|
|---|
| 915 | + value. Otherwise, if a wizard is created from a sequence of 2-tuples,
|
|---|
| 916 | + the ``page_key`` is the name of the page.
|
|---|
| 917 | +
|
|---|
| 918 | +.. method:: SessionWizard.get_wizard_data_key
|
|---|
| 919 | +
|
|---|
| 920 | + Given a user's session, returns a value to be used as a key for storing and
|
|---|
| 921 | + retrieving a wizard's data. Note that a request is provided so that a
|
|---|
| 922 | + wizard could potentially avoid namespace collision in the event that
|
|---|
| 923 | + multiple instances of a wizard are required concurrently for a single user.
|
|---|
| 924 | +
|
|---|
| 925 | + Default implementation::
|
|---|
| 926 | +
|
|---|
| 927 | + def get_wizard_data_key(self, request):
|
|---|
| 928 | + return "session_wizard_data"
|
|---|
| 929 | +
|
|---|
| 930 | +.. method:: SessionWizard.get_URL_base
|
|---|
| 931 | +
|
|---|
| 932 | + Returns a URL that will be used when generating redirects. To
|
|---|
| 933 | + generate a redirect to GET the next page in a wizard, the SessionWizard
|
|---|
| 934 | + class appends a ``page_key`` to the value returned from this function.
|
|---|
| 935 | +
|
|---|
| 936 | + Default implementation::
|
|---|
| 937 | +
|
|---|
| 938 | + def get_URL_base(self, request, page_key):
|
|---|
| 939 | + return request.path.replace("/" + page_key, "/")
|
|---|
| 940 | +
|
|---|
| 941 | +.. method:: SessionWizard.get_page_title
|
|---|
| 942 | +
|
|---|
| 943 | + Return a title that will be placed in the ``pages`` template context dict.
|
|---|
| 944 | +
|
|---|
| 945 | + Default implementation::
|
|---|
| 946 | +
|
|---|
| 947 | + def get_page_title(self, session, page_key):
|
|---|
| 948 | + if self.contains_named_pages:
|
|---|
| 949 | + return page_key.replace("_", " ").title()
|
|---|
| 950 | + else:
|
|---|
| 951 | + return 'Page %s' % str(int(page_key) + 1)
|
|---|
| 952 | +
|
|---|
| 953 | +.. method:: SessionWizard.process_show_form
|
|---|
| 954 | +
|
|---|
| 955 | + A hook for providing ``extra_context`` for a page.
|
|---|
| 956 | +
|
|---|
| 957 | + By default, this does nothing.
|
|---|
| 958 | +
|
|---|
| 959 | + Example::
|
|---|
| 960 | +
|
|---|
| 961 | + def process_show_form(self, request, page_key, form):
|
|---|
| 962 | + return {'form_title' : '%s Form ' % page_key}
|
|---|
| 963 | +
|
|---|
| 964 | +.. method:: SessionWizard.get_template
|
|---|
| 965 | +
|
|---|
| 966 | + Return the name of the template that should be used for a given ``page_key``.
|
|---|
| 967 | +
|
|---|
| 968 | + By default, this returns :file:`'forms/session_wizard.html'`, regardless of
|
|---|
| 969 | + ``page_key``.
|
|---|
| 970 | +
|
|---|
| 971 | + Example::
|
|---|
| 972 | +
|
|---|
| 973 | + def get_template(self, page_key):
|
|---|
| 974 | + return 'myapp/wizard_%s.html' % page_key
|
|---|
| 975 | +
|
|---|
| 976 | + If :meth:`~SessionWizard.get_template` returns a list of strings, then the
|
|---|
| 977 | + wizard will use the template system's :func:`~django.template.loader.select_template()`
|
|---|
| 978 | + function, :ref:`explained in the template docs <ref-templates-api-the-python-api>`.
|
|---|
| 979 | + This means the system will use the first template that exists on the
|
|---|
| 980 | + filesystem. For example::
|
|---|
| 981 | +
|
|---|
| 982 | + def get_template(self, step):
|
|---|
| 983 | + return ['myapp/wizard_%s.html' % step, 'myapp/wizard.html']
|
|---|
| 984 | +
|
|---|
| 985 | + .. _explained in the template docs: ../templates_python/#the-python-api
|
|---|
| 986 | +
|
|---|
| 987 | +.. method:: SessionWizard.preprocess_submit_form
|
|---|
| 988 | +
|
|---|
| 989 | + Provides a means to short-circuit form posts and do something different
|
|---|
| 990 | + than the normal flow of validating the form and proceeding to the next page.
|
|---|
| 991 | + For instance, a wizard could present the user with a "Delete this page"
|
|---|
| 992 | + button, and use this hook to remove the stored data associated with the
|
|---|
| 993 | + provided ``page_key`` and redirect to a specific ``page_key``.
|
|---|
| 994 | +
|
|---|
| 995 | + Example::
|
|---|
| 996 | +
|
|---|
| 997 | + def preprocess_submit_form(self, request, page_key, form):
|
|---|
| 998 | + if request.POST['submit'] == "Delete this page":
|
|---|
| 999 | + self._remove_page(request.session, page_key)
|
|---|
| 1000 | + return "next_page"
|
|---|
| 1001 | +
|
|---|
| 1002 | +.. method:: SessionWizard.process_submit_form
|
|---|
| 1003 | +
|
|---|
| 1004 | + This is a hook for doing something after a valid form submission. For
|
|---|
| 1005 | + instance, a wizard could store the state of the wizard after each submission
|
|---|
| 1006 | + and allow users to resume their work after a logout or system failure.
|
|---|
| 1007 | +
|
|---|
| 1008 | + The function signature::
|
|---|
| 1009 | +
|
|---|
| 1010 | + def process_submit_form(self, request, page_key, form):
|
|---|
| 1011 | + # ...
|
|---|
| 1012 | Index: django-trunk/docs/ref/contrib/formtools/index.txt
|
|---|
| 1013 | ===================================================================
|
|---|
| 1014 | --- django-trunk/docs/ref/contrib/formtools/index.txt (revision 9084)
|
|---|
| 1015 | +++ django-trunk/docs/ref/contrib/formtools/index.txt (working copy)
|
|---|
| 1016 | @@ -10,3 +10,4 @@
|
|---|
| 1017 |
|
|---|
| 1018 | form-preview
|
|---|
| 1019 | form-wizard
|
|---|
| 1020 | + session-wizard
|
|---|