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