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
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
Please login first before commenting.