Customizing Django Admin with practical tips and techniques.
Home Django Mastering the Django Admin

Mastering the Django Admin

16 September, 2024

One of Django's strong points is its Admin interface. It really helps in getting projects off the ground faster since most web applications are composed of two parts: the user facing part and the admin interface. Django Admin practically makes the second one really easy. However, to unlock its full potential, we need to dive deeper into customization.

In this article, we will focus on customizing the Admin for a simple Article model, walking through various methods to build a more functional and user-friendly admin view. From adjusting the layout and form fields to adding custom actions, filters, and inline editing, we’ll explore how to tailor Django Admin to fit your project's specific requirements and enhance the experience for both developers and administrators.

Here's our starting Article model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Article(models.Model):
    class Status(models.TextChoices):
        DRAFT = "DRAFT", "Draft"
        PENDING_REVIEW = "PENDING_REVIEW", "Pending Review"
        PUBLISHED = "PUBLISHED", "Published"
        TRASH = "TRASH", "Trash"

    title = models.CharField("Title", max_length=256)
    body = models.TextField("Body")
    category = models.ForeignKey(
            Category,
            null=True,
            blank=True,
            on_delete=models.SET_NULL
    )

    author = models.ForeignKey(
            UserModel,
            null=True,
            blank=True,
            on_delete=models.SET_NULL
    )
    status = models.CharField(
            max_length=20,
            choices=Status.choices,
            default=Status.DRAFT
    )

    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    published = models.DateTimeField(null=True, blank=True)

    def publish(self, user):
        if not user.has_perm("blog.publish_article", self):
            raise PermissionDenied()
        self.status = Article.Status.PUBLISHED
        self.published = now()
        self.save()

Customizing the Django Admin List View

Let's start by playing with the most common configs:

Basic Admin List View Customization

Here are the basic customization elements for the Article Django Model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_display = ('title', 'category', 'author', 'status')
    list_filter = (
            'author',
            'category',
            'published',
            'status',
            'author__role'
    )
    list_per_page = 10
    ordering = ("-published", )

Here's the quick breakdown:

  1. list_display - Choose what columns to display
  2. list_filter - Choose what filters to display on the right side
  3. list_per_page - How many rows to display per page
  4. ordering - How is the list ordered

Notice how author__role is a field on the related User model.

Here's how our Article admin interface looks so far:

Django Admin basic customization
Django Admin basic customization

Adding Search to Django Admin

Adding search functionality to Django Admin is easy-peasy, we just need to specify on which fields to perform the search:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_display = ('title', 'category', 'author', 'status')
    list_filter = (
            'author',
            'category',
            'published',
            'status',
            'author__role'
    )
    list_per_page = 10
    search_fields = ('title', 'body')  # <--

Notice the new search field up top:

Adding Search to admin
Adding Search to admin

Display computed fields in list view

Sometimes we want to display data that is not explicitly available on the model. Sometimes these are called computed fields. As an example, let's compute the number of articles the author of an article has written. Here's how to do that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_display = (
        'title',
        'category',
        'author',
        'status',
        'author_article_count'
    )
    list_filter = (
        'author',
        'category',
        'published',
        'status',
        'author__role'
    )
    list_per_page = 10
    search_fields = ('title', 'body')

    def author_article_count(self, obj):  # <--
        return Article.objects.filter(author=obj.author).count()

    author_article_count.short_description = "Author Experience"

Check out our new column:

Django Admin list computed field
Django Admin list computed field

Notice how we also attached a short_description to the method. This will be the column header in the list view. If not defined, the name of the method will be used.

Date Hierarchy

Another type of cool filtering we can add is a date hierarchy filter. It is used to select the year, then narrow down to the month and then day. This would be useful if we want to see what articles have been published in a certain date or period.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_display = (
        'title',
        'category',
        'author',
        'status',
        'author_article_count'
    )
    list_filter = (
        'author',
        'category',
        'published',
        'status',
        'author__role'
    )
    list_per_page = 10
    search_fields = ('title', 'body')
    date_hierarchy = 'published'  # <--

    def author_article_count(self, obj):
        return Article.objects.filter(author=obj.author).count()

    author_article_count.short_description = "Author Experience"
