1""" 2Formtools Preview application. 3""" 4from django.http import Http404 5from django.shortcuts import render 6from django.utils.crypto import constant_time_compare 7 8from .utils import form_hmac 9 10AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter. 11 12 13class FormPreview(object): 14 preview_template = 'formtools/preview.html' 15 form_template = 'formtools/form.html' 16 17 # METHODS SUBCLASSES SHOULDN'T OVERRIDE ################################### 18 19 def __init__(self, form): 20 # form should be a Form class, not an instance. 21 self.form, self.state = form, {} 22 23 def __call__(self, request, *args, **kwargs): 24 stage = { 25 '1': 'preview', 26 '2': 'post', 27 }.get(request.POST.get(self.unused_name('stage')), 'preview') 28 self.parse_params(request, *args, **kwargs) 29 try: 30 method = getattr(self, stage + '_' + request.method.lower()) 31 except AttributeError: 32 raise Http404 33 return method(request) 34 35 def unused_name(self, name): 36 """ 37 Given a first-choice name, adds an underscore to the name until it 38 reaches a name that isn't claimed by any field in the form. 39 40 This is calculated rather than being hard-coded so that no field names 41 are off-limits for use in the form. 42 """ 43 while 1: 44 try: 45 self.form.base_fields[name] 46 except KeyError: 47 break # This field name isn't being used by the form. 48 name += '_' 49 return name 50 51 def preview_get(self, request): 52 "Displays the form" 53 f = self.form(auto_id=self.get_auto_id(), 54 initial=self.get_initial(request)) 55 return render(request, self.form_template, self.get_context(request, f)) 56 57 def preview_post(self, request): 58 """ 59 Validates the POST data. If valid, displays the preview page. 60 Else, redisplays form. 61 """ 62 f = self.form(request.POST, auto_id=self.get_auto_id()) 63 context = self.get_context(request, f) 64 if f.is_valid(): 65 self.process_preview(request, f, context) 66 context['hash_field'] = self.unused_name('hash') 67 context['hash_value'] = self.security_hash(request, f) 68 return render(request, self.preview_template, context) 69 else: 70 return render(request, self.form_template, context) 71 72 def _check_security_hash(self, token, request, form): 73 expected = self.security_hash(request, form) 74 return constant_time_compare(token, expected) 75 76 def post_post(self, request): 77 """ 78 Validates the POST data. If valid, calls done(). Else, redisplays form. 79 """ 80 form = self.form(request.POST, auto_id=self.get_auto_id()) 81 if form.is_valid(): 82 if not self._check_security_hash( 83 request.POST.get(self.unused_name('hash'), ''), 84 request, form): 85 return self.failed_hash(request) # Security hash failed. 86 return self.done(request, form.cleaned_data) 87 else: 88 return render(request, self.form_template, self.get_context(request, form)) 89 90 # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ######################## 91 92 def get_auto_id(self): 93 """ 94 Hook to override the ``auto_id`` kwarg for the form. Needed when 95 rendering two form previews in the same template. 96 """ 97 return AUTO_ID 98 99 def get_initial(self, request): 100 """ 101 Takes a request argument and returns a dictionary to pass to the form's 102 ``initial`` kwarg when the form is being created from an HTTP get. 103 """ 104 return {} 105 106 def get_context(self, request, form): 107 "Context for template rendering." 108 return { 109 'form': form, 110 'stage_field': self.unused_name('stage'), 111 'state': self.state, 112 } 113 114 def parse_params(self, request, *args, **kwargs): 115 """ 116 Given captured args and kwargs from the URLconf, saves something in 117 self.state and/or raises :class:`~django.http.Http404` if necessary. 118 119 For example, this URLconf captures a user_id variable:: 120 121 (r'^contact/(?P<user_id>\d{1,6})/$', MyFormPreview(MyForm)), 122 123 In this case, the kwargs variable in parse_params would be 124 ``{'user_id': 32}`` for a request to ``'/contact/32/'``. You can use 125 that ``user_id`` to make sure it's a valid user and/or save it for 126 later, for use in :meth:`~formtools.preview.FormPreview.done()`. 127 """ 128 pass 129 130 def process_preview(self, request, form, context): 131 """ 132 Given a validated form, performs any extra processing before displaying 133 the preview page, and saves any extra data in context. 134 135 By default, this method is empty. It is called after the form is 136 validated, but before the context is modified with hash information 137 and rendered. 138 """ 139 pass 140 141 def security_hash(self, request, form): 142 """ 143 Calculates the security hash for the given 144 :class:`~django.http.HttpRequest` and :class:`~django.forms.Form` 145 instances. 146 147 Subclasses may want to take into account request-specific information, 148 such as the IP address. 149 """ 150 return form_hmac(form) 151 152 def failed_hash(self, request): 153 """ 154 Returns an :class:`~django.http.HttpResponse` in the case of 155 an invalid security hash. 156 """ 157 return self.preview_post(request) 158 159 # METHODS SUBCLASSES MUST OVERRIDE ######################################## 160 161 def done(self, request, cleaned_data): 162 """ 163 Does something with the ``cleaned_data`` data and then needs to 164 return an :class:`~django.http.HttpResponseRedirect`, e.g. to a 165 success page. 166 """ 167 raise NotImplementedError('You must define a done() method on your ' 168 '%s subclass.' % self.__class__.__name__) 169