| | 1 | """ |
|---|
| | 2 | USAGE: |
|---|
| | 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 | """ |
|---|
| | 29 | from django.conf import settings |
|---|
| | 30 | from django.http import Http404 |
|---|
| | 31 | from django.shortcuts import render_to_response |
|---|
| | 32 | from django.template.context import RequestContext |
|---|
| | 33 | |
|---|
| | 34 | from django import newforms as forms |
|---|
| | 35 | import cPickle as pickle |
|---|
| | 36 | import md5 |
|---|
| | 37 | |
|---|
| | 38 | class 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__) |