Wizard class - subclass this, supply a done() method and then you can use MyWizard( [list, of, newforms, classes])
as a view function.
see #3218
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 | """
USAGE:
views:
class MyWizard( Wizard ):
def done( self, request, form_list ):
return render_to_response('done.html', {'form_data' : [ form.clean_data for form in form_list ] })
urls:
( r'^$', MyWizard( [MyForm, MyForm, MyForm] ) ),
form template:
<form action="." method="POST">
FORM( {{ step }} ): {{ form }}
step_info : <input type="hidden" name="{{ step_field }}" value="{{ step }}" />
previous_fields: {{ previous_fields }}
<input type="submit">
</form>
done.html:
{% for data in form_data %}
{% for item in data.items %}
{{ item.0 }} : {{ item.1 }}<br />
{% endfor %}
{% endfor %}
"""
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 import newforms as forms
import cPickle as pickle
import md5
class Wizard( object ):
PREFIX="%d"
STEP_FIELD="wizard_step"
# METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
def __init__( self, form_list, initial=None ):
" Pass list of Form classes (not instances !) "
self.form_list = form_list[:]
self.initial = initial or {}
def __repr__( self ):
return "step: %d\nform_list: %s\ninitial_data: %s" % ( self.step, self.form_list, self.initial )
def get_form( self, step, data=None ):
" Shortcut to return form instance. "
return self.form_list[step]( data, prefix=self.PREFIX % step, initial=self.initial.get( step, None ) )
def __call__( self, request, *args, **kwargs ):
"""
Main function that does all the hard work:
- initializes the wizard object (via parse_params())
- veryfies (using security_hash()) that noone has tempered with the data since we last saw them
calls failed_hash() if it is so
calls process_step() for every previously submitted form
- validates current form and
returns it again if errors were found
returns done() if it was the last form
returns next form otherwise
"""
# add extra_context, we don't care if somebody overrides it, as long as it remains a dict
self.extra_context = kwargs.get( 'extra_context', {} )
self.parse_params( request, *args, **kwargs )
# we only accept POST method for form delivery no POST, no data
if not request.POST:
self.step = 0
return self.render( self.get_form( 0 ), request )
# verify old steps' hashes
for i in range( self.step ):
form = self.get_form( i, request.POST )
# somebody is trying to corrupt our data
if request.POST.get( "hash_%d" % i, '' ) != self.security_hash( request, form ):
# revert to the corrupted step
return self.failed_hash( request, i )
self.process_step( request, form, i )
# process current step
form = self.get_form( self.step, request.POST )
if form.is_valid():
self.process_step( request, form, self.step )
self.step += 1
# this was the last step
if self.step == len( self.form_list ):
return self.done( request, [ self.get_form( i, request.POST ) for i in range( len( self.form_list ) ) ] )
form = self.get_form( self.step )
return self.render( form, request )
def render( self, form, request ):
"""
Prepare the form and call the render_template() method to do tha actual rendering.
"""
if self.step >= len( self.form_list ):
raise Http404
old_data = request.POST
prev_fields = ''
if old_data:
# old data
prev_fields = '\n'.join(
bf.as_hidden() for i in range(self.step) for bf in self.get_form( i, old_data )
)
# hashes for old forms
hidden = forms.widgets.HiddenInput()
prev_fields += '\n'.join(
hidden.render( "hash_%d" % i, old_data.get( "hash_%d" % i, self.security_hash( request, self.get_form( i, old_data ) ) ) )
for i in range( self.step)
)
return self.render_template( request, form, prev_fields )
# METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
def failed_hash( self, request, i ):
"""
One of the hashes verifying old data doesn't match.
"""
self.step = i
return self.render( self.get_form(self.step), request )
def security_hash(self, request, form):
"""
Calculates the security hash for the given Form instance.
This creates a list of the form field names/values in a deterministic
order, pickles the result with the SECRET_KEY setting and takes an md5
hash of that.
Subclasses may want to take into account request-specific information
such as the IP address.
"""
data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY]
# Use HIGHEST_PROTOCOL because it's the most efficient. It requires
# Python 2.3, but Django requires 2.3 anyway, so that's OK.
pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
return md5.new(pickled).hexdigest()
def parse_params( self, request, *args, **kwargs ):
"""
Set self.step, process any additional info from parameters and/or form data
"""
if request.POST:
self.step = int( request.POST.get( self.STEP_FIELD, 0 ) )
else:
self.step = 0
def get_template( self ):
"""
Return name of the template to be rendered, use self.step to get the step number.
"""
return "wizard.html"
def render_template( self, request, form, previous_fields ):
"""
Render template for current step, override this method if you wish to add custom context, return a different mimetype etc.
If you only wish to override the template name, use get_template
Some additional items are added to the context:
'step_field' is the name of the hidden field containing step
'step' holds the current step
'form' containing the current form to be processed (either empty or with errors)
'previous_data' contains all the addtitional information, including
hashes for finished forms and old data in form of hidden fields
any additional data stored in self.extra_context
"""
return render_to_response( self.get_template(), dict(
step_field=self.STEP_FIELD,
step=self.step,
form=form,
previous_fields=previous_fields,
** self.extra_context
), context_instance=RequestContext( request ) )
def process_step( self, request, form, step ):
"""
This should not modify any data, it is only a hook to modify wizard's internal state
(such as dynamically generating form_list based on previously submited forms).
It can also be used to add items to self.extra_context base on the contents of previously submitted forms.
Note that this method is called every time a page is rendered for ALL submitted steps.
Only valid data enter here.
"""
pass
# METHODS SUBCLASSES MUST OVERRIDE ########################################
def done( self, request, form_list ):
"""
this method must be overriden, it is responsible for the end processing - it will be called with instances of all form_list with their data
"""
raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 9 months, 3 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months ago
- Serializer factory with Django Rest Framework by julio 1 year, 4 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 5 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
Very nice! Thanks for sharing!
A note on usage: If using the latest release of Django, line 6 should be changed so that form.clean_data reads form.cleaned_data.
Ciao!
Branton
#
Is this what became the FormWizard that is part of Django 1.02? Sure seems like it.
#
Please login first before commenting.