«List_display» dans un Django ModelAdmin peut-il afficher les attributs des champs ForeignKey?

296

J'ai un Personmodèle qui a une relation de clé étrangère avec Book, qui a un certain nombre de champs, mais je suis le plus préoccupé par author(un CharField standard).

Cela étant dit, dans mon PersonAdminmodèle, je voudrais afficher en book.authorutilisant list_display:

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

J'ai essayé toutes les méthodes évidentes pour le faire, mais rien ne semble fonctionner.

Aucune suggestion?

Huuuze
la source

Réponses:

472

Comme autre option, vous pouvez faire des recherches comme:

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'
imjoevasquez
la source
Les deux ne devraient pas l'être get_author, puisque c'est ce à quoi la chaîne que vous retournez (et la courte description) fait référence? Ou remplacez l'argument de format de chaîne par obj.book.reviews?
Carl G
1
@AnatoliyArkhipov, il existe un moyen (basé sur la réponse de Terr ). J'ai déjà mis à jour le code dans cette réponse.
Denilson Sá Maia
pourquoi ne pouvez-vous pas simplement avoir author = ForeignKey(Author)dans le modèle de livre, et ensuite list_display = ('author')?
alias51
3
Cela provoque une requête par ligne affichée dans l'admin :(
marcelm
1
@marcelm c'est pour ça select_related. le get_queryset()du UserAdmindevra être écrasé.
interDist
142

Malgré toutes les bonnes réponses ci-dessus et étant donné que j'étais nouveau à Django, j'étais toujours coincé. Voici mon explication d'un point de vue très novice.

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) - vous pensez que cela fonctionnerait en utilisant 'model__field' pour faire référence, mais cela ne fonctionne pas

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

admin.site.register(Book, BookAdmin)

admin.py (Correct Way) - c'est ainsi que vous référencez un nom de clé étrangère à la manière de Django

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)

Pour des références supplémentaires, voir le lien du modèle Django ici

Volonté
la source
3
pour le champ de commande ne devrait-il pas être = 'author__name'?
Yunti
2
Cela fonctionne parfaitement, mais je ne sais pas pourquoi. objest BookAdmin?
Steven Church
Sensationnel. Ça m'a pris une heure sur le web pour trouver ça. Cela devrait être beaucoup plus clair dans la documentation de Django
Sevenearths
67

Comme le reste, je suis allé avec des callables aussi. Mais ils ont un inconvénient: par défaut, vous ne pouvez pas commander sur eux. Heureusement, il existe une solution pour cela:

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'
Arjen
la source
la signature de la méthode devrait êtredef author(self, obj):
sheats
À l'époque où j'ai fait le commentaire, ce n'était pas le cas, mais il semble que depuis la version 1.8, la méthode obtient l'objet qui lui est transmis. J'ai mis à jour ma réponse.
Arjen
46

Veuillez noter que l'ajout de la get_authorfonction ralentirait le list_display dans l'admin, car afficher chaque personne ferait une requête SQL.

Pour éviter cela, vous devez modifier la get_querysetméthode dans PersonAdmin, par exemple:

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

Avant: 73 requêtes en 36,02 ms (67 requêtes dupliquées en admin)

Après: 6 requêtes en 10,81 ms

Faim
la source
3
C'est vraiment important et devrait toujours être implémenté
xleon
C'est important en effet. Alternativement, si l'on devait suivre la __str__route, il suffit d'ajouter la clé étrangère à list_displayetlist_select_related
Scratch'N'Purr
22

Selon la documentation, vous ne pouvez afficher que la __unicode__représentation d'une clé étrangère:

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

Semble étrange qu'il ne prend pas en charge le 'book__author'format de style qui est utilisé partout ailleurs dans l'API DB.

Il s'avère qu'il y a un ticket pour cette fonctionnalité , qui est marqué comme ne sera pas réparé.

