Pull to refresh

Django admin dynamic Inline positioning

Reading time5 min
Views11K
Original author: Maxim Danilov
Django ModelAdmin and Inlines
Django ModelAdmin and Inlines

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 ProductModelwhich 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.

Standard ModelAdmin With Inline
Standard ModelAdmin With Inline

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.

ModelAdminForm with Inline on the top
ModelAdminForm with Inline on the top

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.

ModelAdminForm has inline in the middle
ModelAdminForm has inline in the middle

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.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 2: ↑2 and ↓0+2
Comments0

Articles