My forms are being populated with the data that was entered in the last CreateView.

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 %}

Unfortunately, I am currently unable to recreate the symptoms you’re reporting with the code you’ve supplied here - either through a web interface or using shell_plus.

Is there any chance that there’s other code involved that might be affecting this? Either something like a data or page cache, or some other processing that you’ve trimmed because it didn’t appear relevant? (Is it possible that there’s some sort of browser cache involved here?)

1 Like

Hi Ken, thanks again for the reply. I’ve tried clearing my browser cache several times, tried different browsers, and I’m still getting the same behavior from them all. I haven’t trimmed much, just a DeleteView and a signal handler, but I don’t think those should be a part of the issue. I can’t think of any other things that might be affecting this, but them again, I am rather new to this. If you want to look through all of my code unmodified, here is a link to my github branch for it (the user/password is test/test): https://github.com/harrybeards/BSIT-Capstone/tree/jud

Fantastic, that helped a lot. I’m seeing what you’re talking about now and can recreate it.

While I don’t have an answer for you at the moment, I can also confirm that it is something being stored internally that is causing this. Additional behavior that I have noticed:

  • Hitting refresh when it occurs doesn’t change anything (as you’ve noted)
  • Stopping the app and restarting it does clear the forms.
  • Continuing the “create” process with those forms present and populated result in an error when submitted.
  • Browsing and selecting a different recipe to edit, and then going back to create does not change the form data being added - it’s always the last recipe created, not edited.
1 Like

Yep, those are also the behaviors I’m seeing as well. I’m glad its not just me that can’t seem to figure out whats going on!

Maybe I’m barking up the wrong tree here, but I was thinking that a good way to solve this issue would be to use a queryset in the get_context_data method, and try to query for the recipe object. In my other methods, like the DetailView and ListView I used this method to make sure that the only recipes showing up would be the ones associated with each user:

    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)**

The meat of this is accessing the Recipe object by using the self.request method, and extending it to self.request.user.recipebook as you can always access the user object from self.request. And because I have a OneToOne key for the users, making each User have one associated RecipeBook, that (apparently) means that I can access the recipebook object from the self.request.user method. However, no matter what I’ve tried, I can’t access a Recipe object from the self.request method (I’ve tried queries such as self.request.user.recipebook.recipe, self.request.user.recipebook.recipe_id, and self.request.user.recipebook.recipe_set). But I think the issue there is that because the Recipe objects only have a foriegn key relationship to the RecipeBook object, I can’t access the Recipe objects through the parent RecipeBook objects. And because of that issue, I also can’t access the ingredient objects either. However, what I find most confusing about this issue is that it is only with the Ingredient/Direction objects. The above method for the ListView successfully works for the recipe objects, even though they only have a FK to the RecipeBook objects. However, if I replace the models.Recipe.objects.filter with models.Ingredient.objects.filter, it doesn’t work. I suspect that this might be because I can query the RecipeBook object through the self.request method, and get the associated Recipe object. But if the Recipe object only has a FK, like the Ingredient/Direction objects, why can I access the Recipe object, but not the Ingredient/Direction objects?

So I don’t think trying to make the Recipe objects have a OneToOne key to the RecipeBook objects would be a good idea, because I want the users to be able to create as many Recipes as they want. However, I need a way to make sure that when a user is accessing their Recipe view, they only see their associated Ingredients. With the get_context_data method, I’ve tried using a queryset on the inlineformset_factory object, such as:

        if self.request.POST:
            data['ingredients'] = IngredientFormset(self.request.POST,
                queryset=models.Recipe.objects.filter(recipebook=self.request.user.recipebook)) 

and

        if self.request.POST:
            data['ingredients'] = IngredientFormset(self.request.POST,
                queryset=models.Ingredient.objects.filter(recipebook=self.request.user.recipebook)) 

but nothing works. Sorry for the wall of text, I just figured since we’re both having issues with this, I’d lay out my thought process and what I’ve tried and what has/hasn’t worked.

I haven’t had time to fully digest everything you’ve written here - but there are a couple quick thoughts that come to mind.

  • Always keep in mind that for every request, a new instance of a View class is created. Nothing is supposed to be retained between requests outside of what’s stored in the database. (It is possible to store data at the module layer such that it becomes available for all new requests - but that’s something different and generally speaking not something you would do regularly since that’s a global issue and not restricted to any one request or user.)

  • In a Create view, there is no “pre-existing” data. You shouldn’t be querying for anything when prepping the view. Why? Because the entire purpose of the create view is to add new data. This holds true for the ingredients and directions as well, because they exist in a many-to-one relationship. (Each ingredient can only be related to one recipe. If you’re going to use the same ingredient in multiple recipes, you’d have to convert this to a many-to-many relationship. Otherwise, you need to duplicate those ingredients for each recipe.)

  • Outside the parameters being passed through the URL (or possibly data stored in the database or cache), there is no information that comes from your list view that would be available in the Create view.

More to come later…

1 Like

Awesome, thanks for the pointers! I didn’t know that about the Views/CreateViews. And again, my apologies for the huge post, thanks for taking time out of your day to help!

