Mdjon26
Mdjon26

Reputation: 2255

How to show a many-to-many field with "list_display" in Django Admin?

class Product(models.Model):
    products = models.CharField(max_length=256)
    
    def __unicode__(self):
        return self.products

class PurchaseOrder(models.Model):
    product = models.ManyToManyField('Product')
    vendor = models.ForeignKey('VendorProfile')
    dollar_amount = models.FloatField(verbose_name='Price')

I have that code. Unfortunately, the error comes in admin.py with the ManyToManyField

class PurchaseOrderAdmin(admin.ModelAdmin):
    fields = ['product', 'dollar_amount']
    list_display = ('product', 'vendor')

The error says:

'PurchaseOrderAdmin.list_display[0]', 'product' is a ManyToManyField which is not supported.

However, it compiles when I take 'product' out of list_display. So how can I display 'product' in list_display without giving it errors?

edit: Maybe a better question would be how do you display a ManyToManyField in list_display?

Upvotes: 145

Views: 98783

Answers (4)

For example, there are Category and Product models which have many-to-many relationship as shown below. *I use Django 4.2.1:

# "models.py"

from django.db import models

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

    def __str__(self):
        return self.name

class Product(models.Model):
    categories = models.ManyToManyField(Category)
    name = models.CharField(max_length=50)
    price = models.DecimalField(decimal_places=2, max_digits=5)

Then, there are Category and Product admins as shown below:

# "admin.py"

from django.contrib import admin
from .models import Category, Product

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('id', 'name')
    ordering = ('id',)

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'price')
    ordering = ('id',)

Then, Category admin has 5 objects as shown below:

enter image description here

And, Product admin has 6 objects as shown below:

enter image description here

Now, define get_products and get_categories() with @admin.display(), then set them to list_display in Category and Product admins respectively as shown below. *description argument in @admin.display() can rename the columns in Django Admin from GET PRODUCTS and GET CATEGORIES to PRODUCTS and CATEGORIES respectively:

# "admin.py"

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'get_products')
    ordering = ('id',)            # Here

    # Here
    @admin.display(description='products')
    def get_products(self, obj):
        return [product.name for product in obj.product_set.all()]

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):      # Here  
    list_display = ('id', 'name', 'price', 'get_categories')
    ordering = ('id',)
    
    # Here
    @admin.display(description='categories')
    def get_categories(self, obj):
        return [category.name for category in obj.categories.all()]

Then, PRODUCTS column is displayed in Category admin as shown below:

enter image description here

Then, CATEGORIES column is displayed in Product admin as shown below:

enter image description here

Also, you can display PRODUCTS and CATEGORIES columns by defining get_products() and get_categories() with @admin.display() in Category and Product models respectively as shown below:

# "models.py"

from django.db import models
from django.contrib import admin

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

    def __str__(self):
        return self.name

    # Here
    @admin.display(description='products')
    def get_products(self):
        return [product.name for product in self.product_set.all()]

class Product(models.Model):
    categories = models.ManyToManyField(Category)
    name = models.CharField(max_length=50)
    price = models.DecimalField(decimal_places=2, max_digits=5)

    # Here
    @admin.display(description='categories')
    def get_categories(self):
        return [category.name for category in self.categories.all()]

Then, set them to list_display in Category and Product admins respectively as shown below:

# "admin.py"

...

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'get_products')
    ordering = ('id',)            # Here

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):      # Here
    list_display = ('id', 'name', 'price', 'get_categories')
    ordering = ('id',)

Upvotes: 6

Lucas B
Lucas B

Reputation: 2538

If you want to save extra queries, you can use prefetch_related in the get_queryset method like below:

class PurchaseOrderAdmin(admin.ModelAdmin):
    fields = ['product', 'dollar_amount']
    list_display = ('get_products', 'vendor')

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        return qs.prefetch_related('product')

    def get_products(self, obj):
        return ",".join([p.products for p in obj.product.all()])

According to the Docs, In this way, there would be just one extra query needed to fetch related Product items of all PurchaseOrder instances instead of needing one query per each PurchaseOrder instance.

Upvotes: 25

karthikr
karthikr

Reputation: 99670

You may not be able to do it directly. From the documentation of list_display

ManyToManyField fields aren’t supported, because that would entail executing a separate SQL statement for each row in the table. If you want to do this nonetheless, give your model a custom method, and add that method’s name to list_display. (See below for more on custom methods in list_display.)

You can do something like this:

class PurchaseOrderAdmin(admin.ModelAdmin):
    fields = ['product', 'dollar_amount']
    list_display = ('get_products', 'vendor')

    def get_products(self, obj):
        return "\n".join([p.products for p in obj.product.all()])

OR define a model method, and use that

class PurchaseOrder(models.Model):
    product = models.ManyToManyField('Product')
    vendor = models.ForeignKey('VendorProfile')
    dollar_amount = models.FloatField(verbose_name='Price')

    def get_products(self):
        return "\n".join([p.products for p in self.product.all()])

and in the admin list_display

list_display = ('get_products', 'vendor')

Upvotes: 252

JKV
JKV

Reputation: 331

This way you can do it, kindly checkout the following snippet:

class Categories(models.Model):
    """ Base category model class """

    title       = models.CharField(max_length=100)
    description = models.TextField()
    parent      = models.ManyToManyField('self', default=None, blank=True)
    when        = models.DateTimeField('date created', auto_now_add=True)

    def get_parents(self):
        return ",".join([str(p) for p in self.parent.all()])

    def __unicode__(self):
        return "{0}".format(self.title)

And in your admin.py module call method as follows:

class categories(admin.ModelAdmin):
    list_display    = ('title', 'get_parents', 'when')

Upvotes: 32

Related Questions