Django

Code

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@saltvik.no>, 3 years ago)

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

  • django/contrib/formtools/wizard.py

    old new  
     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

    old new  
     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

    old new  
     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

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

    old new  
     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

    old new  
     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

    old new  
    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)