- Author:
- flashingpumpkin
- Posted:
- May 20, 2009
- Language:
- Python
- Version:
- 1.0
- Score:
- 4 (after 4 ratings)
This is an extended version of the FormWizard which allows display of multiple forms per step and allows usage of ModelForms with initial objects
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 | """
FormWizard class -- implements a multi-page form, validating between each
step and storing the form's state as HTML hidden fields so that no state is
stored on the server side.
This is an extended version which allows display of multiple forms per step
"""
from django import forms
from django.conf import settings
from django.http import Http404
from django.shortcuts import render_to_response
from django.template.context import RequestContext
from django.utils.hashcompat import md5_constructor
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_str, force_unicode
from django.contrib.formtools.utils import security_hash
class MultiFormWizard(object):
# The HTML (and POST data) field name for the "step" variable.
step_field_name = "wizard_step"
def __init__(self, **kwargs):
"""
Changed __init__ method to suit our needs
Takes a dictionary as first argument and abstracts from there the
FormWizard's arguments
@param kwargs: dict(
forms = [ # List of dictionaries for each step
dict(
form = Form1,
linkform = Form2,
), # Step 1
dict(
textform = Form3,
) # Step 2
],
prefixes = [ # List of dictionaries for each step
dict(
form = 'main_form',
linkform = 'link_form',
),
dict(
textform = 'text_form',
)
],
initial = [ # List of dictionaries with initial values for each step
dict(
form = None,
linkform = None,
),
dict(
textform = None,
)
],
template = 'foo/bar.html'
)
"""
self.forms = kwargs.get('forms')
self.prefixes = kwargs.get('prefixes', [])
self.initial = kwargs.get('initial', [])
self._build_initials()
self.template = kwargs.get('template')
self.extra_context = kwargs.get('extra_context', {})
def _build_initials(self):
"""
Builds the list with initial data to prevent IndexErrors in
self.set_initial
"""
for step, forms in enumerate(self.forms):
try:
init = self.initial[step]
except IndexError:
self.initial.append(dict())
def set_inital(self, step, form_name, initial):
self.initial[step][form_name] = initial
def get_initials(self, step, form_name):
"""
Return the initial data for a form
"""
return self.initial[step].get(form_name)
def get_prefix(self, step, form_name):
"""
Returns the prefix for a form
"""
try:
prefixes = self.prefixes[step]
except:
prefixes = {}
return u'%d_%s' % (
step,
prefixes.get(form_name)
) if prefixes.get(form_name) else u'%d_%s' % (
step,
form_name
)
def get_forms(self, step, data = None, files = None):
"""
Returns the forms for the given step
"""
forms = dict()
args = (data, files)
for name, form in self.forms[step].iteritems():
kwargs = dict()
if hasattr(form, 'Meta') and hasattr(form.Meta, 'model'):
init = 'instance'
elif hasattr(form, 'management_form'):
init = 'queryset'
else:
init = 'initial'
kwargs.update({
init : self.get_initials(step, name),
'prefix' : self.get_prefix(step, name)
})
forms.update({name : form(*args, **kwargs)})
return forms
def num_steps(self):
"Helper method that returns the number of steps."
return len(self.forms)
def __call__(self, request, *args, **kwargs):
if kwargs.has_key('extra_context'):
self.extra_context.update(kwargs.get('extra_context'))
current_step = self.determine_step(request, *args, **kwargs)
self.parse_params(request, *args, **kwargs)
# sanity check
if current_step >= self.num_steps():
raise Http404('Step %s does not exist' % current_step)
# For each previous step, verify the hash and process.
# TODO: Move "hash_%d" to a method to make it configurable
for step in range(current_step):
forms = self.get_forms(step, request.POST)
for name, form in forms.iteritems():
form.full_clean()
if request.POST.get('hash_%d_%s' % (step, name), '') != self.security_hash(request, form):
return self.render_hash_failure(request, step)
# Process the current step. If it's valid, go to next step or done()
if request.method == 'POST':
forms = self.get_forms(current_step, request.POST, request.FILES)
else:
forms = self.get_forms(current_step)
invalid = False
for name, form in forms.iteritems():
if not form.is_valid():
invalid = True
if not invalid:
self.process_step(request, forms, current_step)
next_step = current_step + 1
# If this was the last step, validate all of the forms a last time
# for sanity checking, then call done()
steps = self.num_steps()
if next_step == steps:
final_form_list = [] # [ ( step, name, form ) , ... ]
for step in range(steps):
for name in self.forms[step]:
final_form_list.append((
step, name, self.get_forms(step, request.POST, request.FILES).get(name)
))
for i, (step, name, form) in enumerate(final_form_list):
if not form.is_valid():
return self.render_revalidation_failure(request, step)
return self.done(request, final_form_list)
else:
forms = self.get_forms(next_step)
self.step = current_step = next_step
return self.render(forms, request, current_step)
def render(self, bound_forms, request, step, context = {}):
"Renders the given Form object, returning an HttpResponse."
old_data = request.POST
prev_fields = []
if old_data:
hidden = forms.HiddenInput()
# Collect all data from previous steps and render it as HTML hidden fields.
for i in range(step):
old_forms = self.get_forms(i, old_data)
for name, form in old_forms.iteritems():
hash_name = u'hash_%d_%s' % (i, name)
prev_fields.extend(
[field.as_hidden() for field in form]
)
prev_fields.append(
hidden.render(
hash_name,
old_data.get(
hash_name,
self.security_hash(request, form)
)
)
)
return self.render_template(request, bound_forms, ''.join(prev_fields), step, context)
# METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
def render_hash_failure(self, request, step):
"""
Hook for rendering a template if a hash check failed.
step is the step that failed. Any previous step is guaranteed to be
valid.
This default implementation simply renders the form for the given step,
but subclasses may want to display an error message, etc.
"""
return self.render(
self.get_forms(step),
request,
step,
context = dict(
wizard_error = _('We apologize, but your form has expired. Please continue filling out the form from this page.')
)
)
def render_revalidation_failure(self, request, step):
"""
Hook for rendering a template if final revalidation failed.
It is highly unlikely that this point would ever be reached, but See
the comment in __call__() for an explanation.
"""
return self.render(
self.get_forms(step, request.POST, request.FILES), request, step
)
def security_hash(self, request, form):
"""
Calculates the security hash for the given HttpRequest and Form instances.
Subclasses may want to take into account request-specific information,
such as the IP address.
"""
return security_hash(request, form)
def determine_step(self, request, *args, **kwargs):
"""
Given the request object and whatever *args and **kwargs were passed to
__call__(), returns the current step (which is zero-based).
Note that the result should not be trusted. It may even be a completely
invalid number. It's not the job of this method to validate it.
"""
if not request.POST:
return 0
try:
step = int(request.POST.get(self.step_field_name, 0))
except ValueError:
return 0
return step
def parse_params(self, request, *args, **kwargs):
"""
Hook for setting some state, given the request object and whatever
*args and **kwargs were passed to __call__(), sets some state.
This is called at the beginning of __call__().
"""
pass
def get_template(self, step):
"""
Hook for specifying the name of the template to use for a given step.
Note that this can return a tuple of template names if you'd like to
use the template system's select_template() hook.
"""
if not self.template:
raise ValueError, 'Please provide a template'
return self.template
def render_template(self, request, forms, previous_fields, step, context = {}):
"""
Renders the template for the given step, returning an HttpResponse object.
Override this method if you want to add a custom context, return a
different MIME type, etc. If you only need to override the template
name, use get_template() instead.
The template will be rendered with the following context:
step_field -- The name of the hidden field containing the step.
step0 -- The current step (zero-based).
step -- The current step (one-based).
step_count -- The total number of steps.
form -- The Form instance for the current step (either empty
or with errors).
previous_fields -- A string representing every previous data field,
plus hashes for completed forms, all in the form of
hidden fields. Note that you'll need to run this
through the "safe" template filter, to prevent
auto - escaping, because it's raw HTML.
"""
context = context or {}
context.update(self.extra_context)
return render_to_response(self.get_template(step), dict(context,
step_field = self.step_field_name,
step0 = step,
step = step + 1,
step_count = self.num_steps(),
forms = forms,
previous_fields = previous_fields
), context_instance = RequestContext(request))
def process_step(self, request, forms, step):
"""
Hook for modifying the FormWizard's internal state, given a fully
validated Form object. The Form is guaranteed to have clean, valid
data.
This method should * not * modify any of that data. Rather, it might want
to set self.extra_context or dynamically alter self.form_list, based on
previously submitted forms.
Note that this method is called every time a page is rendered for * all *
submitted steps.
"""
pass
# METHODS SUBCLASSES MUST OVERRIDE ########################################
def done(self, request, form_list):
"""
Hook for doing something with the validated data. This is responsible
for the final processing.
form_list is a list of Form instances, each containing clean, valid
data.
"""
raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 8 months ago
Comments
why no subclass of FormWizard?
#
Please login first before commenting.