Skip to content

Commit 9520961

Browse files
committed
Introduce contrib/ to hold form wizard tool from django's trac.
Move form_decorator help function from utils.py to contrib/ - Legacy-Id: 121
1 parent 73db42d commit 9520961

4 files changed

Lines changed: 272 additions & 71 deletions

File tree

ietf/contrib/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from form_decorator import *

ietf/contrib/form_decorator.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
## formfield_callback generator
2+
## http://www.djangosnippets.org/snippets/59/
3+
def form_decorator(fields = {}, attrs = {}, widgets = {},
4+
labels = {}, choices = {}):
5+
6+
"""
7+
This function helps to add overrides when creating forms from models/instances.
8+
Pass in dictionary of fields to override certain fields altogether, otherwise
9+
add widgets or labels as desired.
10+
11+
For example:
12+
13+
class Project(models.Model):
14+
15+
name = models.CharField(maxlength = 100)
16+
description = models.TextField()
17+
owner = models.ForeignKey(User)
18+
19+
project_fields = dict(
20+
owner = None
21+
)
22+
23+
project_widgets = dict(
24+
name = forms.TextInput({"size":40}),
25+
description = forms.Textarea({"rows":5, "cols":40}))
26+
27+
project_labels = dict(
28+
name = "Enter your project name here"
29+
)
30+
31+
callback = form_decorator(project_fields, project_widgets, project_labels)
32+
project_form = forms.form_for_model(Project, formfield_callback = callback)
33+
34+
This saves having to redefine whole fields for example just to change a widget
35+
setting or label.
36+
"""
37+
38+
def formfields_callback(f, **kw):
39+
40+
if f.name in fields:
41+
42+
# replace field altogether
43+
field = fields[f.name]
44+
f.initial = kw.pop("initial", None)
45+
return field
46+
47+
if f.name in widgets:
48+
49+
kw["widget"] = widgets[f.name]
50+
51+
if f.name in attrs:
52+
53+
widget = kw.pop("widget", f.formfield().widget)
54+
if widget :
55+
widget.attrs.update(attrs[f.name])
56+
kw["widget"] = widget
57+
58+
if f.name in labels:
59+
60+
kw["label"] = labels[f.name]
61+
62+
if f.name in choices:
63+
64+
choice_set = choices[f.name]
65+
if callable(choice_set) : choice_set = choice_set()
66+
kw["choices"] = choice_set
67+
68+
69+
return f.formfield(**kw)
70+
71+
return formfields_callback
72+

