Recently I've received an interesting request from a client about one of our Django projects.
He asked if it would be possible to show an inline component above other fields in the Django admin panel.
At the beginning I thought, that there shouldn't be any issue with that.
Though there was no easy solution other then installing another battery to the project. My gut feeling told me, there were another way around that problem.
The first solution I found was from 2017. It had too much code for such a simple task.
Our executive lead programmer, Maxim Danilov found quite a short solution. He published his work online in russian about a month ago.
I´d like to share these ideas with englisch speaking Django community in order to help others simplify their code. It might come handy for such a "simple" at first glance issues.
Long story short, let's dive into the code:
Imagine you are building an E-commerce project. You have a ProductModel
which have O2M relation to ImageModel
.
from django.db import models
from django.utils.translation import gettext_lazy as _
class Product(models.Model):
title = models.CharField(verbose_name=_('Title of product'), max_length=255)
price = models.DecimalField(verbose_name=_('Price of product'), max_digits=6, decimal_places=2)
class Image(models.Model):
src = models.ImageField(verbose_name=_('Imagefile'))
product = models.ForeignKey(Product, verbose_name=_('Link to product'), on_delete=models.CASCADE)
You also need ModelAdmins
for given models.
from django.contrib import admin
from .models import Image, Product
@admin.register(Product)
class ProductModelAdmin(admin.ModelAdmin):
fields = ('title', 'price')
@admin.register(Image)
class ImageModelAdmin(admin.ModelAdmin):
fields = ('src', 'product')
Now let's create a simple inline to put into our ProductModelAdmin
.
from django.contrib import admin
from django.contrib.admin.options import TabularInline
from .models import Image, Product
class ImageAdminInline(TabularInline):
extra = 1
model = Image
@admin.register(Product)
class ProductModelAdmin(admin.ModelAdmin):
inlines = (ImageAdminInline,)
fields = ('title', 'price')
@admin.register(Image)
class ImageModelAdmin(admin.ModelAdmin):
fields = ('src', 'product')
So far we have two simple models and basic ModelAdmins
with an InlineModel
.
How should we squeeze that Inline above or in between the two fields of the ProductModelAdmin
?
I suppose, you are familiar with the added field concept in ModelAdminForm
. You can create a method in the ModelAdmin
to display the response of the method in the form as a readonly field.
Keep in mind, that the rendering sequence of the ModelAdmin
will create the InlineModels
first, then render AdminForm
and after that render the InlineForms
.
We can use that to rearrange the order of Inlines and fields.
from django.contrib import admin
from django.contrib.admin.options import TabularInline
from django.template.loader import get_template
from .models import Image, Product
class ImageAdminInline(TabularInline):
extra = 1
model = Image
@admin.register(Product)
class ProductModelAdmin(admin.ModelAdmin):
inlines = (ImageAdminInline,)
fields = ('image_inline', 'title', 'price')
readonly_fields= ('image_inline',) # method as readonly field
def image_inline(self, *args, **kwargs):
context = getattr(self.response, 'context_data', None) or {}
inline = context['inline_admin_formset'] = context['inline_admin_formsets'].pop(0)
return get_template(inline.opts.template).render(context, self.request)
def render_change_form(self, request, *args, **kwargs):
self.request = request
self.response = super().render_change_form(request, *args, **kwargs)
return self.response
We use the render_change_form
to get the objects request
and response
.
We use those objects in the image_inline
method to take one inline_formset
from the list of inline_admin_formsets
that have not been processed yet, and render InlineFormset
.
After the change_form
rendering the remaining inline_admin_formsets
will be rendered, in case if the ModelAdmin
still has some.
Now we can use the method image_inline
to determine the position of our InlineFormset
.
With the code-snippet above the inline element will be placed above all other fields.
When we rearrange the fields this way the inline is rendered between the fields:
@admin.register(Product)
class ProductModelAdmin(admin.ModelAdmin):
inlines = ImageAdminInline,
fields = 'title','image_inline', 'price'
readonly_fields= 'image_inline', # method as readonly field
Of course Django admin adds a lable infront of the Inline with the name of the method, but that can be easily removed by some simple CSS in Media
attribute of ProductModelAdmin
.
This solution has one fatal error! Every Django ModelAdmin is singelton, that is why we can not use ModelAdmin.self as a container in render_change_form
!
It is possible to change the ModelAdmins singleton´s behavior with a Mixin, staying inline with the concept of Djangos GCBV. We will take a closer look at it in my next article.
It simply means, that we can't use the instance of the ModelAdmin
as a container to save our request
and response
.
The solution is to save those objects in the AdminForm
instance.
@admin.register(Product)
class ProductModelAdmin(admin.ModelAdmin):
inlines = (ImageAdminInline,)
fields = ('title', 'image_inline', 'price')
readonly_fields= ('image_inline',) # we set the method as readonly field
def image_inline(self, obj=None, *args, **kwargs):
context = obj.response['context_data']
inline = context['inline_admin_formset'] = context['inline_admin_formsets'].pop(0)
return get_template(inline.opts.template).render(context, obj.request)
def render_change_form(self, request, context, *args, **kwargs):
instance = context['adminform'].form.instance # get the model instance from modelform
instance.request = request
instance.response = super().render_change_form(request, context, *args, **kwargs)
return instance.response
The argument obj
is not always given in render_change_form
(i.e. add new object). That is why we have to get it from the ModelForm
, which is wrapped into the AdminForm
.
Now we can set our request and response as attributes of the ModelForm
instance and use those in image_inline
.
Summing up the above: you don't have to install another battery to your project to solve a simple problem. Sometimes you need to dig deep enough into the framework that you use, and find a simple, short and quick solution.
I´d like to thank Martin Achenrainer the intern of wPsoft for contributing to this article and translating it.