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?

Alasdair
  • 298,606
  • 55
  • 578
  • 516
chefsmart
  • 6,873
  • 9
  • 42
  • 47

3 Answers3

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 %}
Drew Thaler
  • 183
  • 1
  • 5
priestc
  • 33,060
  • 24
  • 83
  • 117
  • 1
    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? – chefsmart Jul 11 '09 at 07:43
  • 1
    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. – priestc Jul 11 '09 at 08:03
  • 2
    I wasn't thinking!! inlineformset_factory accepts can_delete (of course!) so I just passed can_delete=False. – chefsmart Jul 11 '09 at 08:06
  • 6
    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. – chefsmart Jul 11 '09 at 08:28
  • 2
    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. – Carl Meyer Dec 07 '14 at 17:05
  • @chefsmart How do you hide with the css/jquery? – KhoPhi Apr 23 '15 at 12:21
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").

MikeRand
  • 4,788
  • 9
  • 41
  • 70
  • 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? – Kritz Oct 26 '17 at 09:13
  • 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. – MikeRand Oct 27 '17 at 19:05
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

Tryph
  • 5,946
  • 28
  • 49
Ogre Codes
  • 18,693
  • 1
  • 17
  • 24