Filter dropdown options in Django inline formset based on attribute of through model

I have a basic restaurant inventory tracking app that allows the user to create ingredients, menus, and menu items. For each menu item, the user can select the required ingredients for the menu item, along with a quantity required per ingredient.

Menu items have a many-to-many relationship with ingredients, and are connected via an “IngredientQuantity” through table.

PROBLEM: I can’t figure out how to filter the Ingredients list shown in the dropdown list in my inline formset to only show the ingredients created by the currently logged in user, despite including a queryset filter in my inline formset definition.

Here are my models:

class Ingredient(models.Model):
    GRAM = 'Grams'
    OUNCE = 'Ounces'
    PIECE = 'Pieces'

    UNIT_CHOICES = [
        ('Grams', 'Grams'),
        ('Ounces', 'Ounces'),
        ('Pieces', 'Pieces')
    ]

    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) 
    name = models.CharField(max_length=200)
    unitType = models.CharField(max_length=200, choices=UNIT_CHOICES, verbose_name='Unit')
    unitCost = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Unit Cost')
    inventoryQuantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Quantity')

    def __str__(self):
        return self.name + ' (' + self.unitType + ')'

class Menu(models.Model):
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) 
    name = models.CharField(max_length=200)
    timeCreated = models.DateTimeField(auto_now_add=True)
    timeUpdated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return str(self.name)

class MenuItem(models.Model):
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    ingredients = models.ManyToManyField(Ingredient, through='IngredientQuantity')
    menu = models.ForeignKey(Menu, on_delete=models.CASCADE, null=True)

    def __str__(self):
        return self.name

class IngredientQuantity(models.Model):
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) 
    ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE)
    menuItem = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
    ingredientQuantity = models.IntegerField(default=0)

    def __str__(self):
        return str(self.ingredient)

This is a multi-user app, so when a user creates a new menu item and assigns ingredients to it, they should only have the option of choosing ingredients THEY have created, not those of other users. Here is my attempt to do that in my views:

def ItemUpdate(request, pk):
    item = MenuItem.objects.get(id=pk)
    user = request.user

    IngredientQuantityFormset = inlineformset_factory(
        MenuItem, IngredientQuantity, fields=('ingredient', 'ingredientQuantity'), can_delete=True, extra=0
    )

    form = ItemCreateForm(instance=item)
    formset = IngredientQuantityFormset(instance=item, queryset=IngredientQuantity.objects.filter(ingredient__user=user))

    if request.method == 'POST':
        form = ItemCreateForm(request.POST, instance=item)
        formset = IngredientQuantityFormset(request.POST, instance=item, queryset=IngredientQuantity.objects.filter(ingredient__user=user))

# rest of view...

I’ve searched everywhere for how to implement the queryset parameter properly, but I cannot get it to work. When creating a menu item, the user still has the ability to choose from every ingredient in the database - including the ones created by other users - which is not what I want. I would like the user to only be able to choose from the ingredients they themselves created.

Does anyone know how to do this properly? Thank you!

Hi @emikes919 — Welcome!

OK, so the dropdown will be a ModelChoiceField(). That takes a queryset kwarg. What you’ll need to do is pass the request into your form (maybe via get_form_kwargs) so that you can set the right queryset from request.user in your form’s __init__() method.

Thanks so much Carl! I’m new to Django so this really helps point me in the right direction. Let me take this away and come back if I have any follow up questions!

Hello again Carlton (apologies for spelling your name wrong previously :slight_smile: )

I figured it out!

I had to create a new form and override the __init__ method like you said. The newly created form inherits the ModelForm class. I knew I was going to be using the “ingredient” field from my model when constructing the inline formset, so I filtered that field in the __init__ method by the current user’s Ingredient objects.

Sidebar: It turns out that adding a specific ModelChoiceField wasn’t necessary, but I added it anyway to fill in the empty_label placeholder text for styling purposes.

I then had to pass the newly created form into the inlineformset_factory construction function with form=.

Finally, when creating the formset in my views, I had to pass in a new form_kwargs={'user': request.user} argument.

No changes were required to my models.

Here are is my updated forms.py (I moved the inlineformset_factory function out of views and into forms):

class ItemIngredientForm(forms.ModelForm):
    def __init__(self, *args, user, **kwargs):
        self.user = user
        super().__init__(*args, **kwargs)
        self.fields['ingredient'].queryset = Ingredient.objects.filter(user=self.user)

    ingredient = forms.ModelChoiceField(queryset=Ingredient.objects.all(), empty_label="Select an ingredient...") # added this for styling purposes only, the queryset arguement here doesn't appear to do anything given the presence of the queryset in the __init__ function, although including the queryset here seems to be required to get the code to work

IngredientQuantityFormset = inlineformset_factory(
    MenuItem, IngredientQuantity, form=ItemIngredientForm, fields=('ingredient', 'ingredientQuantity'), can_delete=True, extra=0
)

And here is my updated views.py:

def ItemUpdate(request, pk):
    item = MenuItem.objects.get(id=pk)
    user = request.user

    form = ItemCreateForm(instance=item)
    formset = IngredientQuantityFormset(instance=item, form_kwargs={'user': request.user})

    if request.method == 'POST':
        form = ItemCreateForm(request.POST, instance=item)
        formset = IngredientQuantityFormset(request.POST, instance=item, form_kwargs={'user': request.user})

# rest of view...

Thanks so much for pointing me in the right direction here! I was stuck on this for weeks! Feels good to get moving again!

1 Like

Glad you got it working @emikes919! :tada: