|
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__) |