ietf/contrib/wizard.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
##
2+
## django form wizard
3+
## http://code.djangoproject.com/ticket/3218
4+
"""
5+
TODO:
6+
!!!! documentation !!!! including examples
7+
8+
USAGE:
9+
urls: (replace wizard.Wizard with something that overrides its done() method,
10+
othervise it will complain at the end that __call__ does not return HttpResponse)
11+
12+
( r'^$', MyWizard( [MyForm, MyForm, MyForm] ) ),
13+
14+
template:
15+
<form action="." method="POST">
16+
FORM( {{ step }} ): {{ form }}
17+
18+
step_info : <input type="hidden" name="{{ step_field }}" value="{{ step }}" />
19+
20+
previous_fields: {{ previous_fields }}
21+
22+
<input type="submit">
23+
</form>
24+
25+
"""
26+
from django.conf import settings
27+
from django.http import Http404
28+
from django.shortcuts import render_to_response
29+
from django.template.context import RequestContext
30+
31+
from django import newforms as forms
32+
import cPickle as pickle
33+
import md5
34+
35+
class Wizard( object ):
36+
PREFIX="%d"
37+
STEP_FIELD="wizard_step"
38+
39+
# METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
40+
def __init__( self, form_list, initial=None ):
41+
" Pass list of Form classes (not instances !) "
42+
self.form_list = form_list[:]
43+
self.initial = initial or {}
44+
45+
def __repr__( self ):
46+
return "step: %d\nform_list: %s\ninitial_data: %s" % ( self.step, self.form_list, self.initial )
47+
48+
def get_form( self, step, data=None ):
49+
" Shortcut to return form instance. "
50+
return self.form_list[step]( data, prefix=self.PREFIX % step, initial=self.initial.get( step, None ) )
51+
52+
def __call__( self, request, *args, **kwargs ):
53+
"""
54+
Main function that does all the hard work:
55+
- initializes the wizard object (via parse_params())
56+
- veryfies (using security_hash()) that noone has tempered with the data since we last saw them
57+
calls failed_hash() if it is so
58+
calls process_step() for every previously submitted form
59+
- validates current form and
60+
returns it again if errors were found
61+
returns done() if it was the last form
62+
returns next form otherwise
63+
"""
64+
# add extra_context, we don't care if somebody overrides it, as long as it remains a dict
65+
self.extra_context = kwargs.get( 'extra_context', {} )
66+
67+
self.parse_params( request, *args, **kwargs )
68+
69+
# we only accept POST method for form delivery no POST, no data
70+
if not request.POST:
71+
self.step = 0
72+
return self.render( self.get_form( 0 ), request )
73+
74+
# verify old steps' hashes
75+
for i in range( self.step ):
76+
form = self.get_form( i, request.POST )
77+
# somebody is trying to corrupt our data
78+
if request.POST.get( "hash_%d" % i, '' ) != self.security_hash( request, form ):
79+
# revert to the corrupted step
80+
return self.failed_hash( request, i )
81+
self.process_step( request, form, i )
82+
83+
# process current step
84+
form = self.get_form( self.step, request.POST )
85+
if form.is_valid():
86+
self.process_step( request, form, self.step )
87+
self.step += 1
88+
# this was the last step
89+
if self.step == len( self.form_list ):
90+
return self.done( request, [ self.get_form( i, request.POST ) for i in range( len( self.form_list ) ) ] )
91+
form = self.get_form( self.step )
92+
return self.render( form, request )
93+
94+
def render( self, form, request ):
95+
"""
96+
Prepare the form and call the render_template() method to do tha actual rendering.
97+
"""
98+
if self.step >= len( self.form_list ):
99+
raise Http404
100+
101+
old_data = request.POST
102+
prev_fields = ''
103+
if old_data:
104+
# old data
105+
prev_fields = '\n'.join(
106+
bf.as_hidden() for i in range(self.step) for bf in self.get_form( i, old_data )
107+
)
108+
# hashes for old forms
109+
hidden = forms.widgets.HiddenInput()
110+
prev_fields += '\n'.join(
111+
hidden.render( "hash_%d" % i, old_data.get( "hash_%d" % i, self.security_hash( request, self.get_form( i, old_data ) ) ) )
112+
for i in range( self.step)
113+
)
114+
return self.render_template( request, form, prev_fields )
115+
116+
117+
# METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
118+
119+
def failed_hash( self, request, i ):
120+
"""
121+
One of the hashes verifying old data doesn't match.
122+
"""
123+
self.step = i
124+
return self.render( self.get_form(self.step), request )
125+
126+
def security_hash(self, request, form):
127+
"""
128+
Calculates the security hash for the given Form instance.
129+
130+
This creates a list of the form field names/values in a deterministic
131+
order, pickles the result with the SECRET_KEY setting and takes an md5
132+
hash of that.
133+
134+
Subclasses may want to take into account request-specific information
135+
such as the IP address.
136+
"""
137+
data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY]
138+
# Use HIGHEST_PROTOCOL because it's the most efficient. It requires
139+
# Python 2.3, but Django requires 2.3 anyway, so that's OK.
140+
pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
141+
return md5.new(pickled).hexdigest()
142+
143+
def parse_params( self, request, *args, **kwargs ):
144+
"""
145+
Set self.step, process any additional info from parameters and/or form data
146+
"""
147+
if request.POST:
148+
self.step = int( request.POST.get( self.STEP_FIELD, 0 ) )
149+
else:
150+
self.step = 0
151+
152+
def get_template( self ):
153+
"""
154+
Return name of the template to be rendered, use self.step to get the step number.
155+
"""
156+
return "wizard.html"
157+
158+
def render_template( self, request, form, previous_fields ):
159+
"""
160+
Render template for current step, override this method if you wish to add custom context, return a different mimetype etc.
161+
162+
If you only wish to override the template name, use get_template
163+
164+
Some additional items are added to the context:
165+
'step_field' is the name of the hidden field containing step
166+
'step' holds the current step
167+
'form' containing the current form to be processed (either empty or with errors)
168+
'previous_data' contains all the addtitional information, including
169+
hashes for finished forms and old data in form of hidden fields
170+
any additional data stored in self.extra_context
171+
"""
172+
return render_to_response( self.get_template(), dict(
173+
step_field=self.STEP_FIELD,
174+
step=self.step,
175+
form=form,
176+
previous_fields=previous_fields,
177+
** self.extra_context
178+
), context_instance=RequestContext( request ) )
179+
180+
def process_step( self, request, form, step ):
181+
"""
182+
This should not modify any data, it is only a hook to modify wizard's internal state
183+
(such as dynamically generating form_list based on previously submited forms).
184+
It can also be used to add items to self.extra_context base on the contents of previously submitted forms.
185+
186+
Note that this method is called every time a page is rendered for ALL submitted steps.
187+
188+
Only valid data enter here.
189+
"""
190+
pass
191+
192+
# METHODS SUBCLASSES MUST OVERRIDE ########################################
193+
194+
def done( self, request, form_list ):
195+
"""
196+
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
197+
"""
198+
raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)
199+

