Can "list_display" in a Django ModelAdmin display attributes of ForeignKey fields?
Asked Answered
N

15

422

I have a Person model that has a foreign key relationship to Book, which has a number of fields, but I'm most concerned about author (a standard CharField).

With that being said, in my PersonAdmin model, I'd like to display book.author using list_display:

class PersonAdmin(admin.ModelAdmin):
    list_display = ['book.author',]

I've tried all of the obvious methods for doing so, but nothing seems to work.

Any suggestions?

Nimitz answered 2/10, 2008 at 18:26 Comment(0)
T
664

As another option, you can do lookups like:

#models.py
class UserAdmin(admin.ModelAdmin):
    list_display = (..., 'get_author')
    
    def get_author(self, obj):
        return obj.book.author
    get_author.short_description = 'Author'
    get_author.admin_order_field = 'book__author'

Since Django 3.2 you can use display() decorator:

#models.py
class UserAdmin(admin.ModelAdmin):
    list_display = (..., 'get_author')
    
    @admin.display(ordering='book__author', description='Author')
    def get_author(self, obj):
        return obj.book.author
Transmigrate answered 2/10, 2008 at 21:11 Comment(12)
Shouldn't both be get_author, since that is what the string you're returning (and the short description) actually reference? Or change string format argument to obj.book.reviews?Myrilla
@AnatoliyArkhipov, there is a way (based on Terr answer). I've already updated the code in this answer.Towelling
why can't you just have author = ForeignKey(Author) in the book model, and then list_display = ('author')?Coquelicot
Gah, I accidentally clicked downvote instead of upvote and it won't let me change it unless the answer is edited. :( Sorry.Methodize
@alias51: because the user is not linked to an author, but to a book, and the book is linked to an author?Galwegian
This causes one query per row displayed in the admin :(Stadium
@Stadium that’s what select_related is for. the get_queryset() of the UserAdmin will have to be overwritten.Afferent
For Django Version > 3.2, please refer this answer: https://mcmap.net/q/86003/-can-quot-list_display-quot-in-a-django-modeladmin-display-attributes-of-foreignkey-fieldsTessatessellate
The display decorator is defined as @admin.display(....)Eccrine
What's the use of ellipsis... before 'get_author'?Extrusive
@dark_prince, I think that's meant to suggest other fields in the list display in this example, it's not actual code you would execute.Sven
Can something similar be done except that a) it's displayed in the details view, i.e. not list_display=[...] but fields=[...], and b) what's displayed is a link to the detail page of the related object?Sandrasandro
T
203

Despite all the great answers above and due to me being new to Django, I was still stuck. Here's my explanation from a very newbie perspective.

models.py

