Unable to add custom behaviors to django-formtools

This isn’t a core django issue, per se, but I thought someone may be able to help me here:

I’m using formtools to generate a multi-page wizard form with individual templates and the SessionWizardView

I’m able to successfully traverse the pages in the form list, but when trying to write custom code to save the form data at each step, the API doesn’t appear to behave as expected. I’m assuming I’ve missed an initialization step somewhere, but it isn’t immediately obvious. Here’s what I can say about my efforts so far:

  • I’m including the {{ wizard.management_form }} in each of the templates.
  • process_step() is only called at the last step of my 3-part form, not at each subsequent step. My understanding is it should be called after each form step. However, if that’s not the case…
  • If I try to save at the render() step, the raw POST data is available, but calling get_form_step_data() always returns an empty dictionary for the form I just completed.

From what I can deduce, I’m assuming that my data isn’t hooked into the Wizard’s session, but I’m unable to figure out what I might be missing that will allow me to access form data at each step.

Thanks.

Welcome @tbittner-cc !

Please post the code for the form(s) and view(s) being used by your wizard. We can’t determine what might be incorrect or missing from a description of that code.

Side Note: When posting code here, mark it as Preformatted text. This forces the forum software to keep your code properly formatted.

In the following code, the words render and POST print on every form page generation and post submission respectively. process_step only prints on the final page submission and the done() method never fires, even to throw an exception.

Any attempt to retrieve form data always returns an empty MultiValueDict

urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('wizard/',views.ListingWizard.as_view(views.FORMS), name='listing.wizard'),
]

forms.py

from django import forms
from .models import Listing

class LandlordApprovalForm(forms.Form):
    is_landlord_approved = forms.BooleanField(required=True,
        label="Has your landlord given you permission to sublet your unit?",
        initial=False,
        widget=forms.RadioSelect(choices=[(True, 'Yes'), (False, 'No')]))

class DateForm(forms.Form):
    start_date = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}))
    end_date = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}))

class RentForm(forms.Form):
    monthly_rent = forms.IntegerField(label='What is your portion of the monthly rent?')
    total_rent = forms.IntegerField(label="What is the total monthly rent for the unit among all tenants?")
    desired_rent = forms.IntegerField(label="What are you looking to receive in rent?")

views.py

from django.shortcuts import redirect,render

from formtools.wizard.views import SessionWizardView

from .forms import DateForm, LandlordApprovalForm, RentForm
from .models import Listing

FORMS = [
    ('landlordApproval', LandlordApprovalForm), 
    ('availabilityDates', DateForm), 
    ('rent', RentForm)]

TEMPLATES = {
            'landlordApproval': 'listing/landlord.html',
            'availabilityDates': 'listing/date.html',
            'rent': 'listing/rent.html'
        }

class ListingWizard(SessionWizardView):
    template_data = {
        'landlordApproval': {'form_header': 'Before we begin...'},
        'availabilityDates': {'form_header': 'Tell us about your place...'},
        'rent': {'form_header': 'Tell us about your place...'}
    }

    template_name = 'form_base.html'

    def get_context_data(self, form, **kwargs):
        context = super().get_context_data(form, **kwargs)
        context['template_data'] = self.template_data[self.steps.current]
        return context

    def render(self, form, **kwargs):
        print("render")
        if self.request.method == 'POST':
            print('POST')
            prev_form = self.get_form_step_data(self.get_form(self.steps.prev))
            print(prev_form)
        return super().render(form, **kwargs)

    def process_step(self, form, **kwargs):
        print(form)
        print("process step")
        return super().process_step(form, **kwargs)

    def done(self, form_list, **kwargs):
        print("done")

form_base.html

{% extends "base.html" %}

{% block content %}
<div class="modal-content flex justify-center items-center">
    <div class="bg-white rounded-lg shadow-lg p-4 w-7/8 md:w-1/3 lg:w-1/3 mt-8">
        <div class="text-center font-poppins text-lg md:text-2xl font-bold text-spothue mb-4">
            {{ template_data.form_header }}
        </div>
        <form method="POST">
            {% csrf_token %}
            {{ wizard.management_form }}
            {{ wizard.form }}
            <div class="flex justify-between">
                <div class="w-1/2 text-left">
                {% if wizard.steps.prev %}
                <button type='submit' name='wizard_goto_step' value='{{ wizard.steps.prev }}' class="text-lg">
                    <i class="text-spothue bi bi-arrow-left-circle-fill"></i>
                    <span>Previous</span>
                </button>
                {% endif %}
                </div>
                <div class="w-1/2 text-right">
                {% if wizard.steps.next %}
                <button type='submit'name='wizard_goto_step' value='{{ wizard.steps.next }}' class="text-lg">
                    <span>Next</span> 
                    <i class="text-spothue bi bi-arrow-right-circle-fill"></i>
                </button>
                {% else %}
                <button type='submit' class=" bg-spothue border-yellow-400 border-2 text-white rounded-md px-2 py-1 text-lg">
                    Submit 
                </button>
                {% endif %}
                </div>
            </div>
        </form>
    </div>
</div>
{% endblock %}

Also,

Django==5.1.5
django-formtools==2.5.1

and I’ve confirmed that I I’ve got formtools listed in my INSTALLED_APPS

You’ve got a couple different issues here.

The biggest one is that you’re not rendering a “Submit” button for your form.

The purpose of the “Next” button is to take you to the next step in the wizard without handling the form as a submission, so pressing next bypasses the “POST” from being processed. (See the WizardView.post method.)

So you always need a “Submit” button for form data to be processed.

Next, the get_form_step_data doesn’t work the way you seem to want it to work in your render function in that it doesn’t retrieve the data for that form step from the session. You would need to bind the form with the data for this to work - which is just doing excess work for nothing because if you have the data to bind it, you don’t need to use the get_form_step_data to get the data…
I believe the easiest way to get the data for an arbitrary step would be to call self.storage.get_step_data(self.steps.prev)

Next, as an artifact of how Django forms handle the forms.BooleanField, it must be defined as required=False. (See Form fields | Django documentation | Django)