Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save muminoff/e9a087af60e62e14ce02f877fb9eafa6 to your computer and use it in GitHub Desktop.
Save muminoff/e9a087af60e62e14ce02f877fb9eafa6 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}    |
+----------+
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment