Add a read-only field to Django admin’s change form template.

This is a draft post. Find out more about what that means. Get in touch if this post could use an improvement.

This is a part of the 100 Days To Offload challenge.

My employer is looking to hire a full-time junior Django developer.

Django comes with a fantastic admin area out of the box. Registering and customizing models is a breath of fresh air with its class based structure.

For the unaware, a change form template is shown when you edit/change an existing model instance, or an object.

Setting up context for this blog post.

First: I have set up a base model with soft deletion capabilities by following the excellent tutorial by Adrienne Domingus titled ‘Soft Deletion in Django‘. Written originally in 2017 and last updated in 2020, this is super useful and a highly recommended read. I got to see a ton of nice, production-quality patterns here.

The field is not editable (editable=False) as per the model definition. It is not shown in any form. The only way to interact with this field is to use class methods.

# model.py

class SoftDeletionModel(models.Model):
    deleted_at = models.DateTimeField(
        blank=False, null=True, default=None, editable=False
    )

Second: another tutorial-of-sorts I followed was from the Django Admin Cookbook. It turned up several times in my searches and is generally helpful to point me in the right direction, if nothing else. However, it doesn’t appear to have been updated since Django 2.0.

Its chapter, How to show an uneditable field in admin?, recommends the following solution:

If you have a field with editable=False in your model, that field, by default, is hidden in the change page. This also happens with any field marked as auto_now or auto_now_add, because that sets the editable=False on these fields.
If you want these fields to show up on the change page, you can add them to readonly_fields.

Django Admin Cookbook by Agiliq, retrieved 2021-05-24.

They share the following snippet next as a demonstration:

@admin.register(Villain)
class VillainAdmin(admin.ModelAdmin, ExportCsvMixin):
    # ...
    readonly_fields = ["added_on"]

I would highly recommend reading the Django Admin Cookbook article first, before proceeding any further. For the most part, it should take care of your use-case.

The edge-case I encountered, or why the cookbook advice did not work for me.

In my case, I found the snippet to be incomplete. For context, I am running Django 3.2, though that has nothing has to do with the solution. It’s just good to mention for posterity!

As any good Django citizen would, I looked up the API for a ModelAdmin on the Django documentation site, specifically for the readonly_fields property, and found the following note:

Note that when specifying ModelAdmin.fields or ModelAdmin.fieldsets the read-only fields must be present to be shown (they are ignored otherwise).

This makes sense, given my code (continuing the example offered by Django Admin Cookbook):

# admin.py, before the fix

@admin.register(Villain)
class VillainAdmin(admin.ModelAdmin, ExportCsvMixin):
    # ...
    fields = ("name",)
    readonly_fields = ("deleted_at",)

Since I had defined a custom fields property, I needed to have the model field on both my readonly_fields property as well as the fields property. Once I put this into action, everything worked as I expected.

Continuing the same example, the solution then is:

# admin.py, after the fix

@admin.register(Villain)
class VillainAdmin(admin.ModelAdmin, ExportCsvMixin):
    # ...
    fields = ("name", "deleted_at",)
    readonly_fields = ("deleted_at",)

And, that’s it!