Being two weeks into my Django initiation, I'm quickly learning that if something seems really hard to do the odds are it's being done wrong. Yesterday, while trying to set up a public-facing form for a recipes app, I ran into the issue of combining Django's ModelForm (for the Recipe model) with an inline for the foreign key Ingredients. Coming from a PHP background, I was thinking about manually creating a set of ingredients-related fields and then parsing them on postback. One of my astute coworkers (there are many of them) pointed out that this was the ideal use-case for the inline formset factory, which allows you to add foreign key fields to a ModelForm. So I began googling -- and didn't find much. In the interests of helping out someone with a similar problem, here's what I did.

Assume you've got a couple of models - one to many relationship. We'll call them Recipe and RecipeIngredient.

class Recipe(models.Model):
    pub_date = models.DateTimeField('Date Published', auto_now_add = True)
    title = models.CharField(max_length=200)
    instructions = models.TextField()

class RecipeIngredient(models.Model):
    recipe = models.ForeignKey(Recipe, related_name="ingredients")
    ingredient = models.CharField(max_length=255)

These look ok (of course you might want to add some fields for amount, or unit, but you get the idea). Now we need to create a public-facing form to allow users to submit recipes.

from django import forms
from recipes.models import Recipe, RecipeIngredient
from django.forms.models import inlineformset_factory

MAX_INGREDIENTS = 20

IngredientFormSet = inlineformset_factory(Recipe, 
    RecipeIngredient, 
    can_delete=False,
    extra=MAX_INGREDIENTS)

class UserSubmittedRecipeForm(forms.ModelForm):
    class Meta:
        model = Recipe
        exclude = ('pub_date', )

This should look pretty straightforward (believe it or not it took me a long time to get to this point). The ModelForm (UserSubmittedRecipeForm) and the IngredientFormSet will both be passed to the template and put in the same form tag, but first I'll show you the view:

from django.template import RequestContext
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.core.urlresolvers import reverse
from recipes.models import Recipe, RecipeIngredient
from recipes.forms import UserSubmittedRecipeForm, IngredientFormSet

def submit_recipe(request):
    if request.POST:

        form = UserSubmittedRecipeForm(request.POST)
        if form.is_valid():
            recipe = form.save(commit=False)
            ingredient_formset = IngredientFormSet(request.POST, instance=recipe)
            if ingredient_formset.is_valid():
                recipe.save()
                ingredient_formset.save()                
                return HttpResponseRedirect(reverse('recipes_submit_posted'))
    else:
        form = UserSubmittedRecipeForm()
        ingredient_formset = IngredientFormSet(instance=Recipe())
    return render_to_response("recipes/submit.html", {
        "form": form,
        "ingredient_formset": ingredient_formset,
    }, context_instance=RequestContext(request))

I was having a hard time wrapping my head around how to save the recipe, get the id (for the ingredients), and then validate the ingredients. Django does it all for you -- just pass commit=False when the Recipe ModelForm gets saved, and then save the recipe after validating Ingredients.

Lastly, here's a peek a the form section of my template:

<form action="." method="post" class="recipe_form">
    <table>
      {{ form.as_table }}
      {{ ingredient_formset.as_table }}
      <tr>
        <th></th>
        <td><input type="submit" name="submit" value="Submit" class="button"></td>
      </tr>
    </table>
  </form>

That's all there is to it. Hopefully this helps someone out there with the same problem!

Comments (0)

Commenting has been closed, but please feel free to contact me