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)