1

I am creating a form that creates a dynamic number of models. FormSets seem like the tool for the job, but I can't find examples that use multiple models.

Using the models in this example (Author and Book), My formset needs exactly one Author form, and at least one Book form. The user can add as many books as they would like to the author, all within the same form(set).

What is the best way to accomplish this in the Django backend?

Would I be better off posting JSON to a REST api to create these models?

roob
  • 2,419
  • 3
  • 29
  • 45

1 Answers1

2

This example is what you are looking for. It explains how to use inline formsets to populate models parents and children:

# models.py
from django.db import models

class Recipe(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField()


class Ingredient(models.Model):
    recipe = models.ForeignKey(Recipe)
    description = models.CharField(max_length=255)


class Instruction(models.Model):
    recipe = models.ForeignKey(Recipe)
    number = models.PositiveSmallIntegerField()
    description = models.TextField()

# forms.py
from django.forms import ModelForm
from django.forms.models import inlineformset_factory

from .models import Recipe, Ingredient, Instruction


class RecipeForm(ModelForm):
    class Meta:
        model = Recipe


IngredientFormSet = inlineformset_factory(Recipe, Ingredient)
InstructionFormSet = inlineformset_factory(Recipe, Instruction)

# views.py
from django.http import HttpResponseRedirect
from django.views.generic import CreateView

from .forms import IngredientFormSet, InstructionFormSet, RecipeForm
from .models import Recipe


class RecipeCreateView(CreateView):
    template_name = 'recipe_add.html'
    model = Recipe
    form_class = RecipeForm
    success_url = 'success/'

    def get(self, request, *args, **kwargs):
        """
        Handles GET requests and instantiates blank versions of the form
        and its inline formsets.
        """
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        ingredient_form = IngredientFormSet()
        instruction_form = InstructionFormSet()
        return self.render_to_response(
            self.get_context_data(form=form,
                                  ingredient_form=ingredient_form,
                                  instruction_form=instruction_form))

    def post(self, request, *args, **kwargs):
        """
        Handles POST requests, instantiating a form instance and its inline
        formsets with the passed POST variables and then checking them for
        validity.
        """
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        ingredient_form = IngredientFormSet(self.request.POST)
        instruction_form = InstructionFormSet(self.request.POST)
        if (form.is_valid() and ingredient_form.is_valid() and
            instruction_form.is_valid()):
            return self.form_valid(form, ingredient_form, instruction_form)
        else:
            return self.form_invalid(form, ingredient_form, instruction_form)

    def form_valid(self, form, ingredient_form, instruction_form):
        """
        Called if all forms are valid. Creates a Recipe instance along with
        associated Ingredients and Instructions and then redirects to a
        success page.
        """
        self.object = form.save()
        ingredient_form.instance = self.object
        ingredient_form.save()
        instruction_form.instance = self.object
        instruction_form.save()
        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form, ingredient_form, instruction_form):
        """
        Called if a form is invalid. Re-renders the context data with the
        data-filled forms and errors.
        """
        return self.render_to_response(
            self.get_context_data(form=form,
                                  ingredient_form=ingredient_form,
                                  instruction_form=instruction_form))

<!-- recipe_add.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Multiformset Demo</title>
    <script src="{{ STATIC_URL }}js/jquery.min.js"></script>
    <script src="{{ STATIC_URL }}js/jquery.formset.js"></script>
    <script type="text/javascript">
        $(function() {
            $(".inline.{{ ingredient_form.prefix }}").formset({
                prefix: "{{ ingredient_form.prefix }}",
            })
            $(".inline.{{ instruction_form.prefix }}").formset({
                prefix: "{{ instruction_form.prefix }}",
            })
        })
    </script>
</head>

<body>
    <div>
        <h1>Add Recipe</h1>
        <form action="." method="post">
            {% csrf_token %}
            <div>
                {{ form.as_p }}
            </div>
            <fieldset>
                <legend>Recipe Ingredient</legend>
                {{ ingredient_form.management_form }}
                {{ ingredient_form.non_form_errors }}
                {% for form in ingredient_form %}
                    {{ form.id }}
                    <div class="inline {{ ingredient_form.prefix }}">
                        {{ form.description.errors }}
                        {{ form.description.label_tag }}
                        {{ form.description }}
                    </div>
                {% endfor %}
            </fieldset>
            <fieldset>
                <legend>Recipe instruction</legend>
                {{ instruction_form.management_form }}
                {{ instruction_form.non_form_errors }}
                {% for form in instruction_form %}
                    {{ form.id }}
                    <div class="inline {{ instruction_form.prefix }}">
                        {{ form.number.errors }}
                        {{ form.number.label_tag }}
                        {{ form.number }}
                        {{ form.description.errors }}
                        {{ form.description.label_tag }}
                        {{ form.description }}
                    </div>
                {% endfor %}
            </fieldset>
            <input type="submit" value="Add recipe" class="submit" />
        </form>
    </div>
</body>
</html>

By doing it this way (assuming you are using class based views), it will allow you to define as many forms you want in your formset.

Have a look at the inlineformset_factory signature to see how you can define how many forms per formset you would like.

To handle the dynamic number of forms created, you can specify extra, min_num and max_num

Then for the display, using javascript is the most convenient option in my opinion. This is where you can find answers.

Ramon Melo
  • 270
  • 2
  • 9
  • 21
Val F.
  • 1,436
  • 2
  • 9
  • 18