class Author(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    author = models.ForeignKey(Author)
    title = models.CharField(max_length=255)

admin.py (Incorrect Way) - you think it would work by using 'model__field' to reference, but it doesn't

class BookAdmin(admin.ModelAdmin):
    model = Book
    list_display = ['title', 'author__name', ]

admin.site.register(Book, BookAdmin)

admin.py (Correct Way) - this is how you reference a foreign key name the Django way

class BookAdmin(admin.ModelAdmin):
    model = Book
    list_display = ['title', 'get_name', ]

    def get_name(self, obj):
        return obj.author.name
    get_name.admin_order_field  = 'author'  #Allows column order sorting
    get_name.short_description = 'Author Name'  #Renames column head

    #Filtering on side - for some reason, this works
    #list_filter = ['title', 'author__name']

admin.site.register(Book, BookAdmin)

For additional reference, see the Django model link here

Taper answered 19/5, 2014 at 21:55 Comment(4)
for order field shouldn't it be = 'author__name' ?Signalize
This works perfectly, however I am unsure why. obj is BookAdmin?Ploce
Wow. Took me an hour on the web to find this. This should be made a lot clearer in the Django documentationWestberg
Thanks @Will. Do you realize that for list_display, [..., 'get_name', ] has to be assigned but for search_field, it does not work, instead, [..., 'author__name', ] has to be assigned? It seems counter-intuitive for me, isn't it?Dufour
A
74

Like the rest, I went with callables too. But they have one downside: by default, you can't order on them. Fortunately, there is a solution for that:

Django >= 1.8

def author(self, obj):
    return obj.book.author
author.admin_order_field  = 'book__author'

Django < 1.8

def author(self):
    return self.book.author
author.admin_order_field  = 'book__author'
Aedile answered 28/7, 2010 at 9:13 Comment(3)
method signature should be def author(self, obj):Hagan
Back when I made the comment it wasn't the case but it appears that since version 1.8 the method gets the object passed to it. I've updated my answer.Aedile
Depends whether you define it on the admin or the model, ehHouseclean
I
74

Please note that adding the get_author function would slow the list_display in the admin, because showing each person would make a SQL query.

To avoid this, you need to modify get_queryset method in PersonAdmin, for example:

def get_queryset(self, request):
    return super(PersonAdmin,self).get_queryset(request).select_related('book')

Before: 73 queries in 36.02ms (67 duplicated queries in admin)

After: 6 queries in 10.81ms

Indivisible answered 28/1, 2015 at 11:20 Comment(3)
This is really important and should always be implementedEricaericaceous
This is important indeed. Alternatively, if one were to go down the __str__ route, just add the foreignkey to list_display and list_select_relatedGuadiana
"list_select_related" is the best solution to the title questionEstuary
C
41

For Django >= 3.2

The proper way to do it with Django 3.2 or higher is by using the display decorator

class BookAdmin(admin.ModelAdmin):
    model = Book
    list_display = ['title', 'get_author_name']

    @admin.display(description='Author Name', ordering='author__name')
    def get_author_name(self, obj):
        return obj.author.name
Corncrib answered 29/5, 2021 at 0:52 Comment(0)
B
24

According to the documentation, you can only display the __unicode__ representation of a ForeignKey:

http://docs.djangoproject.com/en/dev/ref/contrib/admin/#list-display

Seems odd that it doesn't support the 'book__author' style format which is used everywhere else in the DB API.

Turns out there's a ticket for this feature, which is marked as Won't Fix.

Bianca answered 2/10, 2008 at 18:53 Comment(2)
@Mermoz really? It appears the ticket remains set as wontfix. It doesn't appear to work, either (Django 1.3)Reserpine
1.11 still doesn't exist. Been doing django for a dozen years and I never remember this one :(Now
G
13

I just posted a snippet that makes admin.ModelAdmin support '__' syntax:

http://djangosnippets.org/snippets/2887/

So you can do:

class PersonAdmin(RelatedFieldAdmin):
    list_display = ['book__author',]

This is basically just doing the same thing described in the other answers, but it automatically takes care of (1) setting admin_order_field (2) setting short_description and (3) modifying the queryset to avoid a database hit for each row.

Gunmaker answered 3/2, 2013 at 21:21 Comment(1)
I like this idea a lot, but it doesn't seem to work anymore with recent django verions: AttributeError: type object 'BaseModel' has no attribute '__metaclass__'Gingerich
A
12

There is a very easy to use package available in PyPI that handles exactly that: django-related-admin. You can also see the code in GitHub.

Using this, what you want to achieve is as simple as:

class PersonAdmin(RelatedFieldAdmin):
    list_display = ['book__author',]

Both links contain full details of installation and usage so I won't paste them here in case they change.

Just as a side note, if you're already using something other than model.Admin (e.g. I was using SimpleHistoryAdmin instead), you can do this: class MyAdmin(SimpleHistoryAdmin, RelatedFieldAdmin).

Acronym answered 28/5, 2016 at 10:30 Comment(2)
getter_for_related_field doesn't work in 1.9 so It seems to be not the best choice to those who likes customizing.Lyricist
This library is up to date and works great for us here on Django 3.2Keegan
Y
11

You can show whatever you want in list display by using a callable. It would look like this:


def book_author(object):
  return object.book.author

class PersonAdmin(admin.ModelAdmin):
  list_display = [book_author,]
Yockey answered 30/1, 2009 at 17:41 Comment(2)
This one is nice for situations, where a lot of different models often call to same attribute; is it supported in 1.3+?Bequeath
The problem about this is the amount of SQL queries done in the end. For each object in the list, it will make a query. This is why 'field__attribute' would be very handy, because certainly Django would span that to one SQL query only. Odd that there is no support this already.Pallet
T
6

This one's already accepted, but if there are any other dummies out there (like me) that didn't immediately get it from the presently accepted answer, here's a bit more detail.

The model class referenced by the ForeignKey needs to have a __unicode__ method within it, like here:

class Category(models.Model):
    name = models.CharField(max_length=50)

    def __unicode__(self):
        return self.name

That made the difference for me, and should apply to the above scenario. This works on Django 1.0.2.

Tiffanietiffanle answered 23/1, 2009 at 23:40 Comment(1)
On python 3 this would be def __str__(self):.Cavan
J
6

If you have a lot of relation attribute fields to use in list_display and do not want create a function (and it's attributes) for each one, a dirt but simple solution would be override the ModelAdmin instace __getattr__ method, creating the callables on the fly:

class DynamicLookupMixin(object):
    '''
    a mixin to add dynamic callable attributes like 'book__author' which
    return a function that return the instance.book.author value
    '''

    def __getattr__(self, attr):
        if ('__' in attr
            and not attr.startswith('_')
            and not attr.endswith('_boolean')
            and not attr.endswith('_short_description')):

            def dyn_lookup(instance):
                # traverse all __ lookups
                return reduce(lambda parent, child: getattr(parent, child),
                              attr.split('__'),
                              instance)

            # get admin_order_field, boolean and short_description
            dyn_lookup.admin_order_field = attr
            dyn_lookup.boolean = getattr(self, '{}_boolean'.format(attr), False)
            dyn_lookup.short_description = getattr(
                self, '{}_short_description'.format(attr),
                attr.replace('_', ' ').capitalize())

            return dyn_lookup

        # not dynamic lookup, default behaviour
        return self.__getattribute__(attr)


# use examples    

@admin.register(models.Person)
class PersonAdmin(admin.ModelAdmin, DynamicLookupMixin):
    list_display = ['book__author', 'book__publisher__name',
                    'book__publisher__country']

    # custom short description
    book__publisher__country_short_description = 'Publisher Country'


@admin.register(models.Product)
class ProductAdmin(admin.ModelAdmin, DynamicLookupMixin):
    list_display = ('name', 'category__is_new')

    # to show as boolean field
    category__is_new_boolean = True

As gist here

Callable especial attributes like boolean and short_description must be defined as ModelAdmin attributes, eg book__author_verbose_name = 'Author name' and category__is_new_boolean = True.

The callable admin_order_field attribute is defined automatically.

Don't forget to use the list_select_related attribute in your ModelAdmin to make Django avoid aditional queries.

Joeljoela answered 12/1, 2016 at 3:42 Comment(1)
Just tried this with a Django 2.2 install & it worked great for me while other approaches did not, for whatever reason. Note that nowadays you need to import reduce from functools or elsewhere...Kort
E
6

I may be late, but this is another way to do it. You can simply define a method in your model and access it via the list_display as below:

models.py

class Person(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)

    def get_book_author(self):
        return self.book.author

admin.py

class PersonAdmin(admin.ModelAdmin):
    list_display = ('get_book_author',)

But this and the other approaches mentioned above add two extra queries per row in your listview page. To optimize this, we can override the get_queryset to annotate the required field, then use the annotated field in our ModelAdmin method

admin.py

from django.db.models.expressions import F

@admin.register(models.Person)
class PersonAdmin(admin.ModelAdmin):
    list_display = ('get_author',)
    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        queryset = queryset.annotate(
            _author = F('book__author')
        )
        return queryset

    @admin.display(ordering='_author', description='Author')
    def get_author(self, obj):
        return obj._author
Eccrine answered 28/9, 2021 at 10:53 Comment(0)
S
5

if you try it in Inline, you wont succeed unless:

in your inline:

class AddInline(admin.TabularInline):
    readonly_fields = ['localname',]
    model = MyModel
    fields = ('localname',)

in your model (MyModel):

class MyModel(models.Model):
    localization = models.ForeignKey(Localizations)

    def localname(self):
        return self.localization.name
Seigniory answered 30/1, 2014 at 12:30 Comment(0)
L
-1

AlexRobbins' answer worked for me, except that the first two lines need to be in the model (perhaps this was assumed?), and should reference self:

def book_author(self):
  return self.book.author

Then the admin part works nicely.

Lavenialaver answered 25/3, 2009 at 0:27 Comment(0)
A
-5

I prefer this:

class CoolAdmin(admin.ModelAdmin):
    list_display = ('pk', 'submodel__field')

    @staticmethod
    def submodel__field(obj):
        return obj.submodel.field
Asthmatic answered 22/9, 2016 at 14:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.