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:
Follow @vasylOk
Here is workaround:
-
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. - To pass step and form to render_done, our implementation of wizard's done method contain such code:
-
In the get_context_data we initialize context variable has_errors:
if self.steps.current == 'last': context['has_errors'] = self.has_errors
- 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 %}
{% else %} {% 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.
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)