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