This snippet provides a subclass of admin.ModelAdmin that lets you span foreign key relationships in list_display using '__'. The foreign key columns are sortable and have pretty names, and select_related() is set appropriately so you don't need queries for each line.
EDITS:
- Fixed error when DEBUG=False.
- Broke out
getter_for_related_field
so you can override short_description manually (see example). - Added regular foreign key fields to select_related(), since this is overriding the code in ChangeList that usually does it.
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 58 59 60 61 62 63 64 65 66 67 68 69 | from django.contrib import admin
from django.db import models
def getter_for_related_field(name, admin_order_field=None, short_description=None):
"""
Create a function that can be attached to a ModelAdmin to use as a list_display field, e.g:
client__name = getter_for_related_field('client__name', short_description='Client')
"""
related_names = name.split('__')
def getter(self, obj):
for related_name in related_names:
obj = getattr(obj, related_name)
return obj
getter.admin_order_field = admin_order_field or name
getter.short_description = short_description or related_names[-1].title().replace('_',' ')
return getter
class RelatedFieldAdminMetaclass(admin.ModelAdmin.__metaclass__):
"""
Metaclass used by RelatedFieldAdmin to handle fetching of related field values.
We have to do this as a metaclass because Django checks that list_display fields are supported by the class.
"""
def __new__(cls, name, bases, attrs):
new_class = super(RelatedFieldAdminMetaclass, cls).__new__(cls, name, bases, attrs)
for field in new_class.list_display:
if '__' in field:
setattr(new_class, field, getter_for_related_field(field))
return new_class
class RelatedFieldAdmin(admin.ModelAdmin):
"""
Version of ModelAdmin that can use related fields in list_display, e.g.:
list_display = ('address__city', 'address__country__country_code')
"""
__metaclass__ = RelatedFieldAdminMetaclass
def queryset(self, request):
qs = super(RelatedFieldAdmin, self).queryset(request)
# include all related fields in queryset
select_related = [field.rsplit('__',1)[0] for field in self.list_display if '__' in field]
# Include all foreign key fields in queryset.
# This is based on ChangeList.get_query_set().
# We have to duplicate it here because select_related() only works once.
# Can't just use list_select_related because we might have multiple__depth__fields it won't follow.
model = qs.model
for field_name in self.list_display:
try:
field = model._meta.get_field(field_name)
except models.FieldDoesNotExist:
continue
if isinstance(field.rel, models.ManyToOneRel):
select_related.append(field_name)
return qs.select_related(*select_related)
#### USAGE ####
class FooAdmin(RelatedFieldAdmin):
# these fields will work automatically:
list_display = ('address__phone','address__country__country_code','address__foo')
# ... but you can also define them manually if you need to override short_description:
address__foo = getter_for_related_field('address__foo', short_description='Custom Name')
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
Hi, thanks for usefull snippet.
I have problem running it in non-DEBUG mode. Although it is working perfectly when DEBUG=True is set, when I set it to False, I am always getting following error:
Any suggestions?
#
admin.ModelAdmin.metaclass not work in django 1.5+ .metaclass is deprecated
i replace for type(admin.ModelAdmin) and i think it work... but cant use this snippet like midlewareclass.. i need import in admin.py
sorry.. bad english
#
To get this working in newer Django versions, replace
by
#
To get it working in versions since 1.8, replace:
by
#
Please login first before commenting.