| 1 | Index: /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__)
|
|---|