Jonny Buchanan
la source
11
@Mermoz vraiment? Il semble que le ticket reste défini comme wontfix. Cela ne semble pas fonctionner non plus (Django 1.3)
Dave
1.11 n'existe toujours pas. Je fais du django depuis une douzaine d'années et je ne me souviens plus de celui-ci :(
Aaron McMillin
12

Je viens de publier un extrait de code qui permet à admin.ModelAdmin de prendre en charge la syntaxe '__':

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

Vous pouvez donc faire:

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

Il s'agit essentiellement de faire la même chose que celle décrite dans les autres réponses, mais cela prend automatiquement en charge (1) la configuration admin_order_field (2) la configuration short_description et (3) la modification du jeu de requêtes pour éviter un accès à la base de données pour chaque ligne.

Jack Cushman
la source
J'aime beaucoup cette idée, mais elle ne semble plus fonctionner avec les versions récentes de Django:AttributeError: type object 'BaseModel' has no attribute '__metaclass__'
Vincent van Leeuwen
10

Vous pouvez afficher tout ce que vous voulez dans l'affichage de la liste en utilisant un appelable. Cela ressemblerait à ceci:

def book_author (objet):
  renvoyer object.book.author

classe PersonAdmin (admin.ModelAdmin):
  list_display = [book_author,]

la source
Celui-ci convient aux situations où de nombreux modèles différents appellent souvent le même attribut; est-il pris en charge dans la version 1.3+?
kagali-san
3
Le problème à ce sujet est la quantité de requêtes SQL effectuées à la fin. Pour chaque objet de la liste, il fera une requête. C'est pourquoi «field__attribute» serait très pratique, car Django ne couvrirait cela que sur une seule requête SQL. Bizarre qu'il n'y ait pas de support déjà.
emyller
7

Celui-ci est déjà accepté, mais s'il y a d'autres mannequins (comme moi) qui ne l'ont pas immédiatement obtenu de la réponse actuellement acceptée , voici un peu plus de détails.

La classe de modèle référencée par le ForeignKeybesoin d'avoir une __unicode__méthode en elle, comme ici:

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

    def __unicode__(self):
        return self.name

Cela a fait la différence pour moi et devrait s'appliquer au scénario ci-dessus. Cela fonctionne sur Django 1.0.2.

Communauté
la source
4
Sur python 3, ce serait def __str__(self):.
Martlark
5

Si vous avez beaucoup de champs d'attributs de relation à utiliser list_displayet que vous ne voulez pas créer une fonction (et ses attributs) pour chacun, une solution simple mais sale serait de remplacer la méthode ModelAdmininstace __getattr__, créant les callables à la volée:

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

Comme l' essentiel ici

Les attributs spéciaux appelables comme booleanet short_descriptiondoivent être définis comme des ModelAdminattributs, par exemple book__author_verbose_name = 'Author name'et category__is_new_boolean = True.

L' admin_order_fieldattribut callable est défini automatiquement.

N'oubliez pas d'utiliser l' attribut list_select_related dans votre ModelAdminpour que Django évite les requêtes supplémentaires.

Cauê Thenório
la source
1
Je viens de l'essayer avec une installation de Django 2.2 et cela a très bien fonctionné pour moi, contrairement à d'autres approches, pour une raison quelconque. Notez que de nos jours, vous devez importer des réduire à partir de functools ou d'ailleurs ...
Paul Brackin
5

Il existe un package très facile à utiliser disponible dans PyPI qui gère exactement cela: django-related-admin . Vous pouvez également voir le code dans GitHub .

En utilisant cela, ce que vous voulez réaliser est aussi simple que:

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

Les deux liens contiennent tous les détails de l'installation et de l'utilisation, donc je ne les collerai pas ici au cas où ils changeraient.

Tout comme une note de côté, si vous utilisez déjà autre chose que model.Admin(par exemple , j'utilisais à la SimpleHistoryAdminplace), vous pouvez le faire: class MyAdmin(SimpleHistoryAdmin, RelatedFieldAdmin).

Vlad Schnakovszki
la source
getter_for_related_field ne fonctionne pas en 1.9, il ne semble donc pas être le meilleur choix pour ceux qui aiment la personnalisation.
GriMel
4

si vous l'essayez en ligne, vous ne réussirez pas sauf si:

dans votre ligne:

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

dans votre modèle (MyModel):

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

    def localname(self):
        return self.localization.name
Eyal Ch
la source
-1

La réponse d'AlexRobbins a fonctionné pour moi, sauf que les deux premières lignes doivent être dans le modèle (peut-être que cela a été supposé?), Et devraient se référencer soi-même:

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

Ensuite, la partie admin fonctionne bien.


la source
-5

Je préfère ça:

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

    @staticmethod
    def submodel__field(obj):
        return obj.submodel.field
wieczorek1990
la source