Creating a model and related models with Inline formsets
Asked Answered
F

3

49

[I have posted this at the Django users | Google Groups also.]

Using the example in the inline formset docs, I am able to edit objects belonging a particular model (using modelforms). I have been trying to follow the same pattern for creating new objects using inline formsets, but have been unable to clear my head enough to bring out a working view for this purpose.

Using the same example as in the above link, how would I go about creating a new instance of an "Author" model together with its related "Book" objects?

Felucca answered 11/7, 2009 at 5:49 Comment(0)
K
40

First, create a Author model form.

author_form = AuthorModelForm()

then create a dummy author object:

author = Author()

Then create a inline formset using the dummy author like so:

formset = BookFormSet(instance=author)  #since author is empty, this formset will just be empty forms

Send that off to a template. After the data is returned back to the view, you create the Author:

author = AuthorModelForm(request.POST)
created_author = author.save()  # in practice make sure it's valid first

Now hook the inline formset in with the newly created author and then save:

formset = BookFormSet(request.POST, instance=created_author)
formset.save()   #again, make sure it's valid first

edit:

To have no checkboxes on new forms, do this is a template:

{% for form in formset.forms %}
    <table>
    {% for field in form %}
        <tr><th>{{field.label_tag}}</th><td>{{field}}{{field.errors}}</td></tr>
    {% endfor %}

    {% if form.pk %} {# empty forms do not have a pk #}
         <tr><th>Delete?</th><td>{{field.DELETE}}</td></tr>
    {% endif %}
    </table>
{% endfor %}
Kieserite answered 11/7, 2009 at 6:27 Comment(6)
This works, and is a logical solution. The only thing is that this makes can_delete = True so there are corresponding checkboxes for inline instances (which does not make real sense for the user as the instances do not exist as yet). Right now I am hiding these checkboxes with css / JQuery. Or do you know of a better way to hide them?Felucca
Yeah I it is kinda dumb how those delete checkboxes are added even on unbound forms. I'll update my answer with how I do it.Kieserite
I wasn't thinking!! inlineformset_factory accepts can_delete (of course!) so I just passed can_delete=False.Felucca
To clarify, if you're following the documentation's example, you would do BookFormSet = inlineformset_factory(Author, Book, can_delete=False) instead of BookFormSet = inlineformset_factory(Author, Book) and then you won't need to do the {% if form.pk %} check in nbv4's answer above.Felucca
The problem with this answer is that the Author may be saved even though the BookFormset is not valid, possibly causing duplicate authors to be created. There are two answers below which fix that problem.Geometry
@Felucca How do you hide with the css/jquery?Liatris
F
37

I'd actually like to propose a small adjustment to nbv4's solution:

Assume that you don't create the empty created_author outside of the if-else statement and thus need to nest the formset inside the author_form.is_valid() to avoid runtime errors when the author_form is not valid (and thus no created_author is instantiated).

Instead of:

if request.method == 'POST':
    author_form = AuthorModelForm(request.POST)
    if author_form.is_valid():
        created_author = author_form.save()
        formset = BookFormSet(request.POST, instance=created_author)
        if formset.is_valid():
            formset.save()
            return HttpResponseRedirect(...)
else:
    ...

Do the following:

if request.method == 'POST':
    author_form = AuthorModelForm(request.POST)
    if author_form.is_valid():
        created_author = author_form.save(commit=False)
        formset = BookFormSet(request.POST, instance=created_author)
        if formset.is_valid():
            created_author.save()
            formset.save()
            return HttpResponseRedirect(...)
else:
    ...

This version avoids committing the created_author until the book_formset has had a chance to validate. The use case to correct for is that someone fills out a valid AuthorForm with an invalid BookFormSet and keeps resubmitting, creating multiple Author records with no Books associated with them. This seems to work for my project-tracker app (replace "Author" with "Project" and "Book" with "Role").

Fractostratus answered 2/2, 2011 at 15:47 Comment(2)
If you post and the author form does not validate, then you won't have a formset to send to the template? Or am I missing something?Abate
Back when I answered this, if formset isn't valid, in the old version you'd still be stuck with a committed created_author (having saved it from the author form). The new version only commits a created_author if both the author_form and the formset are valid. Up to you if that's better; you may want created_author to be committed even if the book formset isn't valid.Fractostratus
P
11

models.py (Contact)

class Contact(models.Model)
    first = models.CharField(max_length=30)
    middle = models.CharField('M.I.',max_length=30, blank=True)
    last = models.CharField(max_length=30)
    sort_order = models.PositiveIntegerField(default=99)

models.py (Link)

class Link(models.Model):
    contact = models.ForeignKey(Contact)
    link = models.URLField()
    description = models.CharField(max_length=30)
    access_date = models.DateField(blank=True,null=True)

forms.py

from django.forms import ModelForm
from contacts.models import Contact

class ContactAjaxForm(ModelForm):
    class Meta:
        model=Contact

views.py

def edit(request,object_id=False):
    LinkFormSet = inlineformset_factory(Contact, Link, extra=1)
    if object_id:
        contact=Contact.objects.get(pk=object_id)
    else:
        contact=Contact()
    if request.method == 'POST':
        f=forms.ContactAjaxForm(request.POST, request.FILES, instance=contact)
        fs = LinkFormSet(request.POST,instance=contact)
        if fs.is_valid() and f.is_valid():
            f.save()
            fs.save()
            return HttpResponse('success')
    else:
        f  = forms.ContactAjaxForm(instance=contact)
        fs = LinkFormSet(instance=contact)
    return render_to_response(
        'contacts/edit.html', 
        {'fs': fs, 'f': f, 'contact': contact}
    )

This is not based on the example in the book, it's edited down from some code on my site. I haven't tested it so there might be some bugs but overall it should be solid. Using an empty instance of Contact isn't the suggested method but it saves a bit of logic and it works.

Edit: Added Link Model, switched to normal Foreign Key instead of Generic Foreign Key which is confusing

Pressure answered 11/7, 2009 at 6:28 Comment(4)
Could you post the details of your Link model?Felucca
-O- I just realized Links is a generic foreign key... probably not the best example, let me see if I can fix it.Pressure
inlineformset_factory now enforce declaring the fields param explicitly.Liatris
Great stuff! Could anyone post the contents of edit.html??Doloritas

© 2022 - 2024 — McMap. All rights reserved.