Django

Code

Ticket #3218: form_wizard_start_of_tests_using_wizard.5.diff

File form_wizard_start_of_tests_using_wizard.5.diff, 13.3 kB (added by Øyvind Saltvik <oyvind@saltvik.no>, 3 years ago)

Updated to Honza's latest code

  • django/contrib/formtools/wizard.py

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