| 1 |
""" |
|---|
| 2 |
Formtools Preview application. |
|---|
| 3 |
""" |
|---|
| 4 |
|
|---|
| 5 |
from django.conf import settings |
|---|
| 6 |
from django.http import Http404 |
|---|
| 7 |
from django.shortcuts import render_to_response |
|---|
| 8 |
from django.template.context import RequestContext |
|---|
| 9 |
import cPickle as pickle |
|---|
| 10 |
import md5 |
|---|
| 11 |
|
|---|
| 12 |
AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter. |
|---|
| 13 |
|
|---|
| 14 |
class FormPreview(object): |
|---|
| 15 |
preview_template = 'formtools/preview.html' |
|---|
| 16 |
form_template = 'formtools/form.html' |
|---|
| 17 |
|
|---|
| 18 |
# METHODS SUBCLASSES SHOULDN'T OVERRIDE ################################### |
|---|
| 19 |
|
|---|
| 20 |
def __init__(self, form): |
|---|
| 21 |
# form should be a Form class, not an instance. |
|---|
| 22 |
self.form, self.state = form, {} |
|---|
| 23 |
|
|---|
| 24 |
def __call__(self, request, *args, **kwargs): |
|---|
| 25 |
stage = {'1': 'preview', '2': 'post'}.get(request.POST.get(self.unused_name('stage')), 'preview') |
|---|
| 26 |
self.parse_params(*args, **kwargs) |
|---|
| 27 |
try: |
|---|
| 28 |
method = getattr(self, stage + '_' + request.method.lower()) |
|---|
| 29 |
except AttributeError: |
|---|
| 30 |
raise Http404 |
|---|
| 31 |
return method(request) |
|---|
| 32 |
|
|---|
| 33 |
def unused_name(self, name): |
|---|
| 34 |
""" |
|---|
| 35 |
Given a first-choice name, adds an underscore to the name until it |
|---|
| 36 |
reaches a name that isn't claimed by any field in the form. |
|---|
| 37 |
|
|---|
| 38 |
This is calculated rather than being hard-coded so that no field names |
|---|
| 39 |
are off-limits for use in the form. |
|---|
| 40 |
""" |
|---|
| 41 |
while 1: |
|---|
| 42 |
try: |
|---|
| 43 |
f = self.form.base_fields[name] |
|---|
| 44 |
except KeyError: |
|---|
| 45 |
break # This field name isn't being used by the form. |
|---|
| 46 |
name += '_' |
|---|
| 47 |
return name |
|---|
| 48 |
|
|---|
| 49 |
def preview_get(self, request): |
|---|
| 50 |
"Displays the form" |
|---|
| 51 |
f = self.form(auto_id=AUTO_ID) |
|---|
| 52 |
return render_to_response(self.form_template, |
|---|
| 53 |
{'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}, |
|---|
| 54 |
context_instance=RequestContext(request)) |
|---|
| 55 |
|
|---|
| 56 |
def preview_post(self, request): |
|---|
| 57 |
"Validates the POST data. If valid, displays the preview page. Else, redisplays form." |
|---|
| 58 |
f = self.form(request.POST, auto_id=AUTO_ID) |
|---|
| 59 |
context = {'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state} |
|---|
| 60 |
if f.is_valid(): |
|---|
| 61 |
context['hash_field'] = self.unused_name('hash') |
|---|
| 62 |
context['hash_value'] = self.security_hash(request, f) |
|---|
| 63 |
return render_to_response(self.preview_template, context, context_instance=RequestContext(request)) |
|---|
| 64 |
else: |
|---|
| 65 |
return render_to_response(self.form_template, context, context_instance=RequestContext(request)) |
|---|
| 66 |
|
|---|
| 67 |
def post_post(self, request): |
|---|
| 68 |
"Validates the POST data. If valid, calls done(). Else, redisplays form." |
|---|
| 69 |
f = self.form(request.POST, auto_id=AUTO_ID) |
|---|
| 70 |
if f.is_valid(): |
|---|
| 71 |
if self.security_hash(request, f) != request.POST.get(self.unused_name('hash')): |
|---|
| 72 |
return self.failed_hash(request) # Security hash failed. |
|---|
| 73 |
return self.done(request, f.cleaned_data) |
|---|
| 74 |
else: |
|---|
| 75 |
return render_to_response(self.form_template, |
|---|
| 76 |
{'form': f, 'stage_field': self.unused_name('stage'), 'state': self.state}, |
|---|
| 77 |
context_instance=RequestContext(request)) |
|---|
| 78 |
|
|---|
| 79 |
# METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ######################## |
|---|
| 80 |
|
|---|
| 81 |
def parse_params(self, *args, **kwargs): |
|---|
| 82 |
""" |
|---|
| 83 |
Given captured args and kwargs from the URLconf, saves something in |
|---|
| 84 |
self.state and/or raises Http404 if necessary. |
|---|
| 85 |
|
|---|
| 86 |
For example, this URLconf captures a user_id variable: |
|---|
| 87 |
|
|---|
| 88 |
(r'^contact/(?P<user_id>\d{1,6})/$', MyFormPreview(MyForm)), |
|---|
| 89 |
|
|---|
| 90 |
In this case, the kwargs variable in parse_params would be |
|---|
| 91 |
{'user_id': 32} for a request to '/contact/32/'. You can use that |
|---|
| 92 |
user_id to make sure it's a valid user and/or save it for later, for |
|---|
| 93 |
use in done(). |
|---|
| 94 |
""" |
|---|
| 95 |
pass |
|---|
| 96 |
|
|---|
| 97 |
def security_hash(self, request, form): |
|---|
| 98 |
""" |
|---|
| 99 |
Calculates the security hash for the given Form instance. |
|---|
| 100 |
|
|---|
| 101 |
This creates a list of the form field names/values in a deterministic |
|---|
| 102 |
order, pickles the result with the SECRET_KEY setting and takes an md5 |
|---|
| 103 |
hash of that. |
|---|
| 104 |
|
|---|
| 105 |
Subclasses may want to take into account request-specific information |
|---|
| 106 |
such as the IP address. |
|---|
| 107 |
""" |
|---|
| 108 |
data = [(bf.name, bf.data or '') for bf in form] + [settings.SECRET_KEY] |
|---|
| 109 |
# Use HIGHEST_PROTOCOL because it's the most efficient. It requires |
|---|
| 110 |
# Python 2.3, but Django requires 2.3 anyway, so that's OK. |
|---|
| 111 |
pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) |
|---|
| 112 |
return md5.new(pickled).hexdigest() |
|---|
| 113 |
|
|---|
| 114 |
def failed_hash(self, request): |
|---|
| 115 |
"Returns an HttpResponse in the case of an invalid security hash." |
|---|
| 116 |
return self.preview_post(request) |
|---|
| 117 |
|
|---|
| 118 |
# METHODS SUBCLASSES MUST OVERRIDE ######################################## |
|---|
| 119 |
|
|---|
| 120 |
def done(self, request, cleaned_data): |
|---|
| 121 |
""" |
|---|
| 122 |
Does something with the cleaned_data and returns an |
|---|
| 123 |
HttpResponseRedirect. |
|---|
| 124 |
""" |
|---|
| 125 |
raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__) |
|---|