Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
How to customize save in django admin inline form?

Customize Save In Django Admin Inline Form

Background

This is a common case in django ORM.

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    name = models.CharField(max_length=255)
    author = models.ForeignKey(Author)

As a CMS, we are also required to provide a feature to create Author and several Books for him together. So, the admin class is here:

from django import forms

class AuthorAdmin(object):
    form = [AuthorForm,]
    inlines = [BookInline,]
    form_layout = (
        ...
    )

class BookInline(object):
    model = Book
    form = BookForm
    form_layout = (
        ...
    )

class AuthorForm(forms.ModelForm):
    model = Author

class BookForm(forms.ModelForm):
    model = Book

With this BookInline, you can get all things done without doubt. But let’t think about a special case:

Assume that there’s another Model Press, every author belongs to a press.

class Press(models.Model): …

When creating a author and add/update book to him, you also need to create/update the same one for the press, synchronously. May be it is a obvious bad design, but just a example.

So, how to do that?

HowTo

Straightforward to say, you should customize FormSet for BookInline and override these two methods:

  • save_new_objects
  • save_existing_objects

So, the BookInline will becomes

from django.forms.models import BaseInlineFormSet

class BookInline(object):
    model = Book
    form = BookForm
    fromset = BookFormSet
    form_layout = (
        ...
    )

class BookInlineFormSet(BaseInlineFormSet):
    def save_new_objects(self, commit=True):
        saved_instances = super(BookInlineFormSet, self).save_new_objects(commit)
        if commit:
            # create book for press
        return saved_instances

    def save_existing_objects(self, commit=True):
        saved_instances = super(BookInlineFormSet, self).save_existing_objects(commit)
        if commit:
            # update book for press
      return saved_instances

Why

Traps

Inertia of thinking, we can override save function of BookForm like this:

class BookForm(models.ModelForm):
    ...

    def save(self, commit=True):
        instance = super(BookForm, self).save(commit)
        # do anything you want to sync book for Press

In fact that is a mistake I’ve made before, because save function of BookForm instance is called with commit=False, it means book instance is lacking attribute values. So, it can’t be copied or do anything else. I will give some hints to show that how django does it.

# django/forms/models.py

class BaseInlineFormSet(BaseModelFormSet):
    ...
    def save_new(self, form, commit=True):
        # Use commit=False so we can assign the parent key afterwards, then
        # save the object.
        obj = form.save(commit=False)
        pk_value = getattr(self.instance, self.fk.rel.field_name)
        setattr(obj, self.fk.get_attname(), getattr(pk_value, 'pk', pk_value))
        if commit:
            obj.save()
        # form.save_m2m() can be called via the formset later on if commit=False
        if commit and hasattr(form, 'save_m2m'):
            form.save_m2m()
        return obj

Moreover, it is worth mention that although obj's Foreignkey is saved above,

setattr(obj, self.fk.get_attname(), getattr(pk_value, 'pk', pk_value))

you can not access it directly as following at downstream

def save_new_objects(self, commit=True):
    saved_instances = super(BookInlineFormSet, self).save_new_objects(commit)
    if commit:
        book = saved_instances[0]
        author = book.author  # wrong way
    return saved_instances

Because django probably already cached a invalid author before. In order to get a “fresh” author, use the primary key value of the related object as stored in the db field instead.

def save_new_objects(self, commit=True):
    saved_instances = super(BookInlineFormSet, self).save_new_objects(commit)
    if commit:
        book = saved_instances[0]
        author = Author.objects.get(pk=book.author_id)  # correct way
    return saved_instances

About caching ForeignKey in django, see django doc for more details.

Workflow Of Django Admin Inline

+--------------+
|              |
| AuthorAdmin  |
|              |
+--------------+
+----------------+ +--------------+
|                | |              |
| BookInlinAdmin | |  AuthorForm  |
|                | |              |
+----------------+ +--------------+
+--------------+ +--------------+
|              | |              |
|  BookFormSet | |    Author    |
|              | |              |
+--------------+ +--------------+
+--------------+        |
|              |        |
|   BookForm   |        |
|              |        |
+--------------+        |
+--------------+        |
|              |        |
|     Book     |<-------+
|              |
+--------------+
+----------+
|          |
|    DB    |
|   {s}    |
+----------+
@sbpraveen34

This comment has been minimized.

Copy link

commented Oct 16, 2015

Great Explanation

@kccheung

This comment has been minimized.

Copy link

commented Jan 16, 2018

Thanks for such a detailed explanation!

@varghesepaul91

This comment has been minimized.

Copy link

commented Oct 9, 2018

Thanks for the explanation.<3

@ArnoldGitHub

This comment has been minimized.

Copy link

commented Oct 30, 2018

Thanks, you saved me

@belek

This comment has been minimized.

Copy link

commented Jan 17, 2019

Thanks, I really appreciate it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.