Fixed #6209: handle `BooleanField`s in `FormPreview` and `FormWizard`. In the process, broke the the security hash calculation out to a helper function. Thanks to mcroydon and rajeshdhawan.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8597 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jacob Kaplan-Moss 2008-08-26 20:19:12 +00:00
parent a0329d0ae3
commit 6056ab1bee
4 changed files with 73 additions and 32 deletions

View File

@ -9,6 +9,7 @@ from django.http import Http404
from django.shortcuts import render_to_response from django.shortcuts import render_to_response
from django.template.context import RequestContext from django.template.context import RequestContext
from django.utils.hashcompat import md5_constructor from django.utils.hashcompat import md5_constructor
from django.contrib.formtools.utils import security_hash
AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter. AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter.
@ -97,20 +98,12 @@ class FormPreview(object):
def security_hash(self, request, form): def security_hash(self, request, form):
""" """
Calculates the security hash for the given Form instance. Calculates the security hash for the given HttpRequest and Form instances.
This creates a list of the form field names/values in a deterministic Subclasses may want to take into account request-specific information,
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. such as the IP address.
""" """
data = [(bf.name, bf.data or '') for bf in form] + [settings.SECRET_KEY] return security_hash(request, form)
# 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, pickle.HIGHEST_PROTOCOL)
return md5_constructor(pickled).hexdigest()
def failed_hash(self, request): def failed_hash(self, request):
"Returns an HttpResponse in the case of an invalid security hash." "Returns an HttpResponse in the case of an invalid security hash."

View File

@ -4,20 +4,16 @@ from django import http
from django.test import TestCase from django.test import TestCase
success_string = "Done was called!" success_string = "Done was called!"
test_data = {'field1': u'foo',
'field1_': u'asdf'}
class TestFormPreview(preview.FormPreview): class TestFormPreview(preview.FormPreview):
def done(self, request, cleaned_data): def done(self, request, cleaned_data):
return http.HttpResponse(success_string) return http.HttpResponse(success_string)
class TestForm(forms.Form): class TestForm(forms.Form):
field1 = forms.CharField() field1 = forms.CharField()
field1_ = forms.CharField() field1_ = forms.CharField()
bool1 = forms.BooleanField(required=False)
class PreviewTests(TestCase): class PreviewTests(TestCase):
urls = 'django.contrib.formtools.test_urls' urls = 'django.contrib.formtools.test_urls'
@ -27,6 +23,7 @@ class PreviewTests(TestCase):
self.preview = preview.FormPreview(TestForm) self.preview = preview.FormPreview(TestForm)
input_template = '<input type="hidden" name="%s" value="%s" />' input_template = '<input type="hidden" name="%s" value="%s" />'
self.input = input_template % (self.preview.unused_name('stage'), "%d") self.input = input_template % (self.preview.unused_name('stage'), "%d")
self.test_data = {'field1':u'foo', 'field1_':u'asdf'}
def test_unused_name(self): def test_unused_name(self):
""" """
@ -59,8 +56,8 @@ class PreviewTests(TestCase):
""" """
# Pass strings for form submittal and add stage variable to # Pass strings for form submittal and add stage variable to
# show we previously saw first stage of the form. # show we previously saw first stage of the form.
test_data.update({'stage': 1}) self.test_data.update({'stage': 1})
response = self.client.post('/test1/', test_data) response = self.client.post('/test1/', self.test_data)
# Check to confirm stage is set to 2 in output form. # Check to confirm stage is set to 2 in output form.
stage = self.input % 2 stage = self.input % 2
self.assertContains(response, stage, 1) self.assertContains(response, stage, 1)
@ -77,11 +74,30 @@ class PreviewTests(TestCase):
""" """
# Pass strings for form submittal and add stage variable to # Pass strings for form submittal and add stage variable to
# show we previously saw first stage of the form. # show we previously saw first stage of the form.
test_data.update({'stage': 2}) self.test_data.update({'stage':2})
response = self.client.post('/test1/', test_data) response = self.client.post('/test1/', self.test_data)
self.failIfEqual(response.content, success_string) self.failIfEqual(response.content, success_string)
hash = self.preview.security_hash(None, TestForm(test_data)) hash = self.preview.security_hash(None, TestForm(self.test_data))
test_data.update({'hash': hash}) self.test_data.update({'hash': hash})
response = self.client.post('/test1/', test_data) response = self.client.post('/test1/', self.test_data)
self.assertEqual(response.content, success_string)
def test_bool_submit(self):
"""
Test contrib.formtools.preview form submittal when form contains:
BooleanField(required=False)
Ticket: #6209 - When an unchecked BooleanField is previewed, the preview
form's hash would be computed with no value for ``bool1``. However, when
the preview form is rendered, the unchecked hidden BooleanField would be
rendered with the string value 'False'. So when the preview form is
resubmitted, the hash would be computed with the value 'False' for
``bool1``. We need to make sure the hashes are the same in both cases.
"""
self.test_data.update({'stage':2})
hash = self.preview.security_hash(None, TestForm(self.test_data))
self.test_data.update({'hash':hash, 'bool1':u'False'})
response = self.client.post('/test1/', self.test_data)
self.assertEqual(response.content, success_string) self.assertEqual(response.content, success_string)

View File

@ -0,0 +1,39 @@
try:
import cPickle as pickle
except ImportError:
import pickle
from django.conf import settings
from django.utils.hashcompat import md5_constructor
from django.forms import BooleanField
def security_hash(request, form, *args):
"""
Calculates a 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, then takes an md5
hash of that.
"""
# Ensure that the hash does not change when a BooleanField's bound
# data is a string `False' or a boolean False.
# Rather than re-coding this special behaviour here, we
# create a dummy BooleanField and call its clean method to get a
# boolean True or False verdict that is consistent with
# BooleanField.clean()
dummy_bool = BooleanField(required=False)
def _cleaned_data(bf):
if isinstance(bf.field, BooleanField):
return dummy_bool.clean(bf.data)
return bf.data
data = [(bf.name, _cleaned_data(bf) or '') for bf in form]
data.extend(args)
data.append(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, pickle.HIGHEST_PROTOCOL)
return md5_constructor(pickled).hexdigest()

View File

@ -12,6 +12,7 @@ from django.http import Http404
from django.shortcuts import render_to_response from django.shortcuts import render_to_response
from django.template.context import RequestContext from django.template.context import RequestContext
from django.utils.hashcompat import md5_constructor from django.utils.hashcompat import md5_constructor
from django.contrib.formtools.utils import security_hash
class FormWizard(object): class FormWizard(object):
# Dictionary of extra template context variables. # Dictionary of extra template context variables.
@ -140,18 +141,10 @@ class FormWizard(object):
""" """
Calculates the security hash for the given HttpRequest and Form instances. Calculates the security hash for the given HttpRequest and Form instances.
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, Subclasses may want to take into account request-specific information,
such as the IP address. such as the IP address.
""" """
data = [(bf.name, bf.data or '') for bf in form] + [settings.SECRET_KEY] return security_hash(request, form)
# 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, pickle.HIGHEST_PROTOCOL)
return md5_constructor(pickled).hexdigest()
def determine_step(self, request, *args, **kwargs): def determine_step(self, request, *args, **kwargs):
""" """