Search code examples
pythondjangodjango-viewsoverridinginline-formset

How to save inline formset user field in Django using views


I've been using this great post http://kevindias.com/writing/django-class-based-views-multiple-inline-formsets/ to setup my site. I was wondering how to save the user field automatically to an inline formset in views (I used the blockquote for changes to the original). The RecipeForm in (see also below for context)

self.object = form.save(commit=False)
self.object.owner = self.request.user
self.object.save()

saves nicely automatically but not the

ingredient_form.owner= self.request.user

I know Django suggests using BaseInlineFormSet, but most people suggest saving user field in views.py and not forms or models for many different reasons. I would appreciate any suggestions or answers. Here's the full code:

models.py

from django.db import models


class Recipe(models.Model):
    owner = models.ForeignKey(User)
    title = models.CharField(max_length=255)
    description = models.TextField()


class Ingredient(models.Model):
    owner = models.ForeignKey(User)
    recipe = models.ForeignKey(Recipe)
    description = models.CharField(max_length=255)


class Instruction(models.Model):
    recipe = models.ForeignKey(Recipe)
    number = models.PositiveSmallIntegerField()
    description = models.TextField()

forms.py

from django.forms import ModelForm
from django.forms.models import inlineformset_factory 
from .models import Recipe, Ingredient, Instruction


class RecipeForm(ModelForm):
    class Meta:
        model = Recipe
    IngredientFormSet = inlineformset_factory(Recipe, Ingredient)
    InstructionFormSet = inlineformset_factory(Recipe, Instruction)

views.py

from django.http import HttpResponseRedirect
from django.views.generic import CreateView  
from .forms import IngredientFormSet, InstructionFormSet, RecipeForm
from .models import Recipe


class RecipeCreateView(CreateView):
    template_name = 'recipe_add.html'
    model = Recipe
    form_class = RecipeForm
    success_url = 'success/'

    def get(self, request, *args, **kwargs):
        """
        Handles GET requests and instantiates blank versions of the form
        and its inline formsets.
        """
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        ingredient_form = IngredientFormSet()
        instruction_form = InstructionFormSet()
        return self.render_to_response(
            self.get_context_data(form=form,
                                  ingredient_form=ingredient_form,
                                  instruction_form=instruction_form))

    def post(self, request, *args, **kwargs):
        """
        Handles POST requests, instantiating a form instance and its inline
        formsets with the passed POST variables and then checking them for
        validity.
        """
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        ingredient_form = IngredientFormSet(self.request.POST)
        instruction_form = InstructionFormSet(self.request.POST)
        if (form.is_valid() and ingredient_form.is_valid() and
            instruction_form.is_valid()):
            return self.form_valid(form, ingredient_form, instruction_form)
        else:
            return self.form_invalid(form, ingredient_form, instruction_form)

    def form_valid(self, form, ingredient_form, instruction_form):
        """
        Called if all forms are valid. Creates a Recipe instance along with
        associated Ingredients and Instructions and then redirects to a
        success page.
        """
        self.object = form.save(commit=False)
        self.object.owner = self.request.user
        self.object.save()
        ingredient_form.instance = self.object
        ingredient_form.owner= self.request.user
        ingredient_form.save()
        instruction_form.instance = self.object
        instruction_form.save()
        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form, ingredient_form, instruction_form):
        """
        Called if a form is invalid. Re-renders the context data with the
        data-filled forms and errors.
        """
        return self.render_to_response(
            self.get_context_data(form=form,
                                  ingredient_form=ingredient_form,
                                  instruction_form=instruction_form))

Solution

  • I did some more research and the solution looks somewhat complex following this guide of how to add custom formset saving but modified for BaseInlineFormset as mentioned above. I realized it will be simpler just to make ModelForms for each Model and then linking them in a view, since I only need one child form at a time in the add a new recipe view and can reuse the ModelForm code.

    here's the new code that works great! Feel free to contact if you need more info.

    forms.py

    from django.forms import ModelForm
    from .models import Recipe, Ingredient, Instruction
    
    
    class RecipeForm(ModelForm):
    
        class Meta:
            model = Recipe
            exclude = ['owner',]
    
    class IngredientForm(ModelForm):
    
        class Meta:
            model = Ingredient
            exclude = ['owner','recipe',]
    
    class InstructionForm(ModelForm):
    
        class Meta:
            model = Instruction
            exclude = ['recipe',]
    

    views.py

    from .forms import IngredientForm, InstructionForm, RecipeForm
    
    
    def add_new_value(request):
        rform = RecipeForm(request.POST or None)
        iform = IngredientForm(request.POST or None)
        cform = InstructionForm(request.POST or None)
        if rform.is_valid() and iform.is_valid() and cform.is_valid():
            rinstance = rform.save(commit=False)
            iinstance = iform.save(commit=False)
            cinstance = cform.save(commit=False)
            user = request.user
            rinstance.owner = user
            rinstance.save()
            iinstance.owner = user
            cinstance.owner = user
            iinstance.recipe_id = rinstance.id
            cinstance.recipe_id = rinstance.id
            iinstance.save()
            cinstance.save()
            return HttpResponseRedirect('/admin/')
        context = {
            'rform' : rform,
            'iform' : iform,
            'cform' : cform,
        }
        return render(request, "add_new_recipe.html", context)
    

    template: add_new_recipe.html

    <!DOCTYPE html>
    <html>
    <head>
        <title>Add Recipe</title>
    </head>
    
    <body>
        <div>
            <h1>Add Recipe</h1>
            <form action="" method="post">
                {% csrf_token %}
                <div>
                    {{ rform.as_p }}
                    {{ iform.as_p }}
                    {{ cform.as_p }}
                </div>
                <input type="submit" value="Add recipe" class="submit" />
            </form>
        </div>
    </body>
    </html>