Home > front end >  Create Django Form from ForeignKey
Create Django Form from ForeignKey

Time:11-11

I'm trying to create a simple Django app that allows users to create and edit Recipes which consist of Ingredients.

models.py

class Recipe(models.Model):
    name = models.CharField(max_length=100)
    description = models.CharField(max_length=1000)

class Ingredient(models.Model):
    api_id = models.IntegerField(primary_key=True) # id in Spoonacular database 
    name = models.CharField(max_length=100) # human-readable name 
    units = models.CharField(max_length=10) # unit of measure (e.g. 'oz', 'lb'), includes '' for 'number of' 
    amt = models.PositiveIntegerField() # number of units 
    recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)

I have a detail view that displays the Ingredients for a given Recipe, along with buttons to add and remove ingredients. The primary key of the recipe is in the URL.

view of detail page with add/remove ingredient buttons

I'm trying to get it so that when a user clicks on the 'Remove Ingredients' button, it redirects them to a form where they can select which Ingredients from the given Recipe they want to remove. This is the part I can't get to work. As best I can tell, this requires a ModelChoiceField with a queryset where the ForeignKey of the Ingredient matches the primary key of the Recipe ('recipe_pk'). It seems like I need to pass the primary key of the given Recipe into the Form via **kwargs in the constructor, and then have the Form select the appropriate Ingredients. However, this gives me an AttributeError with the description 'tuple' object has no attribute 'get' when I try to render the template. Here's a screenshot: AttributeError

Is this the right way to go about creating such a form, and if so any idea where this is going wrong?

forms.py

# Trying to create a form where a user can select any number of Ingredients 
#  associated with the Model 

class RemoveIngredientForm(forms.Form):
    to_remove = forms.ModelChoiceField(queryset=Ingredient.objects.all())
    def __init__(self, *args, **kwargs):
        super(RemoveIngredientForm, self).__init__(args, kwargs)
        recipe = Recipe.objects.get(pk=kwargs['recipe_pk'])
        self.fields['to_remove'].queryset = Ingredient.objects.filter(recipe=recipe)
views.py

...

def remove_ingredients(request, **kwargs):
    """
    GET: list of existing ingredients 
    POST: remove selected ingreident from model 
    """
    if request.method == 'POST':
        form = RemoveIngredientForm(request.POST, kwargs=kwargs)
        if form.is_valid():
            picked = form.cleaned_data.get('picked')
            return HttpResponse('How did I get here?') # Haven't gotten to this point yet 

    recipe = Recipe.objects.get(pk=kwargs['recipe_pk']) #'recipe_pk' is the primary key of the Recipe and lives in the URL 
    try: 
        ingredients = Ingredient.objects.filter(recipe=recipe)
    except Ingredient.DoesNotExist: 
        ingredients = None
    form = RemoveIngredientForm(**kwargs)
    context = { 
        'recipe': recipe,
        'form': form
    }
    return render(request, 'food/remove_ingredients.html', context=context)
<!-- food/remove_ingredients.html -->

<h1>Removing ingredients from {{recipe.name}}</h1>
<form method='POST'>
    {{ form.as_p }}
    <input type='submit' value='submit'>
</form>

CodePudding user response:

The choicefield won't work because it needs a dictionary of choices. What you want to do is display a listview of ingredients for a particular recipe with a delete button next to each ingredient which calls a deleteview on that particular ingredient. So you call your deleteingredients url with the pk of the recipe. Then in your function you pass the queryset of the ingredients to that recipe pk into the context. Then in your template render each ingredient with a button that calls delete on that particular ingredient with the pk of that ingredient. So no form is needed at all. You just need another view which deletes the ingredient and then redirects to the recipe.

  • Related