Login

Complex Form Preview

Author:
smagala
Posted:
March 30, 2009
Language:
Python
Version:
1.0
Score:
4 (after 4 ratings)

Problem

The FormPreview class provided by contrib.formtools helps automate a common workflow. You display a form, then force a preview, then finally allow a submit. If the form gets tampered with, the original form gets redisplayed.

Unfortunately, this class can only be used when you have an html form that is backed by exactly one Django form. No formsets, no html forms backed by more than one Django form.

Solution

I was asked to create exactly this sort of workflow for a highly complex form + formset. The ComplexFormPreview class provides a base class to help with this problem. As with FormPreview, you must override a few functions.

Code

The abstract ComplexFormPreview class can live anywhere on your python path. Import it and subclass is exactly like you would contrib.formtools FormPreview.

The self.state dictionary is passed to all response calls as the context for your templates. Add any objects you need in your template to this dictionary. This includes all forms, formsets, and any additional variables you want in your template context.

Override the parse_params if you need to get any args/kwargs from your url. Save these values in self.state if you want them in your template context.

Override the init_forms method to do setup for all of your forms and formsets. Save all your forms in self.state. You should provide a unique prefix for all forms and formsets on the page to avoid id collisions in html.

VERY IMPORTANT NOTE: init_forms is called with a kwargs dictionary. You need to pass **kwargs to all of your form definations in init_forms. This is how the POST data is going to be passed to your forms and formsets.

VERY IMPORTANT NOTE No. 2: all of the validation is handled inside the class - all forms will be found and validated, and we will only proceed when everything is found to be valid. This means that you can use the class as a view directly, or provide a thin wrapper function around it if you want.

Override the done method to handle what should be done once your user has successfully previewed and submitted the form. Usually, this will involve calling one or more save() calls to your various forms and formsets.

Because you now have multiple forms, the default contrib.formtools templates don't work. You must make custom templates that reference all of your various forms. The stage_field, hash_field, and hash_value fields are used exactly like the formtools examples. Follow the basic layout demonstrated in the example templates, and substitute your custom forms for the default form.

Example views.py

The views.py demonstrated here has many hooks into my project, including using some complex formset classes. It won't work for you without being customized, but it will demonstrate how to override the default ComplexFormPreview.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
preview.py
==========

"""Complex form preview app"""

# python imports
import cPickle as pickle

# django imports
from django.forms import Form
from django.forms.formsets import BaseFormSet
from django.conf import settings
from django.utils.hashcompat import md5_constructor
from django.http import Http404
from django.shortcuts import render_to_response
from django.template.context import RequestContext


class ComplexFormPreview(object):
    """Like a form preview view, but for many forms and formsets"""
    # override these - they don't actually work!
    preview_template = 'formtools/preview.html'
    form_template = 'formtools/form.html'

    def __init__(self):
        self.state = {}

    def __call__(self, request, *args, **kwargs):
        # because we use prefixes for all forms, we can avoid doing all
        # of the unused_name junk
        stage = {'1': 'preview', '2': 'post'}.get(
                request.POST.get('stage'), 'preview')
        self.parse_params(*args, **kwargs)
        try:
            method = getattr(self, stage + '_' + request.method.lower())
        except AttributeError:
            raise Http404
        return method(request)

    def init_forms(self, **kwargs):
        """Dynamic form initilization

        Save all forms to the self.state dictionary - this will become the
        context for the templates.

        """
        raise NotImplementedError(
                'You must define an init_forms() method on your %s subclass.' \
                        % self.__class__.__name__)

    def all_valid(self):
        """Check that all forms and formsets are valid"""
        # do not short circut the evaluations so that all forms are
        # evaluated and all errors will be shown
        flag = True
        keys = sorted(self.state.keys())
        for key in keys:
            value = self.state[key]
            if isinstance(value, Form):
                form = value
                flag = flag and form.is_valid()
            elif isinstance(value, BaseFormSet):
                formset = value
                flag = flag and formset.is_valid()
        return flag

    def preview_get(self, request):
        """Display the form"""
        self.init_forms()
        self.state['stage_field'] = 'stage'
        return render_to_response(self.form_template, self.state,
                context_instance=RequestContext(request))

    def preview_post(self, request):
        """Redisplay the forms with errors, or show a preview.

        When the form is POSTed, bind the data and validate all forms.
        If valid, display the preview page, else redisplay the forms.

        """
        self.init_forms(data=request.POST)
        self.state['stage_field'] = 'stage'
        if self.all_valid():
            self.state['hash_field'] = 'hash'
            self.state['hash_value'] = self.security_hash()
            return render_to_response(self.preview_template, self.state,
                    context_instance=RequestContext(request))
        else:
            return render_to_response(self.form_template, self.state,
                    context_instance=RequestContext(request))

    def post_post(self, request):
        """Validate the form and call done or redisplay if invalid"""
        self.init_forms(data=request.POST)
        if self.all_valid():
            if self.security_hash() != request.POST.get('hash'):
                return self.preview_post(request)
            return self.done(request)
        else:
            # there were errors on the modified form
            self.state['stage_field'] = 'stage'
            return render_to_response(self.form_template, self.state,
                    context_instance=RequestContext(request))

    def parse_params(self, *args, **kwargs):
        """Handle captured args/kwargs from the URLconf

        Given captured args and kwargs from the URLconf, saves something
        in self.state and/or raises 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 done().

        """
        pass

    def security_hash(self):
        """Calculate an md5 hash for all of form(set)s"""
        data = [settings.SECRET_KEY]
        # ensure that we always process self.state in the same order
        keys = sorted(self.state.keys())
        for key in keys:
            value = self.state[key]
            if isinstance(value, Form):
                # for each form in self.state add (field, value, )
                # tuples to data
                form = value
                data.extend([(bound_field.name,
                        bound_field.field.clean(bound_field.data) or '', )
                        for bound_field in form])
            elif isinstance(value, BaseFormSet):
                # for each formset in self.state, for each form in a
                # formset, add (field, value, ) tuples to data
                formset = value
                for form in formset.forms:
                    data.extend([(bound_field.name,
                            bound_field.field.clean(bound_field.data) or '')
                            for bound_field in form])
        # pickle the data and hash it to get a shared secret
        pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
        return md5_constructor(pickled).hexdigest()

    def done(self, request):
        """Save the results of the form and return an HttpResponseRedirect"""
        raise NotImplementedError(
                'You must define a done() method on your %s subclass.' % \
                self.__class__.__name__)


views.py
========

class HIFormPreview(ComplexFormPreview):
    """View with preview capabilities for the HI/sequence form"""
    preview_template = 'sequence/hi_form_preview.html'
    form_template = 'sequence/hi_form.html'

    def parse_params(self, *args, **kwargs):
        """Handle captured args/kwargs from the URLconf"""
        # get the selected HI test
        try:
            self.state['hi_test'] = HITest.objects.get(id=kwargs['test_id'])
        except HITest.DoesNotExist:
            raise Http404("Invalid HI test id: '%s'" % test_id)

        # get a list of segments that this form will be used for (A or B)
        subtypes = self.state['hi_test'].subtype.all()
        if len(subtypes) != 1:
            raise Http404('This form cannot be used for hi_tests of type %s' %
                    hi_test.get_subtype)
        self.state['subtype'] = subtypes[0]

    def init_forms(self, **kwargs):
        """Dynamic form init"""
        hi_test = self.state['hi_test']
        subtype = self.state['subtype']
        ss_formset = SequenceHITestFormSet(
                subtype=subtype,
                hi_test=hi_test,
                prefix='seq_specimen',
                **kwargs)
        gene_form = SequenceGeneForm(subtype=subtype, prefix='gene', **kwargs)
        self.state['ss_formset'] = ss_formset
        self.state['gene_form'] = gene_form

    def done(self, request):
        """save the results of the form and return an HttpResponseRedirect"""
        self.state['ss_formset'].save(self.state['gene_form'])
        return HttpResponseRedirect('/flu/sequence/pending/')


@login_required
def hi_form(request, *args, **kwargs):
    """A thin wrapper for an HIFormPreview instance"""
    # The wrapper is necessary to allow the entire class-based view
    # (derived from ComplexFormPreview) to be wrapped in a
    # login_required.  It would be possible to decorate individual bound
    # view functions of the class, but would end up being more work than
    # using a function and decorator
    view = HIFormPreview()
    return view(request, *args, **kwargs)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 9 months ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 9 months, 1 week ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 4 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 4 months ago
  5. Help text hyperlinks by sa2812 1 year, 5 months ago

Comments

Please login first before commenting.