Ticket #3218: form_wizard_start_of_tests.2.diff

File form_wizard_start_of_tests.2.diff, 12.1 KB (added by Øyvind Saltvik <oyvind@…>, 17 years ago)

Test so far, anyone else want to give it a go, i'm suffering from test blindness

  • django/contrib/formtools/wizard.py

     
     1from django.conf import settings
     2from django.http import Http404
     3from django.shortcuts import render_to_response
     4from django.template.context import RequestContext
     5 
     6from django import newforms as forms
     7import cPickle as pickle
     8import md5
     9 
     10class Wizard(object):
     11    PREFIX="%d"
     12    STEP_FIELD="wizard_step"
     13 
     14    # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
     15    def __init__(self, form_list, initial=None):
     16        " Pass list of Form classes (not instances !) "
     17        self.form_list = form_list[:]
     18        self.initial = initial or {}
     19 
     20    def __repr__(self):
     21        return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
     22   
     23    def get_form(self, step, data=None):
     24        " Shortcut to return form instance. "
     25        return self.form_list[step](data, prefix=self.PREFIX % step, initial=self.initial.get(step, None))
     26
     27    def __call__(self, request, *args, **kwargs):
     28        """
     29        Main function that does all the hard work:
     30            - initializes the wizard object (via parse_params())
     31            - veryfies (using security_hash()) that noone has tempered with the data since we last saw them
     32                calls failed_hash() if it is so
     33                calls process_step() for every previously submitted form
     34            - validates current form and
     35                returns it again if errors were found
     36                returns done() if it was the last form
     37                returns next form otherwise
     38        """
     39        # add extra_context, we don't care if somebody overrides it, as long as it remains a dict
     40        self.extra_context = getattr(self, 'extra_context', {})
     41        if 'extra_context' in kwargs:
     42            self.extra_context.update(kwargs['extra_context'])
     43 
     44        self.parse_params(request, *args, **kwargs)
     45 
     46        # we only accept POST method for form delivery  no POST, no data
     47        if not request.POST:
     48            self.step = 0
     49            return self.render(self.get_form(0), request)
     50 
     51        # verify old steps' hashes
     52        for i in range(self.step):
     53            form = self.get_form(i, request.POST)
     54            # somebody is trying to corrupt our data
     55            if request.POST.get("hash_%d" % i, '') != self.security_hash(request, form):
     56                # revert to the corrupted step
     57                return self.failed_hash(request, i)
     58            self.process_step(request, form, i)
     59 
     60        # process current step
     61        form = self.get_form(self.step, request.POST)
     62        if form.is_valid():
     63            self.process_step(request, form, self.step)
     64            self.step += 1
     65            # this was the last step
     66            if self.step == len(self.form_list):
     67                return self.done( request, [self.get_form(i, request.POST) for i in range(len(self.form_list))])
     68            form = self.get_form(self.step)
     69        return self.render(form, request)
     70
     71    def render(self, form, request):
     72        """
     73        Prepare the form and call the render_template() method to do tha actual rendering.
     74        """
     75        if self.step >= len(self.form_list):
     76            raise Http404
     77 
     78        old_data = request.POST
     79        prev_fields = ''
     80        if old_data:
     81            # old data
     82            prev_fields = '\n'.join(
     83                    bf.as_hidden() for i in range(self.step) for bf in self.get_form(i, old_data)
     84                )
     85            # hashes for old forms
     86            hidden = forms.widgets.HiddenInput()
     87            prev_fields += '\n'.join(
     88                    hidden.render("hash_%d" % i, old_data.get("hash_%d" % i, self.security_hash(request, self.get_form(i, old_data))))
     89                        for i in range(self.step)
     90                )
     91        return self.render_template(request, form, prev_fields)
     92 
     93       
     94    # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
     95 
     96    def failed_hash(self, request, i):
     97        """
     98        One of the hashes verifying old data doesn't match.
     99        """
     100        self.step = i
     101        return self.render(self.get_form(self.step), request)
     102 
     103    def security_hash(self, request, form):
     104        """
     105        Calculates the security hash for the given Form instance.
     106 
     107        This creates a list of the form field names/values in a deterministic
     108        order, pickles the result with the SECRET_KEY setting and takes an md5
     109        hash of that.
     110 
     111        Subclasses may want to take into account request-specific information
     112        such as the IP address.
     113        """
     114        data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY]
     115        # Use HIGHEST_PROTOCOL because it's the most efficient. It requires
     116        # Python 2.3, but Django requires 2.3 anyway, so that's OK.
     117        pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
     118        return md5.new(pickled).hexdigest()
     119 
     120    def parse_params(self, request, *args, **kwargs):
     121        """
     122        Set self.step, process any additional info from parameters and/or form data
     123        """
     124        if request.POST:
     125            self.step = int(request.POST.get(self.STEP_FIELD, 0))
     126        else:
     127            self.step = 0
     128
     129    def get_template(self):
     130        """
     131        Return name of the template to be rendered, use self.step to get the step number.
     132        """
     133        return "wizard.html"
     134 
     135    def render_template(self, request, form, previous_fields):
     136        """
     137        Render template for current step, override this method if you wish to add custom context, return a different mimetype etc.
     138 
     139        If you only wish to override the template name, use get_template
     140 
     141        Some additional items are added to the context:
     142            'step_field' is the name of the hidden field containing step
     143            'step' holds the current step
     144            'form' containing the current form to be processed (either empty or with errors)
     145            'previous_data' contains all the addtitional information, including
     146                hashes for finished forms and old data in form of hidden fields
     147            any additional data stored in self.extra_context
     148        """
     149        return render_to_response(self.get_template(), dict(
     150                    step_field=self.STEP_FIELD,
     151                    step=self.step,
     152                    form=form,
     153                    previous_fields=previous_fields,
     154                    ** self.extra_context
     155                ), context_instance=RequestContext(request))
     156 
     157    def process_step(self, request, form, step):
     158        """
     159        This should not modify any data, it is only a hook to modify wizard's internal state
     160        (such as dynamically generating form_list based on previously submited forms).
     161        It can also be used to add items to self.extra_context base on the contents of previously submitted forms.
     162 
     163        Note that this method is called every time a page is rendered for ALL submitted steps.
     164 
     165        Only valid data enter here.
     166        """
     167        pass
     168 
     169    # METHODS SUBCLASSES MUST OVERRIDE ########################################
     170 
     171    def done(self, request, form_list):
     172        """
     173        this method must be overriden, it is responsible for the end processing - it will be called with instances of all form_list with their data
     174        """
     175        raise NotImplementedError('You must define a done() method on your %s subclass.' % self.__class__.__name__)
  • tests/regressiontests/formtools/views.py

     
     1from django.contrib.formtools.wizard import Wizard
     2from django import newforms as forms
     3
     4class NameForm(forms.Form):
     5    first_name = forms.CharField()
     6    last_name = forms.CharField()
     7
     8class DetailsForm(forms.Form):
     9    phone = forms.CharField()
     10    adress = forms.CharField(widget=forms.Textarea)
     11
     12class ConfirmForm(forms.Form):
     13    confirm = forms.BooleanField()
     14
     15MyWizard = Wizard([NameForm, DetailsForm, ConfirmForm])
     16
     17def wizard(request):
     18
     19    response = MyWizard(request)
     20
     21    hashes = [MyWizard.security_hash(
     22        request,
     23        MyWizard.get_form(i, request.POST)
     24    ) for i in range(MyWizard.step)]
     25
     26    response['WizardHashes'] = hashes
     27
     28    return response
     29
     30
  • tests/regressiontests/formtools/tests.py

     
     1import unittest
     2from django.test.client import Client
     3
     4first_post = {
     5    '0-first_name': 'John',
     6    '0-last_name': 'Doe',
     7    'wizard_step': '0'
     8}
     9
     10second_post = {
     11    '1-first_name': 'John',
     12    '1-last_name': 'Doe',
     13    '1-phone': '99240889',
     14    '1-adress': 'memory lane',
     15    'wizard_step': '1'
     16}
     17
     18third_post = {
     19    '2-first_name': 'John',
     20    '2-last_name': 'Doe',
     21    '2-phone': '99240889',
     22    '2-adress': 'memory lane',
     23    '2-confirm': '1',
     24    'wizard_step': '2'
     25}
     26
     27
     28class WizardTest(unittest.TestCase):
     29    def setUp(self):
     30        # Every test needs a client.
     31        self.client = Client()
     32
     33    def test_details(self):
     34        # Issue a GET request.
     35        response_1 = self.client.get('/formtools/wizard/')
     36       
     37        # Check that the respose is 200 OK.
     38        self.failUnlessEqual(response_1.status_code, 200)
     39
     40        # Issue a POST request.
     41        response_2 = self.client.post('/formtools/wizard/', first_post)
     42
     43        hashes_1 = response_2['WizardHashes']
     44
     45        postdata = dict(second_post, hash_1=hashes_1[0])
     46        response_2 = self.client.post('/formtools/wizard/', postdata)
     47
     48        assert False, response_2
     49
  • tests/regressiontests/formtools/urls.py

     
     1from django.conf.urls.defaults import *
     2import views
     3
     4urlpatterns = patterns('',
     5    (r'^wizard/$', views.wizard),
     6)
  • tests/regressiontests/formtools/templates/wizard.html

     
     1        <form action="." method="POST">
     2        FORM( {{ step }} ): {{ form }}
     3       
     4        step_info : <input type="hidden" name="{{ step_field }}" value="{{ step }}" />
     5 
     6        previous_fields: {{ previous_fields }}
     7 
     8        <input type="submit">
     9        </form>
     10       
  • tests/regressiontests/formtools/templates/done.html

     
     1        {% for data in form_data %}
     2            {% for item in data.items %}
     3                {{ item.0 }} : {{ item.1 }}<br />
     4            {% endfor %}
     5        {% endfor %}
     6       
  • tests/urls.py

     
    1111
    1212    # test urlconf for {% url %} template tag
    1313    (r'^url_tag/', include('regressiontests.templates.urls')),
     14
     15    # test for form wizard
     16    (r'^formtools/', include('regressiontests.formtools.urls')),
    1417)
Back to Top