ietf/utils.py

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,77 +3,6 @@
33
from django.utils.html import escape
44
# look at snippets 59, 148, 99 for newforms helpers
55

6-
# http://www.djangosnippets.org/snippets/59/
7-
def form_decorator(fields = {}, attrs = {}, widgets = {},
8-
labels = {}, choices = {}):
9-
10-
"""
11-
This function helps to add overrides when creating forms from models/instances.
12-
Pass in dictionary of fields to override certain fields altogether, otherwise
13-
add widgets or labels as desired.
14-
15-
For example:
16-
17-
class Project(models.Model):
18-
19-
name = models.CharField(maxlength = 100)
20-
description = models.TextField()
21-
owner = models.ForeignKey(User)
22-
23-
project_fields = dict(
24-
owner = None
25-
)
26-
27-
project_widgets = dict(
28-
name = forms.TextInput({"size":40}),
29-
description = forms.Textarea({"rows":5, "cols":40}))
30-
31-
project_labels = dict(
32-
name = "Enter your project name here"
33-
)
34-
35-
callback = form_decorator(project_fields, project_widgets, project_labels)
36-
project_form = forms.form_for_model(Project, formfield_callback = callback)
37-
38-
This saves having to redefine whole fields for example just to change a widget
39-
setting or label.
40-
"""
41-
42-
def formfields_callback(f, **kw):
43-
44-
if f.name in fields:
45-
46-
# replace field altogether
47-
field = fields[f.name]
48-
f.initial = kw.pop("initial", None)
49-
return field
50-
51-
if f.name in widgets:
52-
53-
kw["widget"] = widgets[f.name]
54-
55-
if f.name in attrs:
56-
57-
widget = kw.pop("widget", f.formfield().widget)
58-
if widget :
59-
widget.attrs.update(attrs[f.name])
60-
kw["widget"] = widget
61-
62-
if f.name in labels:
63-
64-
kw["label"] = labels[f.name]
65-
66-
if f.name in choices:
67-
68-
choice_set = choices[f.name]
69-
if callable(choice_set) : choice_set = choice_set()
70-
kw["choices"] = choice_set
71-
72-
73-
return f.formfield(**kw)
74-
75-
return formfields_callback
76-
776

787
# Caching accessor for the reverse of a ForeignKey relatinoship
798
# Started by axiak on #django

0 commit comments

Comments
 (0)