The default Django model administration comes with a concept of inlines. If you have a one-to-many relationship, you can edit the parent and its children in the same form. However, you are limited in a way that you cannot have inlines under inlines at nested one-to-many relations. For example, you can't show models Painter
, Picture
, and Review
in the same form if one painter may have drawn multiple pictures and each picture may have several reviews.
In this article I would like to share a workaround allowing you to quickly access the inlines of an inline model. The idea is that for every inline you can provide a HTML link leading to the separate form where you can edit the related model and its own relations. It's as simple as that.
For example, in the form of Painter
model, you have the instances of Picture
listed with specific links "Edit this Picture separately":
When such a link is clicked, the administrator goes to the form of the Picture
model which shows the instances of Review
model listed underneath:
Let's have a look, how to implement this.
First of all, I will create a gallery
app and define the three models there. Nothing fancy here. The important part there is just that the Picture
model has a foreign key to the Painter
model and the Review
model has a foreign key to the Picture
model.
# gallery/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import os
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.utils.text import slugify
@python_2_unicode_compatible
class Painter(models.Model):
name = models.CharField(_("Name"), max_length=255)
class Meta:
verbose_name = _("Painter")
verbose_name_plural = _("Painters")
def __str__(self):
return self.name
def upload_to(instance, filename):
filename_base, filename_ext = os.path.splitext(filename)
return "painters/{painter}/{filename}{extension}".format(
painter=slugify(instance.painter.name),
filename=slugify(filename_base),
extension=filename_ext.lower(),
)
@python_2_unicode_compatible
class Picture(models.Model):
painter = models.ForeignKey(Painter, verbose_name=_("Painter"), on_delete=models.CASCADE)
title = models.CharField(_("Title"), max_length=255)
picture = models.ImageField(_("Picture"), upload_to=upload_to)
class Meta:
verbose_name = _("Picture")
verbose_name_plural = _("Pictures")
def __str__(self):
return self.title
@python_2_unicode_compatible
class Review(models.Model):
picture = models.ForeignKey(Picture, verbose_name=_("Picture"), on_delete=models.CASCADE)
reviewer = models.CharField(_("Reviewer name"), max_length=255)
comment = models.TextField(_("Comment"))
class Meta:
verbose_name = _("Review")
verbose_name_plural = _("Reviews")
def __str__(self):
return self.reviewer
Then I will create the administration definition for the models of the gallery
app. Here I will set two types of administration for the Picture
model:
- By extending
admin.StackedInline
I will create administration stacked as inline. - By extending
admin.ModelAdmin
I will create administration in a separate form.
In Django model administration besides usual form fields, you can also include some computed values. This can be done by your fields
(or fieldsets
) and readonly_fields
attributes referring to a callable or a method name.
You can set a translatable label for those computed values by defining short_description
attribute for the callable or method. If you want to render some HTML, you can also set the allow_tags
attribute to True
(otherwise your HTML string will be escaped).
# gallery/admin.py
# -*- coding: UTF-8 -*-
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.text import force_text
from .models import Painter, Picture, Review
def get_picture_preview(obj):
if obj.pk: # if object has already been saved and has a primary key, show picture preview
return """<a href="{src}" target="_blank"><img src="{src}" alt="{title}" style="max-width: 200px; max-height: 200px;" /></a>""".format(
src=obj.picture.url,
title=obj.title,
)
return _("(choose a picture and save and continue editing to see the preview)")
get_picture_preview.allow_tags = True
get_picture_preview.short_description = _("Picture Preview")
class PictureInline(admin.StackedInline):
model = Picture
extra = 0
fields = ["get_edit_link", "title", "picture", get_picture_preview]
readonly_fields = ["get_edit_link", get_picture_preview]
def get_edit_link(self, obj=None):
if obj.pk: # if object has already been saved and has a primary key, show link to it
url = reverse('admin:%s_%s_change' % (obj._meta.app_label, obj._meta.model_name), args=[force_text(obj.pk)])
return """<a href="{url}">{text}</a>""".format(
url=url,
text=_("Edit this %s separately") % obj._meta.verbose_name,
)
return _("(save and continue editing to create a link)")
get_edit_link.short_description = _("Edit link")
get_edit_link.allow_tags = True
@admin.register(Painter)
class PainterAdmin(admin.ModelAdmin):
save_on_top = True
fields = ["name"]
inlines = [PictureInline]
class ReviewInline(admin.StackedInline):
model = Review
extra = 0
fields = ["reviewer", "comment"]
@admin.register(Picture)
class PictureAdmin(admin.ModelAdmin):
save_on_top = True
fields = ["painter", "title", "picture", get_picture_preview]
readonly_fields = [get_picture_preview]
inlines = [ReviewInline]
UPDATE! Since Django 2.0, the get_picture_preview()
function should use mark_safe()
instead of allow_tags=True
:
from django.utils.safestring import mark_safe
# ...
def get_edit_link(self, obj=None):
if obj.pk: # if object has already been saved and has a primary key, show link to it
url = reverse(
'admin:%s_%s_change' % (obj._meta.app_label, obj._meta.model_name),
args=[force_text(obj.pk)]
)
return mark_safe("""<a href="{url}">{text}</a>""".format(
url=url,
text=_("Edit this %s separately") % obj._meta.verbose_name,
))
return _("(save and continue editing to create a link)")
get_edit_link.short_description = _("Edit link")
In this administration setup, the get_edit_link()
method creates a HTML link between the inline and the separate administration form for the Picture
model.
As you can see, I also added the get_picture_preview()
function as a bonus. It is included in both administration definitions for the Picture
model and its purpose is to show a preview of the uploaded picture after saving it.
To recap, nested inlines are not supported by Django out of the box. However, you can have your inlines edited in a separate page with the forms linked to each other. For the linking you would use some magic of the readonly_fields
attribute.
What if you really need to have inlines under inlines in your project? In that case you might check django-nested-admin and don't hesitate to share your experience with it in the comments.
Cover photo by Denys Nevozhai