Setting readonly_fields attribute of a ModelAdmin affects the following parts of Django admin interface:

readonly_fields affects several aspects of the Django admin interface:

  • Detail view (change form)

    • Fields become non-editable in the add/edit forms
    • Displayed as plain text or formatted HTML
    • Form inputs are disabled/removed
  • Add form (new object creation)

  • List view remains unaffected
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    readonly_fields = ('created_dt',)
    list_display = ('id', 'created_dt')  # Still shows in list view
    list_editable = ('status',)  # Other fields can be editable in list view

The readonly_fields property does NOT Affect: - Database constraints - Model validation - API access - Direct ORM model operations

# These operations still work regardless of readonly_fields
order = Order.objects.create(created_dt=some_date)  # Works
order.created_dt = new_date  # Works
order.save()  # Works

Only Affects Admin Interface

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    readonly_fields = ('status',)  # Only affects admin interface

    def save_model(self, request, obj, form, change):
        # This can still modify readonly fields
        obj.status = 'new_status'
        super().save_model(request, obj, form, change)

The readonly_fields attribute is purely an admin interface restriction and doesn't provide any data security at the model level.

How to define readonly fields

The model for this example:

class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    created_dt = models.DateTimeField(auto_now_add=True)
    completed_dt = models.DateTimeField(null=True, blank=True)
    status = models.IntegerField(default=ORDER_STATUSES.new, choices=ORDER_STATUSES_CHOICES)

In admin.py:

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    readonly_fields = ('created_dt', 'completed_dt')
    list_display = ('id', 'customer', 'status', 'created_dt', 'completed_dt')

Readonly method fields

You can include a calculated/property field into readonly_fields

class Order(models.Model):
    items = models.ManyToManyField(Product)
    total_amount = models.DecimalField(max_digits=10, decimal_places=2)

    @property
    def items_count(self):
        return self.items.count()

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    readonly_fields = ('total_amount', 'items_count')  # Calculated fields

Or a method on the ModelAdmin itself

from django.contrib import admin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.urls import reverse
from decimal import Decimal

class Order(models.Model):
    customer = models.ForeignKey('Customer', on_delete=models.CASCADE)
    created_dt = models.DateTimeField(auto_now_add=True)
    status = models.IntegerField(choices=ORDER_STATUSES_CHOICES)

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ('id', 'customer', 'total_cost', 'customer_link')
    readonly_fields = ('total_cost', 'customer_link')

    def total_cost(self, obj):
        # Calculate total cost from related OrderItems
        total = sum(
            item.product.price * item.quantity 
            for item in obj.orderitem_set.all()
        )
        return f"${total:.2f}"
    total_cost.short_description = "Total Cost"  # Column header in admin

    def customer_link(self, obj):
        if obj.customer:
            url = reverse('admin:myapp_customer_change', args=[obj.customer.id])
            return format_html('<a href="{}">{}</a>', url, obj.customer)
        return "-"
    customer_link.short_description = "Customer Details"

Dynamic Conditional Readonly fields

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    def get_readonly_fields(self, request, obj=None):
        if obj:  # Editing existing object
            return ('created_dt', 'status',)
        if obj.completed_dt: # If order is completed
            return [f.name for f in self.model._meta.fields]  # Make all fields readonly
        return ()  # New object, no readonly fields