| 1 | Index: /home/david/work/django/django-trunk/django/contrib/formtools/wizard.py | 
|---|
| 2 | =================================================================== | 
|---|
| 3 | --- /home/david/work/django/django-trunk/django/contrib/formtools/wizard.py     (revision 9084) | 
|---|
| 4 | +++ /home/david/work/django/django-trunk/django/contrib/formtools/wizard.py     (working copy) | 
|---|
| 5 | @@ -1,21 +1,22 @@ | 
|---|
| 6 | -""" | 
|---|
| 7 | -FormWizard class -- implements a multi-page form, validating between each | 
|---|
| 8 | -step and storing the form's state as HTML hidden fields so that no state is | 
|---|
| 9 | -stored on the server side. | 
|---|
| 10 | -""" | 
|---|
| 11 | - | 
|---|
| 12 | import cPickle as pickle | 
|---|
| 13 |  | 
|---|
| 14 | from django import forms | 
|---|
| 15 | from django.conf import settings | 
|---|
| 16 | from django.http import Http404 | 
|---|
| 17 | +from django.http import HttpResponseRedirect | 
|---|
| 18 | from django.shortcuts import render_to_response | 
|---|
| 19 | from django.template.context import RequestContext | 
|---|
| 20 | from django.utils.hashcompat import md5_constructor | 
|---|
| 21 | from django.utils.translation import ugettext_lazy as _ | 
|---|
| 22 | from django.contrib.formtools.utils import security_hash | 
|---|
| 23 |  | 
|---|
| 24 | + | 
|---|
| 25 | class FormWizard(object): | 
|---|
| 26 | +    """ | 
|---|
| 27 | +    FormWizard class -- implements a multi-page form, validating between each | 
|---|
| 28 | +    step and storing the form's state as HTML hidden fields so that no state is | 
|---|
| 29 | +    stored on the server side. | 
|---|
| 30 | +    """ | 
|---|
| 31 | # Dictionary of extra template context variables. | 
|---|
| 32 | extra_context = {} | 
|---|
| 33 |  | 
|---|
| 34 | @@ -239,3 +240,399 @@ | 
|---|
| 35 | data. | 
|---|
| 36 | """ | 
|---|
| 37 | raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__) | 
|---|
| 38 | + | 
|---|
| 39 | + | 
|---|
| 40 | +class SessionWizard(object): | 
|---|
| 41 | +    """ | 
|---|
| 42 | +    SessionWizard class -- implements multi-page forms with the following | 
|---|
| 43 | +    characteristics: | 
|---|
| 44 | + | 
|---|
| 45 | +       1) easily supports navigation to arbitrary pages in the wizard | 
|---|
| 46 | +       2) uses GETs to display forms (caveat validation errors) and POSTs for | 
|---|
| 47 | +          form submissions | 
|---|
| 48 | + | 
|---|
| 49 | +    Pros are support for back-button and arbitrary navigation within pages | 
|---|
| 50 | +    (including the oddity of someone clicking on the refresh button) | 
|---|
| 51 | + | 
|---|
| 52 | +    The major Con is use of the session scope.  In particular, zero | 
|---|
| 53 | +    consideration has been given to multipart form data. | 
|---|
| 54 | +    """ | 
|---|
| 55 | + | 
|---|
| 56 | +    # keys used to store wizard data in sessions | 
|---|
| 57 | +    __form_list_key = 'form_list' | 
|---|
| 58 | +    __cleaned_data_key = 'cleaned_data' | 
|---|
| 59 | +    __POST_data_key = 'POST_data' | 
|---|
| 60 | +    __page_infos_key = 'page_infos' | 
|---|
| 61 | + | 
|---|
| 62 | +    def __init__(self, form_list): | 
|---|
| 63 | +        """form_list should be a list of Form classes (not instances).""" | 
|---|
| 64 | +        self.base_form_list = form_list[:] | 
|---|
| 65 | + | 
|---|
| 66 | +    def _init_form_list(self, request): | 
|---|
| 67 | +        """ | 
|---|
| 68 | +        Copy self.base_form_list to the session scope so that subclasses can | 
|---|
| 69 | +        manipulate the form_list for individual users. | 
|---|
| 70 | +        """ | 
|---|
| 71 | +        session_key = self.get_wizard_data_key(request) | 
|---|
| 72 | +        if session_key not in request.session: | 
|---|
| 73 | +            request.session[session_key] = { | 
|---|
| 74 | +                self.__form_list_key : self.base_form_list[:], | 
|---|
| 75 | +                self.__cleaned_data_key : [], | 
|---|
| 76 | +                self.__POST_data_key : [], | 
|---|
| 77 | +                self.__page_infos_key : [], | 
|---|
| 78 | +            } | 
|---|
| 79 | + | 
|---|
| 80 | +    def __call__(self, request, *args, **kwargs): | 
|---|
| 81 | +        """ | 
|---|
| 82 | +        Initialize the form_list for a session if needed and call GET or | 
|---|
| 83 | +        POST depending on the http method. | 
|---|
| 84 | +        """ | 
|---|
| 85 | +        self._init_form_list(request) | 
|---|
| 86 | +        page0 = int(kwargs['page0']) | 
|---|
| 87 | + | 
|---|
| 88 | +        if request.method == 'POST': | 
|---|
| 89 | +            return self.POST(request) | 
|---|
| 90 | +        else: | 
|---|
| 91 | +            return self.GET(request, page0) | 
|---|
| 92 | + | 
|---|
| 93 | + | 
|---|
| 94 | +    def GET(self, request, page0): | 
|---|
| 95 | +        """ | 
|---|
| 96 | +        Display the form/page for the page identified by page0 | 
|---|
| 97 | +        """ | 
|---|
| 98 | +        page_data = self._get_cleaned_data(request.session, page0) | 
|---|
| 99 | +        if page_data is None: | 
|---|
| 100 | +            form = self._get_form_list(request.session)[page0]() | 
|---|
| 101 | +        else: | 
|---|
| 102 | +            form_class = self._get_form_list(request.session)[page0] | 
|---|
| 103 | +            if issubclass(form_class, forms.ModelForm): | 
|---|
| 104 | +                form = form_class(instance=form_class.Meta.model(**page_data)) | 
|---|
| 105 | +            else: | 
|---|
| 106 | +                form = form_class(initial=page_data) | 
|---|
| 107 | +        return self._show_form(request, page0, form) | 
|---|
| 108 | + | 
|---|
| 109 | +    def POST(self, request): | 
|---|
| 110 | +        """ | 
|---|
| 111 | +        Validate form submission, and redirect to GET the next form or return | 
|---|
| 112 | +        the response from self.done(). | 
|---|
| 113 | +        """ | 
|---|
| 114 | +        page0 = int(request.POST['page0']) | 
|---|
| 115 | +        url_base = self.get_URL_base(request, page0) | 
|---|
| 116 | +        self._set_POST_data(request.session, request.POST, page0) | 
|---|
| 117 | +        form_list = self._get_form_list(request.session) | 
|---|
| 118 | +        form = form_list[page0](request.POST) | 
|---|
| 119 | +        new_page0 = self.preprocess_submit_form(request, page0, form) | 
|---|
| 120 | + | 
|---|
| 121 | +        if new_page0 is not None: | 
|---|
| 122 | +            return HttpResponseRedirect(url_base + str(new_page0)) | 
|---|
| 123 | +        else: | 
|---|
| 124 | +            if form.is_valid(): | 
|---|
| 125 | +                self._set_cleaned_data(request.session, page0, | 
|---|
| 126 | +                                       form.cleaned_data) | 
|---|
| 127 | +                self._set_page_info(request.session, page0, True) | 
|---|
| 128 | +                is_done = self.process_submit_form(request, page0, form) | 
|---|
| 129 | +                if (is_done is None or is_done == False) and \ | 
|---|
| 130 | +                        len(form_list) > page0 + 1: | 
|---|
| 131 | +                    return HttpResponseRedirect(url_base + str(page0 + 1)) | 
|---|
| 132 | +                else: | 
|---|
| 133 | +                    first_broken_page, form  = \ | 
|---|
| 134 | +                        self._validate_all_forms(request.session) | 
|---|
| 135 | +                    if first_broken_page is not None: | 
|---|
| 136 | +                        return self._show_form(request, first_broken_page, | 
|---|
| 137 | +                                               form) | 
|---|
| 138 | +                    else: | 
|---|
| 139 | +                        return self.done(request) | 
|---|
| 140 | +            else: | 
|---|
| 141 | +                self._set_page_info(request.session, page0, False) | 
|---|
| 142 | + | 
|---|
| 143 | +        return self._show_form(request, page0, form) | 
|---|
| 144 | + | 
|---|
| 145 | + | 
|---|
| 146 | +    # form util methods # | 
|---|
| 147 | +    def _validate_all_forms(self, session): | 
|---|
| 148 | +        """ | 
|---|
| 149 | +        Iterate through the session form list and validate based on the POST | 
|---|
| 150 | +        data stored in the session for this wizard.  Return the page index and | 
|---|
| 151 | +        the form of the first invalid form or None, None if all forms are valid. | 
|---|
| 152 | +        """ | 
|---|
| 153 | +        i = 0 | 
|---|
| 154 | +        for form_class in self._get_form_list(session): | 
|---|
| 155 | +            form = form_class(self._get_POST_data(session, i)) | 
|---|
| 156 | +            if not form.is_valid(): | 
|---|
| 157 | +                return i, form | 
|---|
| 158 | +            else: | 
|---|
| 159 | +                i = i + 1 | 
|---|
| 160 | +        return None, None | 
|---|
| 161 | + | 
|---|
| 162 | +    def _show_form(self, request, page0, form): | 
|---|
| 163 | +        """ | 
|---|
| 164 | +        Show the form associated with indicated page index. | 
|---|
| 165 | +        """ | 
|---|
| 166 | +        url_base = self.get_URL_base(request, page0) | 
|---|
| 167 | +        extra_context = self.process_show_form(request, page0, form) | 
|---|
| 168 | +        self._set_current_page(request.session, page0) | 
|---|
| 169 | +        page_infos = self._get_page_infos(request.session) | 
|---|
| 170 | +        return render_to_response(self.get_template(page0), | 
|---|
| 171 | +             {'page0' : page0, | 
|---|
| 172 | +              'page' : page0 + 1, | 
|---|
| 173 | +              'form' : form, | 
|---|
| 174 | +              'page_infos' : page_infos, | 
|---|
| 175 | +              'url_base' : url_base, | 
|---|
| 176 | +              'extra_context' : extra_context | 
|---|
| 177 | +             }, RequestContext(request)) | 
|---|
| 178 | + | 
|---|
| 179 | +    def _get_form_list(self, session): | 
|---|
| 180 | +        """ | 
|---|
| 181 | +        Return the list of form classes stored in the provided session. | 
|---|
| 182 | +        """ | 
|---|
| 183 | +        return session[self.get_wizard_data_key(session)][self.__form_list_key] | 
|---|
| 184 | + | 
|---|
| 185 | +    def _insert_form(self, session, page0, form_class): | 
|---|
| 186 | +        """ | 
|---|
| 187 | +        Insert a form class into the provided session's form list at index | 
|---|
| 188 | +        page0. | 
|---|
| 189 | +        """ | 
|---|
| 190 | +        form_list = self._get_form_list(session) | 
|---|
| 191 | +        form_list.insert(page0, form_class) | 
|---|
| 192 | +        self._insert_wizard_data(session, self.__form_list_key, form_list) | 
|---|
| 193 | + | 
|---|
| 194 | +    def _remove_form(self, session, page0): | 
|---|
| 195 | +        """ | 
|---|
| 196 | +        Remove the form at index page0 from the provided sessions form list. | 
|---|
| 197 | +        """ | 
|---|
| 198 | +        self._del_wizard_data(session, self.__form_list_key, page0) | 
|---|
| 199 | +    # end form util methods # | 
|---|
| 200 | + | 
|---|
| 201 | + | 
|---|
| 202 | +    # Form data methods # | 
|---|
| 203 | +    def _get_POST_data(self, session, page0): | 
|---|
| 204 | +        """ | 
|---|
| 205 | +        Return the POST data for a given page index page0, stored in the | 
|---|
| 206 | +        provided session. | 
|---|
| 207 | +        """ | 
|---|
| 208 | +        post_data = self._get_all_POST_data(session) | 
|---|
| 209 | +        if len(post_data) > page0: | 
|---|
| 210 | +            return post_data[page0] | 
|---|
| 211 | +        else: | 
|---|
| 212 | +            return {} | 
|---|
| 213 | + | 
|---|
| 214 | +    def _set_POST_data(self, session, data, page0, force_insert=False): | 
|---|
| 215 | +        """ | 
|---|
| 216 | +        Set the POST data for a given page index and session to the 'data' | 
|---|
| 217 | +        provided.  If force_insert is True then the data assignment is forced | 
|---|
| 218 | +        as an list.insert(page0, data) call. | 
|---|
| 219 | +        """ | 
|---|
| 220 | +        post_data = self._get_all_POST_data(session) | 
|---|
| 221 | +        if force_insert or len(post_data) <= page0: | 
|---|
| 222 | +            post_data.insert(page0, data) | 
|---|
| 223 | +        else: | 
|---|
| 224 | +            post_data[page0] = data | 
|---|
| 225 | +        self._insert_wizard_data(session, self.__POST_data_key, post_data) | 
|---|
| 226 | + | 
|---|
| 227 | +    def _remove_POST_data(self, session, page0): | 
|---|
| 228 | +        """ | 
|---|
| 229 | +        Remove the POST data stored in the session at index page0. | 
|---|
| 230 | +        """ | 
|---|
| 231 | +        self._del_wizard_data(session, self.__POST_data_key, page0) | 
|---|
| 232 | + | 
|---|
| 233 | +    def _get_all_POST_data(self, session): | 
|---|
| 234 | +        """ | 
|---|
| 235 | +        Return the list of all POST data for this wizard from the provided | 
|---|
| 236 | +        session. | 
|---|
| 237 | +        """ | 
|---|
| 238 | +        return session[self.get_wizard_data_key(session)][self.__POST_data_key] | 
|---|
| 239 | + | 
|---|
| 240 | +    def _get_cleaned_data(self, session, page0): | 
|---|
| 241 | +        """ | 
|---|
| 242 | +        Return all cleaned data for this wizard from the provided session. | 
|---|
| 243 | +        """ | 
|---|
| 244 | +        cleaned_data = self._get_all_cleaned_data(session) | 
|---|
| 245 | +        if len(cleaned_data) > page0: | 
|---|
| 246 | +            return cleaned_data[page0] | 
|---|
| 247 | +        else: | 
|---|
| 248 | +            return {} | 
|---|
| 249 | + | 
|---|
| 250 | +    def _set_cleaned_data(self, session, page0, data, force_insert=False): | 
|---|
| 251 | +        """ | 
|---|
| 252 | +        Assign the cleaned data for this wizard in the session at index page0, | 
|---|
| 253 | +        optionally forcing a call a list insert call based on the | 
|---|
| 254 | +        'force_insert' argument. | 
|---|
| 255 | +        """ | 
|---|
| 256 | +        cleaned_data = self._get_all_cleaned_data(session) | 
|---|
| 257 | +        if force_insert or len(cleaned_data) <= page0: | 
|---|
| 258 | +            cleaned_data.insert(page0, data) | 
|---|
| 259 | +        else: | 
|---|
| 260 | +            cleaned_data[page0] = data | 
|---|
| 261 | +        self._insert_wizard_data(session, self.__cleaned_data_key, cleaned_data) | 
|---|
| 262 | + | 
|---|
| 263 | + | 
|---|
| 264 | +    def _get_all_cleaned_data(self, session): | 
|---|
| 265 | +        """ | 
|---|
| 266 | +        Return a list of all the cleaned data in the session for this wizard. | 
|---|
| 267 | +        """ | 
|---|
| 268 | +        wizard_data = session[self.get_wizard_data_key(session)] | 
|---|
| 269 | +        return wizard_data[self.__cleaned_data_key] | 
|---|
| 270 | + | 
|---|
| 271 | +    def _remove_cleaned_data(self, session, page0): | 
|---|
| 272 | +        """ | 
|---|
| 273 | +        Remove the cleaned data at index page0 for this wizard from the | 
|---|
| 274 | +        provided session. | 
|---|
| 275 | +        """ | 
|---|
| 276 | +        self._del_wizard_data(session, self.__cleaned_data_key, page0) | 
|---|
| 277 | +    # end Form data methods # | 
|---|
| 278 | + | 
|---|
| 279 | + | 
|---|
| 280 | +    # page methods # | 
|---|
| 281 | +    def _set_current_page(self, session, page0): | 
|---|
| 282 | +        """ | 
|---|
| 283 | +        Iterate through the page info dicts in the session and set | 
|---|
| 284 | +        'current_page' to True for the page_info corresponding to page0 and | 
|---|
| 285 | +        False for all others. | 
|---|
| 286 | +        """ | 
|---|
| 287 | +        page_infos = self._get_page_infos(session) | 
|---|
| 288 | +        for i in range(len(page_infos)): | 
|---|
| 289 | +            if i == page0: | 
|---|
| 290 | +                page_infos[i]['current_page'] = True | 
|---|
| 291 | +            else: | 
|---|
| 292 | +                page_infos[i]['current_page'] = False | 
|---|
| 293 | + | 
|---|
| 294 | +    def _get_page_infos(self, session): | 
|---|
| 295 | +        """ | 
|---|
| 296 | +        Return the list of page info dicts stored in the provided session for | 
|---|
| 297 | +        this wizard. | 
|---|
| 298 | +        """ | 
|---|
| 299 | +        return session[self.get_wizard_data_key(session)][self.__page_infos_key] | 
|---|
| 300 | + | 
|---|
| 301 | +    def _remove_page(self, session, page0): | 
|---|
| 302 | +        """ | 
|---|
| 303 | +        Remove the page for this wizard indicated by the page0 argument from | 
|---|
| 304 | +        the provided session. | 
|---|
| 305 | +        """ | 
|---|
| 306 | +        self._remove_form(session, page0) | 
|---|
| 307 | +        self._remove_page_info(session, page0) | 
|---|
| 308 | +        self._remove_cleaned_data(session, page0) | 
|---|
| 309 | +        self._remove_POST_data(session, page0) | 
|---|
| 310 | + | 
|---|
| 311 | +    def _remove_page_info(self, session, page0): | 
|---|
| 312 | +        """ | 
|---|
| 313 | +        Remove the page info dict for this wizard stored at the page0 index | 
|---|
| 314 | +        from the provided session. | 
|---|
| 315 | +        """ | 
|---|
| 316 | +        self._del_wizard_data(session, self.__page_infos_key, page0) | 
|---|
| 317 | + | 
|---|
| 318 | +    def _insert_page(self, session, page0, form_class): | 
|---|
| 319 | +        """ | 
|---|
| 320 | +        Insert a page into this wizard, storing required session structures. | 
|---|
| 321 | +        """ | 
|---|
| 322 | +        self._insert_form(session, page0, form_class) | 
|---|
| 323 | +        self._set_page_info(session, page0, False, True) | 
|---|
| 324 | +        self._set_cleaned_data(session, page0, {}, True) | 
|---|
| 325 | +        self._set_POST_data(session, {}, page0, True) | 
|---|
| 326 | + | 
|---|
| 327 | +    def _set_page_info(self, session, page0, valid, force_insert=False): | 
|---|
| 328 | +        """ | 
|---|
| 329 | +        Set the page info in this wizard for a page at index page0 and stored | 
|---|
| 330 | +        in the provided session. | 
|---|
| 331 | +        """ | 
|---|
| 332 | +        page_info = { | 
|---|
| 333 | +           'valid' : valid, | 
|---|
| 334 | +           'title' : self.get_page_title(session, page0) | 
|---|
| 335 | +        } | 
|---|
| 336 | +        page_infos = self._get_page_infos(session) | 
|---|
| 337 | +        if force_insert or len(page_infos) <= page0: | 
|---|
| 338 | +            page_infos.insert(page0, page_info) | 
|---|
| 339 | +        else: | 
|---|
| 340 | +            page_infos[page0] = page_info | 
|---|
| 341 | +        self._insert_wizard_data(session, self.__page_infos_key, page_infos) | 
|---|
| 342 | +    # end page methods # | 
|---|
| 343 | + | 
|---|
| 344 | +    # start wizard data utils # | 
|---|
| 345 | +    def _clear_wizard_data_from_session(self, session): | 
|---|
| 346 | +        """ | 
|---|
| 347 | +        Clear the session data used by this wizard from the provided session. | 
|---|
| 348 | +        """ | 
|---|
| 349 | +        del session[self.get_wizard_data_key(session)] | 
|---|
| 350 | + | 
|---|
| 351 | +    def _insert_wizard_data(self, session, key, data): | 
|---|
| 352 | +        """ | 
|---|
| 353 | +        Inserts wizard data into the provided session at the provided key. | 
|---|
| 354 | +        """ | 
|---|
| 355 | +        wizard_data = session[self.get_wizard_data_key(session)] | 
|---|
| 356 | +        wizard_data[key] = data | 
|---|
| 357 | +        session[self.get_wizard_data_key(session)] = wizard_data | 
|---|
| 358 | + | 
|---|
| 359 | +    def _del_wizard_data(self, session, key, page0): | 
|---|
| 360 | +        """ | 
|---|
| 361 | +        Deletes wizard data from the provided session at the key and page0 | 
|---|
| 362 | +        index. | 
|---|
| 363 | +        """ | 
|---|
| 364 | +        wizard_data = session[self.get_wizard_data_key(session)] | 
|---|
| 365 | +        sub_set = wizard_data[key] | 
|---|
| 366 | +        if len(sub_set) > page0: | 
|---|
| 367 | +            del sub_set[page0] | 
|---|
| 368 | +            wizard_data[key] = sub_set | 
|---|
| 369 | +            session[self.get_wizard_data_key(session)] = wizard_data | 
|---|
| 370 | + | 
|---|
| 371 | +    # end wizard data utils # | 
|---|
| 372 | + | 
|---|
| 373 | +    # typically overriden methods # | 
|---|
| 374 | +    def get_wizard_data_key(self, session): | 
|---|
| 375 | +        """ | 
|---|
| 376 | +        Return a session key for this wizard.  The provided session could be | 
|---|
| 377 | +        used to prevent overlapping keys in the case that someone needs | 
|---|
| 378 | +        multiple instances of this wizard at one time. | 
|---|
| 379 | +        """ | 
|---|
| 380 | +        return 'session_wizard_data' | 
|---|
| 381 | + | 
|---|
| 382 | +    def get_URL_base(self, request, page0): | 
|---|
| 383 | +        """ | 
|---|
| 384 | +        Return the URL to this wizard minus the "page0" parto of the URL.  This | 
|---|
| 385 | +        value is passed to template as url_base. | 
|---|
| 386 | +        """ | 
|---|
| 387 | +        return request.path.replace("/" + str(page0), "/") | 
|---|
| 388 | + | 
|---|
| 389 | +    def get_page_title(self, session, page0): | 
|---|
| 390 | +        """ | 
|---|
| 391 | +        Return a user friendly title for the page at index page0. | 
|---|
| 392 | +        """ | 
|---|
| 393 | +        return 'Page %s' % str(page0 + 1) | 
|---|
| 394 | + | 
|---|
| 395 | +    def process_show_form(self, request, page0, form): | 
|---|
| 396 | +        """ | 
|---|
| 397 | +        Called before rendering a form either from a GET or when a form submit | 
|---|
| 398 | +        is invalid. | 
|---|
| 399 | +        """ | 
|---|
| 400 | + | 
|---|
| 401 | +    def preprocess_submit_form(self, request, page0, form): | 
|---|
| 402 | +        """ | 
|---|
| 403 | +        Called when a form is POSTed, but before form is validated.  If this | 
|---|
| 404 | +        function returns None then form submission continues, else it should | 
|---|
| 405 | +        return a new page index that will be redirected to as a GET. | 
|---|
| 406 | +        """ | 
|---|
| 407 | + | 
|---|
| 408 | +    def process_submit_form(self, request, page0, form): | 
|---|
| 409 | +        """ | 
|---|
| 410 | +        Called when a form is POSTed.  This is only called if the form data is | 
|---|
| 411 | +        valid.  If this method returns True, the done() method is called, | 
|---|
| 412 | +        otherwise the wizard continues.  Note that it is possible that this | 
|---|
| 413 | +        method would not return True, and done() would still be called because | 
|---|
| 414 | +        there are no more forms left in the form_list. | 
|---|
| 415 | +        """ | 
|---|
| 416 | + | 
|---|
| 417 | +    def get_template(self, page0): | 
|---|
| 418 | +        """ | 
|---|
| 419 | +        Hook for specifying the name of the template to use for a given page. | 
|---|
| 420 | +        Note that this can return a tuple of template names if you'd like to | 
|---|
| 421 | +        use the template system's select_template() hook. | 
|---|
| 422 | +        """ | 
|---|
| 423 | +        return 'forms/session_wizard.html' | 
|---|
| 424 | + | 
|---|
| 425 | +    def done(self, request): | 
|---|
| 426 | +        """ | 
|---|
| 427 | +        Hook for doing something with the validated data. This is responsible | 
|---|
| 428 | +        for the final processing including clearing the session scope of items | 
|---|
| 429 | +        created by this wizard. | 
|---|
| 430 | +        """ | 
|---|
| 431 | +        raise NotImplementedError("Your %s class has not defined a done() " + \ | 
|---|
| 432 | +                                  "method, which is required." \ | 
|---|
| 433 | +                                  % self.__class__.__name__) | 
|---|