Django Admin date_hierarchy
Django Admin date_hierarchy

Inline editable fields

For quick edits, we might want to add inline editable fields in the admin. For example if we want to move an article to a different category or change its status. Here's how we do that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_display = (
        'title',
        'category',
        'author',
        'status',
        'author_article_count'
    )
    list_filter = (
        'author',
        'category',
        'published',
        'status',
        'author__role'
    )
    list_per_page = 10
    search_fields = ('title', 'body')
    date_hierarchy = 'published'
    list_editable = (   # <--
        'category',
        'status',
    )

    def author_article_count(self, obj):
        return Article.objects.filter(author=obj.author).count()

    author_article_count.short_description = "Author Experience"

The result:

Django Admin inline editable fields
Django Admin inline editable fields

Custom Admin Actions

When deleting models from the Django Admin list view, you might have noticed that you select the Delete action from a dropdown. We can create our own Custom Admin Actions and have them included in that dropdown. For our purpose, we are going to create a publish action for our Article model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@admin.action(description="Publish Articles")
def publish_article(modeladmin, request, queryset):
    for article in queryset:
        try:
            article.publish(request.user)
            messages.success(
                request,
                f"You successfully published {article}"
            )
        except PermissionDenied:
            messages.error(
                request,
                f"You do not have permission to publish {article}"
            )


@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_display = (
        'title',
        'category',
        'author',
        'status',
        'author_article_count'
    )
    list_filter = (
        'author',
        'category',
        'published',
        'status',
        'author__role'
    )
    list_per_page = 10
    search_fields = ('title', 'body')
    date_hierarchy = 'published'
    list_editable = (
        'category',
        'status',
    )

    def author_article_count(self, obj):
        return Article.objects.filter(author=obj.author).count()

    author_article_count.short_description = "Author Experience"

    actions = [publish_article]
Django Admin Writing custom actions
Django Admin Writing custom actions

Customizing the Django Admin Detail View

Autocomplete SlugField in Django Admin

A neat little trick, although of limited use, is to have an article's slug autocomplete as you write the article's title. This is as simple as using the prepopulated_fields feature:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_display = (
        'title',
        'category',
        'author',
        'status',
        'author_article_count'
    )
    list_filter = (
        'author',
        'category',
        'published',
        'status',
        'author__role'
    )
    list_per_page = 10
    search_fields = ('title', 'body')
    date_hierarchy = 'published'
    list_editable = (
        'category',
        'status',
    )

    prepopulated_fields = {"slug": ("title", )}  # <--

    def author_article_count(self, obj):
        return Article.objects.filter(author=obj.author).count()

    author_article_count.short_description = "Author Experience"

    actions = [publish_article]

While you type in the title, the SlugField is autocompleted. Might prove useful.

Django Admin prepopulated fields (autocomplete)
Django Admin prepopulated fields (autocomplete)

Grouping fields in fieldsets

This one has mostly to do with aesthetics. In order to make life easier for your users, you might consider placing the fields in the detail view in logical groups. Here's an example of how we would do that with our Article model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_display = (
        'title',
        'category',
        'author',
        'status',
        'author_article_count'
    )
    list_filter = (
        'author',
        'category',
        'published',
        'status',
        'author__role'
    )
    list_per_page = 10
    search_fields = ('title', 'body')
    date_hierarchy = 'published'
    list_editable = (
        'category',
        'status',
    )

    prepopulated_fields = {"slug": ("title", )}

    fieldsets = [
        (
            "Content",
            {
                "fields": [
                    "title",
                    "body"
                ],
            },
        ),
        (
            "Meta",
            {
                "classes": ["collapse"],
                "fields": [
                    "category",
                    "author",
                    "status",
                    "slug",
                    "published"
                ],
            },
        ),
    ]

    def author_article_count(self, obj):
        return Article.objects.filter(author=obj.author).count()

    author_article_count.short_description = "Author Experience"

    actions = [publish_article]

This can lead to cleaner interfaces. It is a good idea to hide fields that are less used and provide a clutter-free experience for the end/user. Here is how it looks with the second formset collapsed:

