Skip to content

Instantly share code, notes, and snippets.

@shymonk
Last active April 14, 2024 02:55
Show Gist options
  • Star 80 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save shymonk/5d4467bbc7d08dd7f6f4 to your computer and use it in GitHub Desktop.
Save shymonk/5d4467bbc7d08dd7f6f4 to your computer and use it in GitHub Desktop.
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
Copy link

Great Explanation

@kccheung
Copy link

Thanks for such a detailed explanation!

@varghesepaul91
Copy link

Thanks for the explanation.<3

@ArnoldGitHub
Copy link

Thanks, you saved me

@belek
Copy link

belek commented Jan 17, 2019

Thanks, I really appreciate it!

@jeromelefeuvre
Copy link

Awesome. Just small typo on fromset = BookFormSet. Pretty sure it's formset = BookFormSet ;)

@jeromelefeuvre
Copy link

With the code, you save nothing. Goal of this is to update a field. My changes are:

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

@hanzhichao
Copy link

can't get request.user to save creator or modifier for inline model

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment