169 lines
5.9 KiB
Python
169 lines
5.9 KiB
Python
"""
|
|
Formtools Preview application.
|
|
"""
|
|
from django.http import Http404
|
|
from django.shortcuts import render
|
|
from django.utils.crypto import constant_time_compare
|
|
|
|
from .utils import form_hmac
|
|
|
|
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(request, *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:
|
|
self.form.base_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=self.get_auto_id(),
|
|
initial=self.get_initial(request))
|
|
return render(request, self.form_template, self.get_context(request, f))
|
|
|
|
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=self.get_auto_id())
|
|
context = self.get_context(request, f)
|
|
if f.is_valid():
|
|
self.process_preview(request, f, context)
|
|
context['hash_field'] = self.unused_name('hash')
|
|
context['hash_value'] = self.security_hash(request, f)
|
|
return render(request, self.preview_template, context)
|
|
else:
|
|
return render(request, self.form_template, context)
|
|
|
|
def _check_security_hash(self, token, request, form):
|
|
expected = self.security_hash(request, form)
|
|
return constant_time_compare(token, expected)
|
|
|
|
def post_post(self, request):
|
|
"""
|
|
Validates the POST data. If valid, calls done(). Else, redisplays form.
|
|
"""
|
|
form = self.form(request.POST, auto_id=self.get_auto_id())
|
|
if form.is_valid():
|
|
if not self._check_security_hash(
|
|
request.POST.get(self.unused_name('hash'), ''),
|
|
request, form):
|
|
return self.failed_hash(request) # Security hash failed.
|
|
return self.done(request, form.cleaned_data)
|
|
else:
|
|
return render(request, self.form_template, self.get_context(request, form))
|
|
|
|
# METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
|
|
|
|
def get_auto_id(self):
|
|
"""
|
|
Hook to override the ``auto_id`` kwarg for the form. Needed when
|
|
rendering two form previews in the same template.
|
|
"""
|
|
return AUTO_ID
|
|
|
|
def get_initial(self, request):
|
|
"""
|
|
Takes a request argument and returns a dictionary to pass to the form's
|
|
``initial`` kwarg when the form is being created from an HTTP get.
|
|
"""
|
|
return {}
|
|
|
|
def get_context(self, request, form):
|
|
"Context for template rendering."
|
|
return {
|
|
'form': form,
|
|
'stage_field': self.unused_name('stage'),
|
|
'state': self.state,
|
|
}
|
|
|
|
def parse_params(self, request, *args, **kwargs):
|
|
"""
|
|
Given captured args and kwargs from the URLconf, saves something in
|
|
self.state and/or raises :class:`~django.http.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 :meth:`~formtools.preview.FormPreview.done()`.
|
|
"""
|
|
pass
|
|
|
|
def process_preview(self, request, form, context):
|
|
"""
|
|
Given a validated form, performs any extra processing before displaying
|
|
the preview page, and saves any extra data in context.
|
|
|
|
By default, this method is empty. It is called after the form is
|
|
validated, but before the context is modified with hash information
|
|
and rendered.
|
|
"""
|
|
pass
|
|
|
|
def security_hash(self, request, form):
|
|
"""
|
|
Calculates the security hash for the given
|
|
:class:`~django.http.HttpRequest` and :class:`~django.forms.Form`
|
|
instances.
|
|
|
|
Subclasses may want to take into account request-specific information,
|
|
such as the IP address.
|
|
"""
|
|
return form_hmac(form)
|
|
|
|
def failed_hash(self, request):
|
|
"""
|
|
Returns an :class:`~django.http.HttpResponse` in the case of
|
|
an invalid security hash.
|
|
"""
|
|
return self.preview_post(request)
|
|
|
|
# METHODS SUBCLASSES MUST OVERRIDE ########################################
|
|
|
|
def done(self, request, cleaned_data):
|
|
"""
|
|
Does something with the ``cleaned_data`` data and then needs to
|
|
return an :class:`~django.http.HttpResponseRedirect`, e.g. to a
|
|
success page.
|
|
"""
|
|
raise NotImplementedError('You must define a done() method on your '
|
|
'%s subclass.' % self.__class__.__name__)
|