Search code examples
pythondjangodjango-formsdjango-formwizard

Django Form Wizard: why done() method is not called after submitting?


I'm using a SessionWizardView and I can't understand why the done()method is never called. Instead, after posting my form, in the last step, I can see a POST HTTP 200 on my server, but this does nothing. The get_form() method works as expected.

I suspect a distraction error since I have the exact same logic for another view, and this works well.

Here is the whole code bellow.

The view

class DiscountsCreateView(PermissionRequiredCanHandleProducts,
                      ModelInContextMixin,
                      RestaurantMixin, SubSectionDiscounts,
                      SessionWizardView):
    """ Wizard view to create a discount in 2 steps """

    model = Discount  # used for model context
    form_list = [DiscountForm0, DiscountForm1]
    template_name = "discounts/discount_add.html"

    def get_form(self, step=None, data=None, files=None):
        form = super().get_form(step, data, files)

        if step is None:
            step = self.steps.current

        # step0 - name, kind, tax_rate
        # => nothing special to do, always the same form

        # step1 - specific fields related to the chosen kind
        if step == '1':
            step0_data = self.storage.get_step_data('0')
            kind = step0_data['0-kind']
            # combo => combo, combo_unit_price
            if kind == Discount.COMBO:
                form.fields['combo'].queryset = Combo.objects.restaurant(self.restaurant)
                # NOTE : this is not a scalable way to show/hide fields (exponential)
                form.fields['rebate_amount'].widget = forms.HiddenInput()
            elif kind == Discount.REBATE:
                form.fields['combo'].widget = forms.HiddenInput()
                form.fields['combo_unit_price'].widget = forms.HiddenInput()

        return form

    def done(self, form_list, **kwargs):
        data = [form.cleaned_data for form in form_list]
        try:
            Discount.objects.create(
                name=data[0]['name'],
                kind=data[0]['kind'],
                tax_rate=data[0]['tax_rate'],
                rebate_amount=data[1]['rebate_amount'],
                combo=data[1]['combo'],
                combo_unit_price=data[1]['combo_unit_price']
            )
        except Exception as e:
            messages.add_message(self.request, messages.ERROR, MSG_DISCOUNT_ADD_KO.format(e))
        else:
            messages.add_message(self.request, messages.SUCCESS, MSG_DISCOUNT_ADD_OK)

        return redirect(reverse('bo:discount-list'))

The forms

class DiscountForm0(forms.Form):
    name = forms.CharField(
        label=verbose_display(Discount, 'name'))
    kind = forms.ChoiceField(
        label=verbose_display(Discount, 'kind'),
        choices=Discount.KIND_CHOICES)
    tax_rate = forms.ModelChoiceField(
        label=verbose_display(Discount, 'tax_rate'),
        queryset=TaxRate.objects.all())


class DiscountForm1(forms.Form):
    """
    Contains all the specific fields for all discount kinds.
    The goal is to only show the fields related to the right discount kind
    """

    # For REBATE kind only
    rebate_amount = forms.DecimalField(
        label=verbose_display(Discount, 'rebate_amount'),
        validators=[MaxValueValidator(0)])

    # For COMBO kind only
    combo = forms.ModelChoiceField(
        label=verbose_display(Discount, 'combo'),
        queryset=Combo.objects.none()) 
    combo_unit_price = forms.DecimalField(
        label=verbose_display(Discount, 'combo_unit_price'),
        validators=[MinValueValidator(0)])

The templates

add_discount.html

{% extends "base_dashboard.html" %}
{% load verbose_name %}

{% block dashboard_title %}
    Créer une {% model_name model %} : étape {{ wizard.steps.step1 }} / {{ wizard.steps.count }}
{% endblock dashboard_title %}

{% block dashboard_content %}

    <form action='' method='post' novalidate>
        {% csrf_token %}
        {% include 'includes/_wizard_form_horizontal.html' with wizard=wizard %}
    </form>

{% endblock dashboard_content %}

_wizard_form_horizontal.html

{{ wizard.management_form }}
{% if wizard.form.forms %}
    {{ wizard.form.management_form }}
    {% for form in wizard.form.forms %}
        {% include 'includes/_form_horizontal.html' with form=form %}
    {% endfor %}
{% else %}
    {% include 'includes/_form_horizontal.html' with form=wizard.form %}
{% endif %}


{% if wizard.steps.prev %}
    <button class="btn btn-primary" name="wizard_goto_step" type="submit"
            value="{{ wizard.steps.prev }}">
        &laquo; étape précédente
    </button>
{% endif %}
<input type="submit" class="btn btn-primary" value="étape suivante &raquo;"/>

Solution

  • The done() method is always called if the form submitted in the last step is_valid(). So if it's not, it must mean your form isn't valid.

    In your case, you're hiding fields that are required by your DiscountForm1. So you're also hiding the error for these fields. You should make them optional and check in the form's clean() method if the appropriate fields are filled.