Introduce contrib/ to hold form wizard tool from django's trac.
Move form_decorator help function from utils.py to contrib/ - Legacy-Id: 121
This commit is contained in:
parent
73db42db56
commit
95209614b5
1
ietf/contrib/__init__.py
Normal file
1
ietf/contrib/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from form_decorator import *
|
72
ietf/contrib/form_decorator.py
Normal file
72
ietf/contrib/form_decorator.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
## formfield_callback generator
|
||||||
|
## http://www.djangosnippets.org/snippets/59/
|
||||||
|
def form_decorator(fields = {}, attrs = {}, widgets = {},
|
||||||
|
labels = {}, choices = {}):
|
||||||
|
|
||||||
|
"""
|
||||||
|
This function helps to add overrides when creating forms from models/instances.
|
||||||
|
Pass in dictionary of fields to override certain fields altogether, otherwise
|
||||||
|
add widgets or labels as desired.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
class Project(models.Model):
|
||||||
|
|
||||||
|
name = models.CharField(maxlength = 100)
|
||||||
|
description = models.TextField()
|
||||||
|
owner = models.ForeignKey(User)
|
||||||
|
|
||||||
|
project_fields = dict(
|
||||||
|
owner = None
|
||||||
|
)
|
||||||
|
|
||||||
|
project_widgets = dict(
|
||||||
|
name = forms.TextInput({"size":40}),
|
||||||
|
description = forms.Textarea({"rows":5, "cols":40}))
|
||||||
|
|
||||||
|
project_labels = dict(
|
||||||
|
name = "Enter your project name here"
|
||||||
|
)
|
||||||
|
|
||||||
|
callback = form_decorator(project_fields, project_widgets, project_labels)
|
||||||
|
project_form = forms.form_for_model(Project, formfield_callback = callback)
|
||||||
|
|
||||||
|
This saves having to redefine whole fields for example just to change a widget
|
||||||
|
setting or label.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def formfields_callback(f, **kw):
|
||||||
|
|
||||||
|
if f.name in fields:
|
||||||
|
|
||||||
|
# replace field altogether
|
||||||
|
field = fields[f.name]
|
||||||
|
f.initial = kw.pop("initial", None)
|
||||||
|
return field
|
||||||
|
|
||||||
|
if f.name in widgets:
|
||||||
|
|
||||||
|
kw["widget"] = widgets[f.name]
|
||||||
|
|
||||||
|
if f.name in attrs:
|
||||||
|
|
||||||
|
widget = kw.pop("widget", f.formfield().widget)
|
||||||
|
if widget :
|
||||||
|
widget.attrs.update(attrs[f.name])
|
||||||
|
kw["widget"] = widget
|
||||||
|
|
||||||
|
if f.name in labels:
|
||||||
|
|
||||||
|
kw["label"] = labels[f.name]
|
||||||
|
|
||||||
|
if f.name in choices:
|
||||||
|
|
||||||
|
choice_set = choices[f.name]
|
||||||
|
if callable(choice_set) : choice_set = choice_set()
|
||||||
|
kw["choices"] = choice_set
|
||||||
|
|
||||||
|
|
||||||
|
return f.formfield(**kw)
|
||||||
|
|
||||||
|
return formfields_callback
|
||||||
|
|
199
ietf/contrib/wizard.py
Normal file
199
ietf/contrib/wizard.py
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
##
|
||||||
|
## django form wizard
|
||||||
|
## http://code.djangoproject.com/ticket/3218
|
||||||
|
"""
|
||||||
|
TODO:
|
||||||
|
!!!! documentation !!!! including examples
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
urls: (replace wizard.Wizard with something that overrides its done() method,
|
||||||
|
othervise it will complain at the end that __call__ does not return HttpResponse)
|
||||||
|
|
||||||
|
( r'^$', MyWizard( [MyForm, MyForm, MyForm] ) ),
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
"""
|
||||||
|
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__)
|
||||||
|
|
|
@ -3,77 +3,6 @@ import syslog
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
# look at snippets 59, 148, 99 for newforms helpers
|
# look at snippets 59, 148, 99 for newforms helpers
|
||||||
|
|
||||||
# http://www.djangosnippets.org/snippets/59/
|
|
||||||
def form_decorator(fields = {}, attrs = {}, widgets = {},
|
|
||||||
labels = {}, choices = {}):
|
|
||||||
|
|
||||||
"""
|
|
||||||
This function helps to add overrides when creating forms from models/instances.
|
|
||||||
Pass in dictionary of fields to override certain fields altogether, otherwise
|
|
||||||
add widgets or labels as desired.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
class Project(models.Model):
|
|
||||||
|
|
||||||
name = models.CharField(maxlength = 100)
|
|
||||||
description = models.TextField()
|
|
||||||
owner = models.ForeignKey(User)
|
|
||||||
|
|
||||||
project_fields = dict(
|
|
||||||
owner = None
|
|
||||||
)
|
|
||||||
|
|
||||||
project_widgets = dict(
|
|
||||||
name = forms.TextInput({"size":40}),
|
|
||||||
description = forms.Textarea({"rows":5, "cols":40}))
|
|
||||||
|
|
||||||
project_labels = dict(
|
|
||||||
name = "Enter your project name here"
|
|
||||||
)
|
|
||||||
|
|
||||||
callback = form_decorator(project_fields, project_widgets, project_labels)
|
|
||||||
project_form = forms.form_for_model(Project, formfield_callback = callback)
|
|
||||||
|
|
||||||
This saves having to redefine whole fields for example just to change a widget
|
|
||||||
setting or label.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def formfields_callback(f, **kw):
|
|
||||||
|
|
||||||
if f.name in fields:
|
|
||||||
|
|
||||||
# replace field altogether
|
|
||||||
field = fields[f.name]
|
|
||||||
f.initial = kw.pop("initial", None)
|
|
||||||
return field
|
|
||||||
|
|
||||||
if f.name in widgets:
|
|
||||||
|
|
||||||
kw["widget"] = widgets[f.name]
|
|
||||||
|
|
||||||
if f.name in attrs:
|
|
||||||
|
|
||||||
widget = kw.pop("widget", f.formfield().widget)
|
|
||||||
if widget :
|
|
||||||
widget.attrs.update(attrs[f.name])
|
|
||||||
kw["widget"] = widget
|
|
||||||
|
|
||||||
if f.name in labels:
|
|
||||||
|
|
||||||
kw["label"] = labels[f.name]
|
|
||||||
|
|
||||||
if f.name in choices:
|
|
||||||
|
|
||||||
choice_set = choices[f.name]
|
|
||||||
if callable(choice_set) : choice_set = choice_set()
|
|
||||||
kw["choices"] = choice_set
|
|
||||||
|
|
||||||
|
|
||||||
return f.formfield(**kw)
|
|
||||||
|
|
||||||
return formfields_callback
|
|
||||||
|
|
||||||
|
|
||||||
# Caching accessor for the reverse of a ForeignKey relatinoship
|
# Caching accessor for the reverse of a ForeignKey relatinoship
|
||||||
# Started by axiak on #django
|
# Started by axiak on #django
|
||||||
|
|
Loading…
Reference in a new issue