Forked from shymonk/customize-save-in-django-admin-inline-form.org
Last active
July 24, 2020 14:12
-
-
Save oleoneto/85cb863e733a4f13cde6583739d5ba23 to your computer and use it in GitHub Desktop.
How to customize save in django admin inline form?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#+TITLE: Customize Save In Django Admin Inline Form | |
* Background | |
This is a common case in django ORM. | |
#+begin_src python | |
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) | |
#+end_src | |
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: | |
#+begin_src python | |
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 | |
#+end_src | |
With this =BookInline=, you can get all things done without doubt. | |
But let't think about a special case: | |
#+begin_quote | |
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. | |
#+end_quote | |
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 | |
#+begin_src python | |
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 | |
#+end_src | |
* Why | |
** Traps | |
Inertia of thinking, we can override =save= function of =BookForm= | |
like this: | |
#+begin_src python | |
class BookForm(models.ModelForm): | |
... | |
def save(self, commit=True): | |
instance = super(BookForm, self).save(commit) | |
# do anything you want to sync book for Press | |
#+end_src | |
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. | |
#+begin_src python | |
# 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 | |
#+end_src | |
Moreover, it is worth mention that although =obj's= Foreignkey is saved above, | |
#+begin_src python | |
setattr(obj, self.fk.get_attname(), getattr(pk_value, 'pk', pk_value)) | |
#+end_src | |
you can not access it directly as following at downstream | |
#+begin_src python | |
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 | |
#+end_src | |
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. | |
#+begin_src python | |
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 | |
#+end_src | |
About caching ForeignKey in django, see [[https://docs.djangoproject.com/en/dev/topics/db/queries/#one-to-many-relationships][django doc]] for more details. | |
** Workflow Of Django Admin Inline | |
#+BEGIN_SRC ditaa :file django-admin-inline-save.png | |
+--------------+ | |
| | | |
| AuthorAdmin | | |
| | | |
+--------------+ | |
+----------------+ +--------------+ | |
| | | | | |
| BookInlinAdmin | | AuthorForm | | |
| | | | | |
+----------------+ +--------------+ | |
+--------------+ +--------------+ | |
| | | | | |
| BookFormSet | | Author | | |
| | | | | |
+--------------+ +--------------+ | |
+--------------+ | | |
| | | | |
| BookForm | | | |
| | | | |
+--------------+ | | |
+--------------+ | | |
| | | | |
| Book |<-------+ | |
| | | |
+--------------+ | |
+----------+ | |
| | | |
| DB | | |
| {s} | | |
+----------+ | |
#+END_SRC |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment