How to work with ModelFormSet in Form Wizard

1.9k views Asked by At

Django documentation is not very well documented on this subject. Indeed, the only reference they have in the docs is this paragraph:

How to work with ModelForm and ModelFormSet

WizardView.instance_dict. WizardView supports ModelForms and ModelFormSets. Additionally to initial_dict, the as_view() >method takes an instance_dict argument that should contain model instances for steps based on >ModelForm and querysets for steps based on ModelFormSet.

I haven't found any good and clear examples on how to use this. Can someone help me with this?

Specifically:

  • What to do in the forms.py?
  • What if I need a ModelFormSet only on certain steps of the form, not in all of them?
  • What do I need to do in the views.py and templates?

To put a use case and little project I'm working on a as an example I share my code:

  • Use case:
    • A user wants to register in a multistep form, in the first step he introduces his name and last name.
  • in the second step he introduces his passport number and a hotel registration, he also wants to register his son and wife, which are going with him to this hotel (here I want to use the modelformset from the HotelRegistration model).
  • in the third step he types his flight information. And then receives a confirmation message if the form is valid and saved in the database.

Here is my code:

models.py

class Event(models.Model):

    name = models.CharField(max_length=100)
    date_from = models.DateField(auto_now=False)
    date_to = models.DateField(auto_now=False)
    description = models.TextField()

    def __unicode__(self):
       return self.name


class Hotel(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=100)

    def __unicode__(self):
        return self.name


class HotelRegistration(models.Model):
    pax_first_name = models.CharField(max_length=50)
    pax_last_name = models.CharField(max_length=50)
    hotel = models.ForeignKey(Hotel)

    def __unicode__(self):
         return self.pax_first_name


class Registration(models.Model):

    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    passport = models.CharField(max_length=15)
    city_origin = models.CharField(max_length=50)
    flight_date_from = models.DateField(auto_now=False)
    flight_date_to = models.DateField(auto_now=False)
    require_transfer = models.BooleanField(default=None)
    event = models.ForeignKey(Event)
    hotel_registration = models.ForeignKey(HotelRegistration, blank=True, null=True)

    def __unicode__(self):
        return self.first_name

forms.py

from django import forms

from .models import Registration

class FormStep1(forms.ModelForm):
    class Meta:
        model = Registration
        fields = ['first_name', 'last_name']
        widgets = {
            'first_name': forms.TextInput(attrs={'placeholder': 'Nombre de la persona que   esta reservando', 'label_tag': 'Nombre'}),
        'last_name': forms.TextInput(attrs={'placeholder': 'Apellido'})
    }

class FormStep2(forms.ModelForm):
    class Meta:
        model = Registration
        fields = ['passport', 'hotel_registration']
        exclude = ('first_name', 'last_name', 'event' , 'city_origin', 'flight_date_from', 'flight_date_to', 'require_transfer')
        widgets = {
             'passport': forms.NumberInput(attrs={'placeholder':'Escriba su pasaporte'})
        }

class FormStep3(forms.ModelForm):
    class Meta:
        model = Registration
        fields = ['city_origin', 'flight_date_from', 'flight_date_to', 'require_transfer', 'event']
        exclude = ('first_name', 'last_name', 'hotel_registration')
        widgets = {
            'city_origin': forms.TextInput(attrs={'placeholder':'Ciudad desde donde esta viajando'}),
            'flight_date_from': forms.DateInput(format=('%d-%m-%Y'), attrs={'class':'myDateClass', 'placeholder':'Select a date'}),
            'flight_date_to': forms.DateInput(format=('%d-%m-%Y'), attrs={'class':'myDateClass', 'placeholder':'Select a date'}),
            'require_transfer': forms.Select(),
            'event': forms.Select()
    }

views.py

from django.shortcuts import render
from django.contrib.formtools.wizard.views import SessionWizardView
from django.http import HttpResponseRedirect
from django.views.generic import TemplateView
from django.forms.models import inlineformset_factory

from .models import Registration, HotelRegistration
from .forms import FormStep1, FormStep2, FormStep3


FORMS = [
    ("step1", FormStep1),
        ("step2", FormStep2),
        ("step3", FormStep3)
]

TEMPLATES = {
    "step1" : "wizard/step1.html",
    "step2" : "wizard/step2.html",
    "step3" : "wizard/step3.html"
}


class TestFormWizard(SessionWizardView):

    instance = None

def get_form_instance(self, step):
    if self.instance is None:
        self.instance = Registration()
    return self.instance


def get_form(self, step=None, data=None, files=None):
    form = super(TestFormWizard, self).get_form(step, data, files)
    HotelRegistFormSet = inlineformset_factory(HotelRegistration, Registration, can_delete=True, extra=1)

    # determine the step if not given
    if step is None:
        step = self.steps.current

    if step == '2':
        hotel_registration_formset = HotelRegistFormSet(self.steps.current, self.steps.files, prefix="step2")
    return form


def get_template_names(self):
    return [TEMPLATES[self.steps.current]]

def done(self, form_list, **kwargs):
    self.instance.save()
    return HttpResponseRedirect('/register/confirmation')


class ConfirmationView(TemplateView):
    template_name = 'wizard/confirmation.html'

Template

   {% extends "base.html" %}
   {% load i18n %}


   {% block content %}
   <p>Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}</p>
   <form action="" method="post">{% csrf_token %}
   <table>
        {{ wizard.management_form }}
             {% if wizard.form.forms %}
        {{ wizard.form.management_form }}
         {% for form in wizard.form.forms %}
             {{ form }}
         {% endfor %}
     {% else %}
         {{ wizard.form }}
     {% endif %}
    </table>
     {% if wizard.steps.prev %}
     <button name="wizard_goto_step" type="submit" value="{{ wizard.steps.first }}">{% trans   "first step" %}</button>
    <button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}">{% trans "prev step" %}</button>
    {% endif %}
    <input type="submit" value="{% trans "submit" %}"/>
    </form>
    {% endblock %}
1

There are 1 answers

0
Ian Price On

What to do in the forms.py?

Override the get_form_instance method of your SessionWizardView. This is the method the FormWizard uses to determine if a model instance is used w/ a model form

WizardView.get_form_instance(step) This method will be called only if a ModelForm is used as the form for step step. Returns an Model object which will be passed as the instance argument when instantiating the ModelForm for step step. If no instance object was provided while initializing the form wizard, None will be returned.

This can be done conditionally per step within the SessionWizardView implementation. I don't understand what you're trying to do well enough to give you an exact example, so here's a more generic example.

def get_form_instance(self, step):
    if step == u'3':
        past_data =  self.get_cleaned_data_for_step(u'2')
        hotel_name = past_data['hotel_field']
        hotel = Hotel.objects.get(name=hotel_name)
        return hotel #do NOT set self.instance, just return the model instance you want
    return self.instance_dict.get(step, None) # the default implementation

What if I need a ModelFormSet only on certain steps of the form, not in all of them?

See above; use the 'if step == (form/step name)' expression to determine what happens at each step.

What do I need to do in the views.py and templates?

Using a ModelForm and passing it a model object instance will set the initial form values. Do you need more?

Hopefully this shows you the structure expected within the FormWizard. More than any other part of Django I have used, FormWizard requires a very specific structure and is a monolithic class.