bdoubleu
bdoubleu

Reputation: 6107

Django admin dynamically add to list_display and create ModelAdmin methods

I'd like to dynamically include some extra fields in the list_display of the admin for one of my models. I plan on overriding get_list_display to append a string representing a ModelAdmin method but how can I dynamically create the ModelAdmin methods?

In the example below if get_list_display returns [..., 'module_1', 'module_2'] that means I need methods for module_1 and module_2 as they're not model fields.

class Type(models.Model):
    ...
    modules = models.ManyToManyField(Module)

class User(AbstractBaseUser):
    ...
    type = models.ForeignKey(Type)

class UserModuleRecord(User):
    class Meta:
        proxy=True

@admin.register(UserModuleRecord)
class UserModuleRecordAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name']

    def get_queryset(self, request):
        return (
            super().get_queryset(request)
            .annotate_the_additional_list_display_values()
        )

    def get_list_display(self, request):
        list_display = super().get_list_display(request)
        modules = Module.objects.all()
        for module in modules:
            list_display.append('module_%s' % module.id)
        return list_display

Additionally is it possible to create something similar to how get_FOO_display works so there's only one admin method required?

UPDATE

I think I'm close with the following but am getting an error TypeError: 'partialmethod' object is not callable

from functools import partialmethod

def get_list_display(self, request):
    list_display = super().get_list_display(request)
    modules = Module.objects.all()
    for module in modules:
        attr_name = 'module_%s' % module.id
        list_display.append(attr_name)

        if not hasattr(self, attr_name):
            setattr(self, attr_name, partialmethod(
                self._module_id, field=attr_name
            ))
    return list_display

def _module_id(self, obj, field=''):
    return getattr(obj, field, '')

Upvotes: 2

Views: 1857

Answers (2)

bdoubleu
bdoubleu

Reputation: 6107

The following makes use of functools.partial to return a partial object which behaves like a function when called - similar to Django's get_FOO_display().

@admin.register(models.EmployeeTrainingRecord)
class EmployeeTrainingRecordAdmin(admin.ModelAdmin):
    ordering = ('email',)
    list_display = [
        'id',
        'first_name',
        'last_name',
    ]

    def get_queryset(self, request):
        return (
            super().get_queryset(request)
            .annotate_training_dates()
        )

    def get_list_display(self, request):
        list_display = super().get_list_display(request)
        training_modules = models.Training.objects.all()
        for training in training_modules:
            attr_name = 'training_%s' % training.id

            if not hasattr(self, attr_name):
                list_display.append(attr_name)
                func = partial(self._get_training_id, field=attr_name)
                func.short_description = training.name
                setattr(self, attr_name, func)
        return list_display

    @staticmethod
    def _get_training_id(obj, field=''):
        return getattr(obj, field, '')

Upvotes: 1

TimmyGee
TimmyGee

Reputation: 278

If I'm understanding your question correctly I think you're half way to the answer already.

As you know, to add anything custom to the list_display fields that isn't already a model field you need to implement it with a method on your ModelAdmin instance.

You can dynamically create field methods on the ModelAdmin instance using python's setattr and a wrapped function and doing it when the instance is initialised.

For example you could create a series of methods on your instance this way:

def make_dynamic_field(*args):
    def dynamic_field(self, obj):
        i = args[0]
        return f'{obj.first_name} {i}'
    return dynamic_field

@admin.register(UserModuleRecord)
class UserModuleRecordAdmin(admin.ModelAdmin):
    ...
    list_display = ['id', 'first_name', 'last_name', 'foo_0', 'foo_1']

    def __init__(self, model, admin_site):
        super().__init__(model, admin_site)

        for i in range(2):
            setattr(self, f'foo_{i}', make_dynamic_field(i))

I have used the index variable i as an example but you could potentially inject any variable into your dynamic field method this way.

Upvotes: 0

Related Questions