So I’m wondering if this is even an issue with Django. I’m using a jQuery library called django-dynamic-formset which allows me to dynamically add/create objects, and I’m using them for the inline-formsets. I didn’t think that it would’ve been a jQuery issue, because I assumed that being JS, it isn’t stored permanently, which is whats happening across each of the createviews. But I’m also not very knowledgeable about JS. Do you think there could be an issue with the plugin?

Nope, it’s not the plug in. The view is creating these formsets pre-populated, long before the browser ever gets the page.

1 Like

Well, that’s at least good news that its not the plugin! Now I just to figure out what I’m doing wrong in the view.

Found it.

You can chase this down through the source code if you really want to understand what’s going on, but the Reader’s Digest version is that an inline formset is created under the assumption that the formset is linked to an existing instance. If one isn’t supplied, it selects one from the database.

Since you’re creating the Recipe entry at the same time as the formset entries, you don’t want an inline formset, you want a regular model formset that you can assign to your newly created Recipe.

Here’s a sample:

class RecipeCreate(LoginRequiredMixin, CreateView):
    model = models.Recipe
    fields = ['title', 'description', 'servings', 'prep_time', 'cook_time', 'url']

    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)
        IFormset = modelformset_factory(Ingredient, form=AddIngredientForm, extra=1, can_delete=True)
        DFormset = modelformset_factory(Direction, form=AddDirectionForm, extra=1, can_delete=True)

        if self.request.POST:
            data['ingredients'] = IFormset(self.request.POST)
            data['directions'] = DFormset(self.request.POST)
        else:
            data['ingredients'] = IFormset(queryset=Ingredient.objects.none())
            data['directions'] = DFormset(queryset=Direction.objects.none())
        return data

    def form_valid(self, form):
        context = self.get_context_data()
        ingredients = context['ingredients']
        directions = context['directions']

        # self.object is the object being created
        self.object = form.save(commit=False)
        self.object.recipebook = self.request.user.recipebook
        self.object.save()

        if ingredients.is_valid():
            for ingredient_form in ingredients:
                ingredient = ingredient_form.save(commit=False)
                ingredient.recipe = self.object
                ingredient.save()
        if directions.is_valid():
            for direction_form in directions:
                direction = direction_form.save(commit=False)
                direction.recipe = self.object
                direction.save()

        return HttpResponseRedirect(self.get_success_url())
1 Like

Ah, great, thank you so much! It looks like it’s working. My only problem is that I’m not sure how exactly the UpdateView should work. I’m trying to query the objects like this:

    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)
        ingredient_formset = modelformset_factory(models.Ingredient, form=AddIngredientForm, extra=1, can_delete=True)
        direction_formset = modelformset_factory(models.Direction, form=AddDirectionForm, extra=1, can_delete=True)

        if self.request.POST:
            data['ingredients'] = ingredient_formset(self.request.POST)
            data['directions'] = direction_formset(self.request.POST)
        else:
            data['ingredients'] = ingredient_formset(queryset=models.Ingredient.objects.filter(recipe__id=self.kwargs['pk']))
            data['directions'] = direction_formset(queryset=models.Direction.objects.filter(recipe__id=self.kwargs['pk']))
        return data

But I’m only getting one object, when there should be multiple. I can get them all by using queryset=models.Ingredient.objects.all(), but clearly we don’t want that. Is the issue that I need to try to loop around the objects in the GET method of the get_context_data?

Also, I’m having an issue where when I try to create a second object after I’ve just created one, I get: IntegrityError at /recipe-book/recipe/create/
UNIQUE constraint failed: recipe_book_recipe.id
I know this has to do with the recipe id being the same, but I thought that a CreateView creates a new instance of an object every time? But for some reason, each time I try to use the CreateView, I’m getting that error, and I suspect its because its reusing the same id/pk (which I have set to: id = models.UUIDField(primary_key=True, default=uuid.uuid4())
Any idea how to stop that? Does it have to do with the commit=False paramater on the form.save() method?

Don’t have time at the moment for the first issue, but this one is easy - the problem is you’re calling the function uuid4 for the default value - which means it gets called when the model is registered in Django. What you really need to do here is pass the callable:
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
See the docs on the default parameter.

1 Like

While I greatly appreciate Ken’s help, I was able to solve this problem while still using inline_formsets, which I just personally found to be easier. Ken was absolutely correct, the issue is that you need to tell the formsets not to query on your CreateView, but to then query on the UpdateView: My views look like this:
CreateView:

    def get_context_data(self, **kwargs):
        data = super(RecipeCreate, self).get_context_data(**kwargs)

        if self.request.POST:
# IngredientFormset is simply an inline_formset I declared earlier
            data['ingredients'] = IngredientFormset(self.request.POST)
            data['directions'] = DirectionFormset(self.request.POST)
        else:
            data['ingredients'] = IngredientFormset(queryset=models.Ingredient.objects.none())
            data['directions'] = DirectionFormset(queryset=models.Direction.objects.none())
        return data

UpdateView:

    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)
            data['directions'] = DirectionFormset(self.request.POST, instance=self.object)
        else:
            data['ingredients'] = IngredientFormset(instance=self.object)
            data['directions'] = DirectionFormset(instance=self.object)
        return data