Added django.contrib.formtools, including the forced-preview application
git-svn-id: http://code.djangoproject.com/svn/django/trunk@4164 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
6c0219cf72
commit
311fadeee0
|
@ -0,0 +1,160 @@
|
|||
"""
|
||||
Formtools Preview application.
|
||||
|
||||
This is an abstraction of the following workflow:
|
||||
|
||||
"Display an HTML form, force a preview, then do something with the submission."
|
||||
|
||||
Given a django.newforms.Form object that you define, this takes care of the
|
||||
following:
|
||||
|
||||
* Displays the form as HTML on a Web page.
|
||||
* Validates the form data once it's submitted via POST.
|
||||
* If it's valid, displays a preview page.
|
||||
* If it's not valid, redisplays the form with error messages.
|
||||
* At the preview page, if the preview confirmation button is pressed, calls
|
||||
a hook that you define -- a done() method.
|
||||
|
||||
The framework enforces the required preview by passing a shared-secret hash to
|
||||
the preview page. If somebody tweaks the form parameters on the preview page,
|
||||
the form submission will fail the hash comparison test.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Subclass FormPreview and define a done() method:
|
||||
|
||||
def done(self, request, clean_data):
|
||||
# ...
|
||||
|
||||
This method takes an HttpRequest object and a dictionary of the form data after
|
||||
it has been validated and cleaned. It should return an HttpResponseRedirect.
|
||||
|
||||
Then, just instantiate your FormPreview subclass by passing it a Form class,
|
||||
and pass that to your URLconf, like so:
|
||||
|
||||
(r'^post/$', MyFormPreview(MyForm)),
|
||||
|
||||
The FormPreview class has a few other hooks. See the docstrings in the source
|
||||
code below.
|
||||
|
||||
The framework also uses two templates: 'formtools/preview.html' and
|
||||
'formtools/form.html'. You can override these by setting 'preview_template' and
|
||||
'form_template' attributes on your FormPreview subclass. See
|
||||
django/contrib/formtools/templates for the default templates.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render_to_response
|
||||
import cPickle as pickle
|
||||
import md5
|
||||
|
||||
AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter.
|
||||
|
||||
class FormPreview(object):
|
||||
preview_template = 'formtools/preview.html'
|
||||
form_template = 'formtools/form.html'
|
||||
|
||||
# METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
|
||||
|
||||
def __init__(self, form):
|
||||
# form should be a Form class, not an instance.
|
||||
self.form, self.state = form, {}
|
||||
|
||||
def __call__(self, request, *args, **kwargs):
|
||||
stage = {'1': 'preview', '2': 'post'}.get(request.POST.get(self.unused_name('stage')), 'preview')
|
||||
self.parse_params(*args, **kwargs)
|
||||
try:
|
||||
method = getattr(self, stage + '_' + request.method.lower())
|
||||
except AttributeError:
|
||||
raise Http404
|
||||
return method(request)
|
||||
|
||||
def unused_name(self, name):
|
||||
"""
|
||||
Given a first-choice name, adds an underscore to the name until it
|
||||
reaches a name that isn't claimed by any field in the form.
|
||||
|
||||
This is calculated rather than being hard-coded so that no field names
|
||||
are off-limits for use in the form.
|
||||
"""
|
||||
while 1:
|
||||
try:
|
||||
f = self.form.fields[name]
|
||||
except KeyError:
|
||||
break # This field name isn't being used by the form.
|
||||
name += '_'
|
||||
return name
|
||||
|
||||
def preview_get(self, request):
|
||||
"Displays the form"
|
||||
f = self.form(auto_id=AUTO_ID)
|
||||
return render_to_response(self.form_template, {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state})
|
||||
|
||||
def preview_post(self, request):
|
||||
"Validates the POST data. If valid, displays the preview page. Else, redisplays form."
|
||||
f = self.form(request.POST, auto_id=AUTO_ID)
|
||||
context = {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}
|
||||
if f.is_valid():
|
||||
context['hash_field'] = self.unused_name('hash')
|
||||
context['hash_value'] = self.security_hash(request, f)
|
||||
return render_to_response(self.preview_template, context)
|
||||
else:
|
||||
return render_to_response(self.form_template, context)
|
||||
|
||||
def post_post(self, request):
|
||||
"Validates the POST data. If valid, calls done(). Else, redisplays form."
|
||||
f = self.form(request.POST, auto_id=AUTO_ID)
|
||||
if f.is_valid():
|
||||
if self.security_hash(request, f) != request.POST.get(self.unused_name('hash')):
|
||||
return self.failed_hash(request) # Security hash failed.
|
||||
return self.done(request, f.clean_data)
|
||||
else:
|
||||
return render_to_response(self.form_template, {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state})
|
||||
|
||||
# METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
|
||||
|
||||
def parse_params(self, *args, **kwargs):
|
||||
"""
|
||||
Given captured args and kwargs from the URLconf, saves something in
|
||||
self.state and/or raises Http404 if necessary.
|
||||
|
||||
For example, this URLconf captures a user_id variable:
|
||||
|
||||
(r'^contact/(?P<user_id>\d{1,6})/$', MyFormPreview(MyForm)),
|
||||
|
||||
In this case, the kwargs variable in parse_params would be
|
||||
{'user_id': 32} for a request to '/contact/32/'. You can use that
|
||||
user_id to make sure it's a valid user and/or save it for later, for
|
||||
use in done().
|
||||
"""
|
||||
pass
|
||||
|
||||
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 failed_hash(self, request):
|
||||
"Returns an HttpResponse in the case of an invalid security hash."
|
||||
return self.preview_post(request)
|
||||
|
||||
# METHODS SUBCLASSES MUST OVERRIDE ########################################
|
||||
|
||||
def done(self, request, clean_data):
|
||||
"Does something with the clean_data and returns an HttpResponseRedirect."
|
||||
raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)
|
|
@ -0,0 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if form.errors %}<h1>Please correct the following errors</h1>{% else %}<h1>Submit</h1>{% endif %}
|
||||
|
||||
<form action="" method="post">
|
||||
<table>
|
||||
{{ form }}
|
||||
</table>
|
||||
<input type="hidden" name="{{ stage_field }}" value="1" />
|
||||
<p><input type="submit" value="Submit" /></p>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,36 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Preview your submission</h1>
|
||||
|
||||
<table>
|
||||
{% for field in form %}
|
||||
<tr>
|
||||
<th>{{ field.verbose_name }}:</th>
|
||||
<td>{{ field.data|escape }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<p>Security hash: {{ hash_value }}</p>
|
||||
|
||||
<form action="" method="post">
|
||||
{% for field in form %}{{ field.as_hidden }}
|
||||
{% endfor %}
|
||||
<input type="hidden" name="{{ stage_field }}" value="2" />
|
||||
<input type="hidden" name="{{ hash_field }}" value="{{ hash_value }}" />
|
||||
<p><input type="submit" value="Submit" /></p>
|
||||
</form>
|
||||
|
||||
<h1>Or edit it again</h1>
|
||||
|
||||
<form action="" method="post">
|
||||
<table>
|
||||
{{ form }}
|
||||
</table>
|
||||
<input type="hidden" name="{{ stage_field }}" value="1" />
|
||||
<p><input type="submit" value="Submit changes" /></p>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -48,6 +48,23 @@ See the `csrf documentation`_.
|
|||
|
||||
.. _csrf documentation: http://www.djangoproject.com/documentation/csrf/
|
||||
|
||||
formtools
|
||||
=========
|
||||
|
||||
**New in Django development version**
|
||||
|
||||
A set of high-level abstractions for Django forms (django.newforms).
|
||||
|
||||
django.contrib.formtools.preview
|
||||
--------------------------------
|
||||
|
||||
An abstraction of the following workflow:
|
||||
|
||||
"Display an HTML form, force a preview, then do something with the submission."
|
||||
|
||||
Full documentation for this feature does not yet exist, but you can read the
|
||||
code and docstrings in ``django/contrib/formtools/preview.py`` for a start.
|
||||
|
||||
humanize
|
||||
========
|
||||
|
||||
|
|
Loading…
Reference in New Issue