preview.py 5.6 KB

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