| 1 | from django.conf import settings |
| 2 | from django.http import Http404 |
| 3 | from django.shortcuts import render_to_response |
| 4 | from django.template.context import RequestContext |
| 5 | |
| 6 | from django import newforms as forms |
| 7 | import cPickle as pickle |
| 8 | import md5 |
| 9 | |
| 10 | class 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__) |