Model Formsets for 3-way relationships (Model->ManyToMany->ForeignKey)?

Hey everyone! I’m trying to build a form that will create/update a Model “Recipe”. Inside that Recipe is a many_to_many “RecipeFermentable” which uses a ForeignKey “Fermentable”.

Using basic formsets, I can build it, but I’m having a uber-hard time “saving” this Recipe object or even updating an existing object. I figured I should move to ModelForms. But, I’m not sure how. All examples and docs show 2-levels down (Model-to-Model). However, I’m trying to do Model-to-Model-to-Model.

Here are some snippets:

models.py:

class Recipe(models.Model):
    def __str__(self):
        return self.name

    class Meta:
        unique_together = ('name', 'version')

    bs_file = models.FileField(storage=fs,null=True)
    name = models.CharField(max_length=75)
    dateCreated = models.DateField()
    notes = models.TextField()
    fermentables = models.ManyToManyField(RecipeFermentable)
    adjuncts = models.ManyToManyField(RecipeAdjunct)
    yeasts = models.ManyToManyField(RecipeYeasts)

class RecipeFermentable(models.Model):
    amount_metric = models.FloatField(default=0) # Kilograms / Liters
    notes = models.CharField(max_length=200, null=True, blank=True)
    fermentable = models.ForeignKey(Fermentable, on_delete=models.RESTRICT)

class Fermentable(RecipeItem):
    type = models.ForeignKey(FermentableType,on_delete=models.CASCADE)
    sugar_content = models.FloatField()  # Brix

class RecipeItem(models.Model):
    class Meta:
        abstract = True

    name = models.CharField(max_length=75)
    supplier = models.CharField(max_length=75, null=True, blank=True)
    description = models.CharField(max_length=200, null=True, blank=True)

Ergo, Recipe → RecipeFermentable (many) → Fermentable (one)

I’m having a doggone time trying to figure this out. I’ve got my view where I can build a list of Fermentables, in a formset, but it’s a generic formset that I’m having a hard time tying to the actual recipe when saving. It’s all in the same form

Here’s my question. Is the only/best way to do this is using the base formset_factory()? If so, I’ll keep trying my way through it (can’t use ‘instance’). But, was hoping ModelForms could be used to maintain those relationships. I just can’t seem to wrap my head around 3-level model relationships to do it.

It’s highly likely I’m overthinking this. Staring too much at code (tunnel vision)

Current view doesn’t render due to errors, likely because of trying to load instance data the way I’m trying

views.py:

    if request.method == "GET":
        form = RecipeAddForm()
        fermentable_set = formset_factory(FermentableForm)
        adjunct_set = formset_factory(AdjunctForm)
        yeast_set = formset_factory(YeastForm)
        if pk:
            # We have a recipe to edit.  Load it up
            recipe = Recipe.objects.get(pk=pk)
            form = RecipeAddForm(initial=model_to_dict(recipe))
            fermentable_set = fermentable_set(recipe.fermentables.all().values(),prefix="ferm")
            adjunct_set = adjunct_set(recipe.adjuncts.all().values(), prefix="adj")
            yeast_set = yeast_set(recipe.yeasts.all().values(), prefix="yeast")
        context = {'form': form, 'fermentable_set': fermentable_set,
                   'adjunct_set': adjunct_set,
                   'yeast_set': yeast_set}

        return render(request, 'batchthis/addRecipe.html', context)

I haven’t had the time to really look at this in detail yet, but my initial reaction is that you would end up creating multiple formsets, one for each entry of the intermediate model.

In other words, I’d be looking at nested loops, where I create a formset for Fermentable for each instance of RecipeFermentable, “attached” to that instance.

Thanks, Ken! I just recently learned that nested formsets were a thing (I wasn’t sure iterating them would affect the update to DB). Looking into this more.

Sadly, all the examples I’m reading and trying to understand are not many-to-many. They are all inline_formsets() requiring a Foreign Key.

I have a Foreign Key in a model that’s a Many-to-Many in other model.

In my models above, I have a Recipe ----- (ManyToMany) ----> RecipeFermentable ----- (ForeignKey) —> Fermentable.

What I’m trying to accomplish is inside my RecipeForm, have a formset for RecipeFermentable where I have a selectionList of Fermentables. My Javascript to add more RecipeFermentables to Recipe works fine.

From what I understand, an inline_formset is only for ForeignKeys. I suppose, to make this better understandable, you can compare this to a recipe for a Meal:

Recipe ----> IngredientAmounts —> Ingredient. The only difference here, is that I can have duplicate ingredients, requiring the many-to-many.

I’m sorry, I’m not following what your question or issue is at this point.

So taking a step back and starting from the basics as written in the docs:

Inline formsets is a small abstraction layer on top of model formsets. These simplify the case of working with related objects via a foreign key.

Now, keep in mind that a many-to-many relationship exists as the relationship between the two related models through a “join” table. This join table exists as a model with at least two fields - one foreign key to each of the two models being related.

In this case, you have:

class Recipe:
  ...

class RecipeFermentable:
  ...

and Django provides something like:

class Recipe_To_RecipeFermentable:  # Totally fabricated name
  recipe = ForeignKey(Recipe, ...)
  recipe_fermentable = ForeignKey(RecipeFermentable)

If nothing else then you have the basis for using a formset from Recipe to the join table, where the fields being rendered come from the relationship from that join table to RecipeFermentable.

So yes, it will work - you may just need to work with the formset at a deeper level.

I don’t know how close this may be, but you may find this thread helpful as well. It may give you some ideas.

I have read that post before in my google-fu searches. I’ll be honest, I didn’t follow it to full understanding. But, your explanation of the join table makes sense. I’ll just have to do what most of us does… code until it works! haha.

I’ll circle back around and close this up once I find the solution. Thanks again.

I have the same problem: Recipe: RecipeIngredient (intermediate table with amount) : Ingredient.

It all work perfectly in the admin, but I can’t figure out how to do the same using forms on my site.

You can see that you can add/remove ingredients in a recipe with an amount tied to it in the intermediate table. It is done with a TabularInline that is tied to the base model (Recipe in this case)

class RecipeIngredientInline(admin.TabularInline):
    model = RecipeIngredient
    fields = ('ingredient', 'amount')
    extra = 3 # how many rows to show by default

class OrderRecipeInline(admin.TabularInline):
    model = OrderRecipe
    fields = ('quantity',)

class RecipeAdmin(admin.ModelAdmin):
    list_display = ('name', 'description')
    inlines = [RecipeIngredientInline,
                OrderRecipeInline]

I can’t find a way to do the same thing in forms.
I hope this get us closer to the solution we need.