середа, 22 жовтня 2014 р.

Restarting Django SessionWizard from done method

For processing multi-steps forms Django has powerful WizardView, which "maintains state in one of the backends so that the full server-side processing can be delayed until the submission of the final form."(c) Wizard's done method is called, when data for every form is submitted and validated, but sometimes we need to go back to our wizard without lost of collected data, e.g. if we send information to third-party service and cannot do full validation before this action.
Here is workaround:

  1. Create subclass of WizardView and override render_done method in it, we will catch our special exception here, to have ability to go back. Create RevalidationError, subclass of Exception, which will be raised from done, if we have error here.
    from django.contrib.formtools.wizard.views import SessionWizardView
    
    
    class MySessionWizardView(SessionWizardView):
        """
        SessionWizardView modification with ability to go back to steps without data lost, if 
        any exception happens inside wizard's done method
        """
    
        def __init__(self, **kwargs):
            super(MySessionWizardView, self).__init__(**kwargs)
            self.has_errors = False
    
        class RevalidationError(Exception):
            def __init__(self, step, form, **kwargs):
                self.step = step
                self.form = form
                self.kwargs = kwargs
    
            def __repr__(self):
                return '%s(%s)' % (self.__class__, self.step)
            __str__ = __repr__
    
        def render_done(self, form, **kwargs):
            """
            This method gets called when all forms passed. The method should also
            re-validate all steps to prevent manipulation. If any form don't
            validate, `render_revalidation_failure` should get called.
            If everything is fine call `done`.
            """
            final_form_list = []
            # walk through the form list and try to validate the data again.
            for form_key in self.get_form_list():
                form_obj = self.get_form(step=form_key,
                    data=self.storage.get_step_data(form_key),
                    files=self.storage.get_step_files(form_key))
                if not form_obj.is_valid():
                    return self.render_revalidation_failure(form_key, form_obj, **kwargs)
                final_form_list.append(form_obj)
    
            try:
                done_response = super(MySessionWizardView, self).render_done(form, **kwargs)
            except self.RevalidationError as e:
                return self.render_revalidation_failure(e.step, e.form, **e.kwargs)
            self.storage.reset()
            return done_response
    
    Now, if we have error in the done method, we raise RevalidationError and wizard's render_revalidation_failure method is called with parameters from raised error.
  2. To pass step and form to render_done, our implementation of wizard's done method contain such code:
  3.         
    try:
        #main wizard forms handling: e.g. saving to db, call to third-party services
    except Exception as e:
        #we assume, that exception will be raised, if some trouble happen
        self.has_errors = True
        form = self.get_form(data=self.storage.get_step_data(self.steps.current))
        raise MySessionWizardView.RevalidationError(self.steps.current, form, error_message=e.message)
    
  4. In the get_context_data we initialize context variable has_errors:
     
    if self.steps.current == 'last':
        context['has_errors'] = self.has_errors
    
  5. We modificate last step template, it will have two modes: usual - form and error - message about exception and link to wizard. Sometimes last step is only preview step, so we need go to previous one to edit data and it looks so:
    {% if has_errors %}
        {% comment %} Your error message...{% endcomment %}
        
    {% csrf_token %} {{ wizard.management_form }}
    {% else %}
    {% csrf_token %} {{ wizard.management_form }} {% comment %}last wizard's step form fields goes here...{% endcomment %}
    {% endif %}
    If user entered some data at last step, and it's editing is needed, we can do the trick with javascript, just hide error block and show form after click on "edit" button.

3 коментарі: