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\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)