Django Admin fieldsets (collapsed)
Django Admin fieldsets (collapsed)

Here is how it looks with the second formset expanded:

Django Admin fieldsets (expanded)
Django Admin fieldsets (expanded)

Overriding form field widgets

There are times when we want to choose a different widget for a field that the default one provided by Django. The most common example is Select Widget vs Radio Group widget. Let's check how we can override the widget of the category field:

1
2
3
4
5
6
7
8
9
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    # ...

    formfield_overrides = {
        models.ForeignKey: {'widget': RadioSelect()},
    }

    # ...
Django Admin overriding all ForeignKey widgets
Django Admin overriding all ForeignKey widgets

As you can probably notice, the problem with this method is that it overrides all ForeignKey widgets. That might not always be what we want. If we want to change the widget of a specific field, we need to change the underlying ModelForm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class ArticleAdminForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = '__all__'
        widgets = {
            'category': forms.RadioSelect,
        }


@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    form = ArticleAdminForm

    # ...
Django Admin overriding specific field widget
Django Admin overriding specific field widget

Django Inline Admin Models

If you want to embed related models in the Django Admin, we can do so via TabularInline ot StackedInline classes. Here's how we would embed the Comment model in the Article admin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class CommentTabularInlineAdmin(admin.TabularInline):
    fields = (
        'text',
        'author',
        'created'
    )
    model = Comment
    show_change_link = True
    extra = 1


@admin.register(Article)
class ArticleAdmin(ModelAdmin):

    # ...

    inlines = (CommentTabularInlineAdmin, )  # <--

    # ...
Django Admin TabularInlineAdmin
Django Admin TabularInlineAdmin

Widgets: filter_horizontal vs filter_vertical

This feature has to do with how ManyToManyField is displayed in the Django Admin Detail View. The default way is a simple HTML <select multiple="multiple"> ... </select> widget.

Suppose we add a Tag model and link it as a ManyToManyField to the Article model:

1
2
3
4
5
class Tag(models.Model):
    name = models.CharField("name", max_length=32)

    def __str__(self):
        return self.name
1
2
3
4
5
6
7
class Article(RulesModel):

    # ...

    tags = models.ManyToManyField("Tag")

    # ...

Since we used fieldsets, remember to also add the field to ArticleAdmin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    # ...

    fieldsets = [
        # ...
        (
            "Meta",
            {
                "classes": ["collapse"],
                "fields": [
                    "category",
                    "tags",  # <--
                    "author",
                    "status",
                    "slug",
                    "published"
                ],
            },
        ),
    ]
Django Admin ManyToManyField default widget
Django Admin ManyToManyField default widget

Depending on how you have your models and fields structured, using the multi select might be annoying. Django provides some useful javascript widgets to solve this problem. They work exactly the same, the only difference being the orientation. We just need to add the ManyToManyField to either the filter_horizontal or filter_vertical list.

1
2
3
4
5
6
7
@admin.register(Article)
class ArticleAdmin(ModelAdmin):

    # ...

    filter_vertical = ("tags", )
    # OR: filter_horizontal = ("tags", )

Here's the result:

Django Admin ManyToManyField horizontal_filter widget
Django Admin ManyToManyField horizontal_filter widget
Django Admin ManyToManyField vertical_filter widget
Django Admin ManyToManyField vertical_filter widget

Read-Only Fields

In case you don't want to let some fields be editable, there's a config readonly_fields that helps you with that. This is useful for when you don't want to let the user override some business logic. In our example, we want the published field to be populated only when the publish action is ran. This is helpful because we will always know when the article was actually published.

1
2
3
4
5
6
7
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    # ...

    readonly_fields = ("published", )

    # ...
Django Admin readonly_fields
Django Admin readonly_fields

Django Admin Performance Tuning

Overriding get_queryset

