Using Class Based Views, ModelForms, and Inlline Formsets. I’m making a recipe application in Django. Each user has their own OneToOne RecipeBook object, which in turn can hold as many recipes as needed, as each Recipe has a ForeignKey relationship to the RecipeBook object. There are also Ingredient and Direction objects that each have a FK relationship to the Recipe object.
The good news is that I can create a Recipe object using my CreateView, with as many associated Ingredient and Direction objects as I want. The Ingredient/Direction objects should be unique to each Recipe object (and by extension, each User). However, when I create a Recipe object, and then I try to create a new Recipe object, its Ingredient and Direction fields are already populated on the new object, form the old object. So if I had just created a Recipe with the Ingredient/Direction fields all set to one, with 3 associated objects each, and then go to create another Recipe, the new Recipe object will have all blank fields, but will have 3 Ingredient/Direction objects all set to 1. This will happen to each user that is logged in. I want to make it so these objects are all staying together.
I think the issue to this is that either my get_context_data
or my form_valid
methods are saving the Ingredient/Direction objects globally, when I just want each Ingredient/Direction object to only be associated with the specific recipe object. I’ve tried messing with the init function of my Forms, I’ve tried querying for the object before/while its being created, and it seems like no matter what I do I’m just running in circles. I’d appreciate any help/resources anyone can point me towards!
My Models:
class RecipeBook(models.Model):
"""Each user has a single associated RecipeBook object, linked in this OneToOne field"""
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Recipe(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4())
recipebook = models.ForeignKey(RecipeBook, related_name='recipe_set', on_delete=models.CASCADE)
title = models.CharField(max_length=150, help_text='Title of the recipe')
description = models.TextField(help_text='Description of the recipe', blank=True)
# image = models.ImageField(height_field=, width_field=, help_text='Image of the recipe', blank=True)
servings = models.PositiveSmallIntegerField(help_text='The amount of servings the recipe will yield', default=0, blank=True)
prep_time = models.PositiveSmallIntegerField(help_text='The preparation time', default=0, blank=True)
cook_time = models.PositiveSmallIntegerField(help_text='The cooking time', default=0, blank=True)
url = models.URLField(blank=True)
TIME_UNITS = (
('m', 'Minutes'),
('h', 'Hours')
)
def get_absolute_url(self):
return reverse('recipe_book:recipe-detail', args=[str(self.id)])
def __str__(self):
return self.title
class Ingredient(models.Model):
recipe = models.ForeignKey(Recipe, related_name='ingredient_set', on_delete=models.CASCADE)
name = models.CharField(max_length=100)
amount = models.CharField(max_length=20, blank=True)
def __str__(self):
return self.name
class Direction(models.Model):
recipe = models.ForeignKey(Recipe, related_name='direction_set', on_delete=models.CASCADE)
step_instructions = models.TextField(help_text='Write the instructions of the step here')
My Forms:
class AddRecipeForm(ModelForm):
recipe = forms.ModelChoiceField(queryset=Recipe.objects.all())
class Meta:
model = Recipe
fields = ['title', 'description', 'servings', 'prep_time', 'cook_time', 'url']
class AddIngredientForm(ModelForm):
class Meta:
model = Ingredient
fields = ['name', 'amount']
# def __init__(self, *args, **kwargs):
# self.recipe = kwargs.pop('recipe')
# super(AddIngredientForm, self).__init__(*args, **kwargs)
#
# if not self.instance:
# self.fields['name'].initial = self.recipe.default_name
# self.fields['amount'].widget = forms.TextInput(required=False)
#
# def save(self, *args, **kwargs):
# self.instance.recipe = self.recipe
# ingredient = super(AddIngredientForm, self).save(*args, **kwargs)
# return ingredient
IngredientFormset = inlineformset_factory(Recipe, Ingredient, form=AddIngredientForm, extra=1, can_delete=True)
class AddDirectionForm(ModelForm):
class Meta:
model = Direction
fields = ['step_instructions']
# def __init__(self, *args, **kwargs):
# self.recipe = kwargs.pop('recipe')
# super(AddDirectionForm, self).__init__(*args, **kwargs)
#
# if not self.instance:
# self.fields['step_instructions'].initial = self.recipe.default_step_instructions
#
# def save(self, *args, **kwargs):
# self.instance.recipe = self.recipe
# direction = super(AddDirectionForm, self).save(*args, **kwargs)
# return direction
DirectionFormset = inlineformset_factory(Recipe, Direction, form=AddDirectionForm, extra=1, can_delete=True)
My View:
class RecipeListView(LoginRequiredMixin, generic.ListView):
model = models.Recipe
context_object_name = 'recipes'
# Using this method ensures that the only recipes that are displayed are the ones associated with each user
def get_queryset(self):
return models.Recipe.objects.filter(recipebook=self.request.user.recipebook)
class RecipeDetailView(LoginRequiredMixin, generic.DetailView):
model = models.Recipe
fields = ['title', 'description', 'servings', 'prep_time', 'cook_time', 'url']
context_object_name = 'recipe'
def get_queryset(self):
return models.Recipe.objects.filter(recipebook=self.request.user.recipebook)
# Classes used to actually create full recipe objects
class RecipeCreate(LoginRequiredMixin, CreateView):
model = models.Recipe
fields = ['title', 'description', 'servings', 'prep_time', 'cook_time', 'url']
def get_queryset(self):
return models.Recipe.objects.filter(recipebook=self.request.user.recipebook)
def get_context_data(self, **kwargs):
data = super(RecipeCreate, self).get_context_data(**kwargs)
#user = self.request.user
if self.request.POST:
data['ingredients'] = IngredientFormset(self.request.POST)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
data['directions'] = DirectionFormset(self.request.POST)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
else:
data['ingredients'] = IngredientFormset()
#queryset=models.Recipe.objects.filter(self.kwargs['id']))
data['directions'] = DirectionFormset()
#queryset=models.Recipe.objects.filter(self.kwargs['id']))
return data
def form_valid(self, form):
form.instance.recipebook = self.request.user.recipebook
context = self.get_context_data()
ingredients = context['ingredients']
directions = context['directions']
# self.object is the object being created
self.object = form.save()
if ingredients.is_valid():
ingredients.instance = self.object
ingredients.save()
if directions.is_valid():
directions.instance = self.object
directions.save()
return super(RecipeCreate, self).form_valid(form)
class RecipeUpdate(LoginRequiredMixin, UpdateView):
model = models.Recipe
fields = ['title', 'description', 'servings', 'prep_time', 'cook_time', 'url']
def get_context_data(self, **kwargs):
data = super(RecipeUpdate, self).get_context_data(**kwargs)
if self.request.POST:
data['ingredients'] = IngredientFormset(self.request.POST, instance=self.object)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
data['directions'] = DirectionFormset(self.request.POST, instance=self.object)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
else:
data['ingredients'] = IngredientFormset(instance=self.object)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
data['directions'] = DirectionFormset(instance=self.object)
#queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook))
return data
def form_valid(self, form):
form.instance.recipebook = self.request.user.recipebook
context = self.get_context_data()
ingredients = context['ingredients']
directions = context['directions']
self.object = form.save()
if ingredients.is_valid():
ingredients.instance = self.object
ingredients.save()
if directions.is_valid():
directions.instance = self.object
directions.save()
return super(RecipeUpdate, self).form_valid(form)
class RecipeDelete(LoginRequiredMixin, DeleteView):
model = models.Recipe
success_url = reverse_lazy('recipe_book:index')
context_object_name = 'recipe'
def form_valid(self, form):
form.instance.recipebook = self.request.user.recipebook
return super().form_valid(form)
My Template:
{% extends 'base-recipe.html' %}
{# https://simpleit.rocks/python/django/dynamic-add-form-with-add-button-in-django-modelformset-template/ #}
{% block content %}
<div class="container">
<div class="card">
<div class="card-header">Create Recipe</div>
<div class="card-body">
<form action="" method="POST"> {% csrf_token %}
{# table for the Recipe object, manually rendering it for more control #}
<table class="table">
<tr>
<td>{{ form.title.label_tag }}</td>
<td>{{ form.title }}</td>
</tr>
<tr>
<td>{{ form.description.label_tag }}</td>
<td>{{ form.description }}</td>
</tr>
<tr>
<td>{{ form.servings.label_tag }}</td>
<td>{{ form.servings }}</td>
</tr>
<tr>
<td>{{ form.prep_time.label_tag }}</td>
<td>{{ form.prep_time }}</td>
</tr>
<tr>
<td>{{ form.cook_time.label_tag }}</td>
<td>{{ form.cook_time }}</td>
</tr>
<tr>
<td>{{ form.url.label_tag }}</td>
<td>{{ form.url }}</td>
</tr>
</table>
{# table for the ingredient(s) object(s) #}
<table class="table">
{{ ingredients.management_form }}
{% for form in ingredients.forms %}
{% if forloop.first %}
<thead>
<tr>
{% for field in form.visible_fields %}
<th>{{ field.label|capfirst }}</th>
{% endfor %}
</tr>
</thead>
{% endif %}
<tr class="{% cycle row1 row2 %} formset_row-{{ ingredients.prefix }}">
{% for field in form.visible_fields %}
<td>
{# include the hidden fields in the form #}
{% if forloop.first %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
{{ field.errors.as_ul }}
{{ field }}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<table class="table">
{{ directions.management_form }}
{% for form in directions.forms %}
{% if forloop.first %}
<thead>
<tr>
{% for field in form.visible_fields %}
<th>{{ field.label|capfirst }}</th>
{% endfor %}
</tr>
</thead>
{% endif %}
<!--<tr class="{% cycle row1 row2 %} formset_row-{{ directions.prefix }}">-->
<tr class="formset_row-{{ directions.prefix }}">
{% for field in form.visible_fields %}
<td>
{# include the hidden fields #}
{% if forloop.first %}
{% for field in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
{{ field.errors.as_ul }}
{{ field }}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<input class="btn btn-primary" type="submit" value="Submit">
<a class="btn btn-danger" href="{% url 'recipe_book:index' %}">Back to the recipe list</a>
</form>
</div>
</div>
</div>
{% load static %}
<script src="{% static 'js/jquery.formsets.js' %}"></script>
<script type="text/javascript">
$('.formset_row-{{ ingredients.prefix }}').formset({
addText: 'Add Another Ingredient',
deleteText: 'Remove',
prefix: '{{ ingredients.prefix }}',
});
$('.formset_row-{{ directions.prefix }}').formset({
addText: 'Add another',
deleteText: 'Remove',
prefix: '{{ directions.prefix }}',
});
</script>
{% endblock %}