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