When the Django Admin list view gets slow, it might be because you are displaying a field from a related model. For every row, Django makes an extra query to fetch the related object. Depending on how many rows and how costly the extra queries are the Django Admin can get quite slow. Here's how we can fix that by prefetching the related models:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    form = ArticleAdminForm
    list_display = (
        'title',
        'category',
        'author',
        'status',
        'author_article_count',
        'author_role'
    )

    # ...

    def author_article_count(self, obj):
        return obj.author.article_set.all().count()

    author_article_count.short_description = "Author Experience"

    def author_role(self, obj):
        return obj.author.role

    def get_queryset(self, request):
        return Article.objects.all().select_related(
            "category",
            "author",
        ).prefetch_related(
            "author__article_set"
        )

    # ...

Let's break that down. When displaying the list, Django will make an extra query for fetching an article's category, then its author, then the author's articles. This can be reduced quite a bit. We can do a JOIN SQL query using the select_related method to fetch the related Author and Category models and then we can prefetch the author's articles using prefetch_related. In the end we reduced the queries to only 2. If you want to better understand the issue, you can research the N + 1 Query Problem.

Using raw_id_fields

Here's a scenario. One of our models has a ForeignKey to another model. This related model has many instances and fetching all of them might prove very costly or maybe displaying a SelectWidget with a million choices is just bad user experience. Instead, we can just show the ID of the related model. Let's take the Comment model to exemplify this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@admin.register(Comment)
class CommentAdmin(ModelAdmin):

    raw_id_fields = ("article", )  # <--

    def summary(self, obj):
        return obj.text[:30]

    list_display = ('summary', )
    list_filter = ('article', )

Here's how our admin interface looks before and after adding the raw_id_fields:

Comment Admin without raw_id_fields
Comment Admin without raw_id_fields
Comment Admin with raw_id_fields
Comment Admin with raw_id_fields

Miscellaneous

Two different Admin views for the same model

For various reasons you might want to have different Django Admin interfaces for the same model. The most common scenario I've encountered was the need to provide a simple interface with few bells and whistles, and another one with advanced functionality for deeper debugging. You can't do this with Django Admin in a straightforward way, but there is an acceptable workaround: creating a proxy model.

1
2
3
4
5
6
7
8
9
class ArticleProxy(Article):
    class Meta:
        proxy = True
        verbose_name_plural = "Articles (Barebones view)"


@admin.register(ArticleProxy)
class ArticleProxyAdmin(ModelAdmin):
    pass
Django Admin using proxy model
Django Admin using proxy model

Writing custom filters for List View

The default Django Admin filters are great, but we might need something more depending on our business logic. I am going to show you how to write a filter with some very specific filtering capabilities using the SimpleListFilter class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class SpecialArticleTypeListFilter(admin.SimpleListFilter):
    title = "Special Type"
    parameter_name = "special_type"

    def lookups(self, request, model_admin):

        return [
            (
                'pending_review_written_by_editors',
                'Written By Editors & pending reviewed'
            ),
            (
                'published_by_contributors',
                'Published & Written By Contributors'
            ),
            (
                'published_by_inexperienced_contributors',
                'Published & Written By Inexperienced Contributors'
            ),
        ]

    def queryset(self, request, queryset):
        if self.value() == "pending_review_written_by_editors":
            return queryset.filter(
                status=Article.Status.PENDING_REVIEW).filter(
                author__role=User.Role.EDITOR
            )
        if self.value() == "published_by_contributors":
            return queryset.filter(
                status=Article.Status.PUBLISHED).filter(
                author__role=User.Role.CONTRIBUTOR
            )
        if self.value() == "published_by_inexperienced_contributors":
            return queryset.annotate(
                    experience=Count('author__article')).filter(
                status=Article.Status.PUBLISHED).filter(
                author__role=User.Role.CONTRIBUTOR).filter(
                experience__lte=3
            )
        return queryset

Then we add it to the Article admin like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@admin.register(Article)
class ArticleAdmin(ModelAdmin):

    # ...

    list_filter = (
        'author',
        'category',
        SpecialArticleTypeListFilter,  # <--
        'published',
        'status',
        'author__role'
    )

And here's the new filter in action:

Django Admin writing custom filters
Django Admin writing custom filters

We've covered a lot of ground in this comprehensive guide to mastering Django Admin customization. From simple tweaks to more advanced recipes, we've explored various methods to tailor the admin interface to suit your project's needs. If you have your own tips, tricks, share them in the comments section below!