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.
I am trying to make a form that allows the user to create a Recipe, and then dynamically add/remove Ingredient/Direction objects as needed. I’m using Class Based Views, ModelForms, and inlineformsets_factory to accomplish this, as well as using the django-dynamic-formset jQuery plugin, and I’ve been following this guide.
My issue is that I am only able to create two child Ingredient/Direction objects for each recipe. I want to make it so users can create as many Ingredient/Direction objects as they like. Previously I was only able to add one instance of each child object, but through some changes I don’t recall, I am now able to add two. I previously thought that I would need to iterate over each object in my view to allow multiple objects, but since I can now create two I’m not sure if that is the case. So my question is, what is the correct way to allow users to create multiple child objects using inlineformsets?
My CreateView:
class RecipeCreate(LoginRequiredMixin, CreateView):
model = models.Recipe
fields = ['title', 'description', 'servings', 'prep_time', 'cook_time', 'url']
def get_context_data(self, **kwargs):
data = super(RecipeCreate, self).get_context_data(**kwargs)
if self.request.POST:
data['ingredients'] = IngredientFormset(self.request.POST)
data['directions'] = DirectionFormset(self.request.POST)
else:
data['ingredients'] = IngredientFormset()
data['directions'] = DirectionFormset()
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']
with transaction.atomic():
self.object = form.save()
if ingredients.is_valid() and directions.is_valid():
ingredients.instance = self.object
directions.instance = self.object
ingredients.save()
directions.save()
return super(RecipeCreate, self).form_valid(form)
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)
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_user_recipebook(sender, **kwargs):
"""Using a django signal to automatically create model object when user is created"""
if kwargs.get('created', False):
RecipeBook.objects.get_or_create(user=kwargs.get('instance'))
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)
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):
class Meta:
model = Recipe
fields = ['title',
'description',
'servings',
'prep_time',
'cook_time',
'url']
class AddIngredientForm(ModelForm):
class Meta:
model = Ingredient
fields = ['name', 'amount']
IngredientFormset = inlineformset_factory(Recipe, Ingredient, form=AddIngredientForm, extra=1, can_delete=True)
class AddDirectionForm(ModelForm):
class Meta:
model = Direction
fields = ['step_instructions']
DirectionFormset = inlineformset_factory(Recipe, Direction, form=AddDirectionForm, extra=1, can_delete=True)
And my template:
{% extends 'base-recipe.html' %}
{% block content %}
<form action="" method="POST"> {% csrf_token %}
{{ form.as_p }}
<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 field 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 }}">
{% 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>
{% load static %}
<script src="{% static 'js/jquery.formsets.js' %}"></script>
<script type="text/javascript">
$('.formset_row-{{ ingredients.prefix }}').formset({
addText: 'Add another',
deleteText: 'Remove',
prefix: '{{ ingredients.prefix }}',
});
$('.formset_row-{{ directions.prefix }}').formset({
addText: 'Add another',
deleteText: 'Remove',
prefix: '{{ directions.prefix }}',
});
</script>
{% endblock %}