3

I have a use case where I need to retrieve status information for each row in the Django model admin list view.

I can retrieve data using code like:

def blah(admin.ModelAdmin):
    @staticmethod
    def status(instance):
      return Blah(instance).get_info()['status']
       
    readonly_fields = ('id', 'status')

However, this 'Blah' class returns both the status and progress. Is there an easy way to call this 'Blah' class with the instance, return the status field and also a progress field and add both to the readonly_fields tuple without duplication like:

def blah(admin.ModelAdmin):
    @staticmethod
    def status(instance):
      return Blah(instance).get_info()['status']

    @staticmethod
    def progress(instance):
      return Blah(instance).get_info()['progress']
       
    readonly_fields = ('id', 'status', 'progress')
Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
Mike91
  • 520
  • 2
  • 9
  • 25

1 Answers1

1

I think you may use a class decorator.

def get_blah_info(field):
    return staticmethod(lambda x: Blah(x).get_info()[field])

def blah_decorator(*fields):
    def wrapper(cls):
        for field in fields:
            setattr(cls, field, get_blah_info(field))
            cls.readonly_fields.append(field)
        return cls
    return wrapper

@blah_decorator('status', 'progress')
class BlahAdmin(admin.ModelAdmin):
    readonly_fields = ['id']

But I don't catch why you are using a static method.

A more advanced example:

from django.utils.translation import ugettext_lazy as _

def get_blah_info(blah_class, field):
    def get_info(self, instance):
        return blah_class(instance).get_info()[field]
    return get_info

def blah_decorator(blah_class, **fields):
    def wrapper(cls):
        # Make sure readonly_fields is a list so that we can append elements
        readonly_fields = getattr(cls, 'readonly_fields', [])
        if not hasattr(readonly_fields, 'append'):
            readonly_fields = list(readonly_fields)

        for field, short_description in fields.items():
            # Define the method for each field and append it to readonly_fields
            get_info = get_blah_info(blah_class, field)
            get_info.__name__ = field
            get_info.short_description = short_description
            setattr(cls, field, get_info)
            readonly_fields.append(field)
        cls.readonly_fields = readonly_fields
        return cls
    return wrapper

@blah_decorator(Blah, status=_("Status"), progress=_("Progress"))
class BlahAdmin(admin.ModelAdmin):
    readonly_fields = ['id']

Of course, the above example can be adapted to use static methods if you prefer.


Another solution would be to use a metaclass.

class BlahMetaclass(type):

    @staticmethod
    def get_blah_info(blah_class, field):
        def get_info(self, instance):
            return blah_class(instance).get_info()[field]
        return get_info

    def __new__(cls, cls_name, bases, attrs):
        blah_class = attrs['blah_class']
        blah_fields = attrs['blah_fields']
        readonly_fields = attrs.get('readonly_fields', [])
        if not hasattr(readonly_fields, 'append'):
            readonly_fields = list(readonly_fields)

        for field, short_description in blah_fields:
            if field in attrs:
                continue  # Let the class have the precedence
            get_info = cls.get_blah_info(blah_class, field)
            get_info.__name__ = field
            get_info.short_description = short_description
            attrs[field] = get_info
            if field not in readonly_fields:
                # Do not add `field` to `readonly_fields` if it is already present.
                # This enables to redefine the fields order rather than
                # appending `blah_fields`.
                readonly_fields.append(readonly_fields)

        attrs['readonly_fields'] = readonly_fields

        # Optionally remove `blah_class` and `blah_fields` if
        # not useful any further.
        del attrs['blah_class']
        del attrs['blah_fields']

        return super().__new__(cls, clsname, bases, attrs)


class BlahModelAdmin(admin.ModelAdmin, metaclass=BlahMetaclass):
    """Optionally, create a new base ModelAdmin."""


class BlahAdmin(BlahModelAdmin):

    blah_class = Blah
    blah_fields = [
        ('status' _("Status")),
        ('progress', _("Progress")),
    ]

    readonly_fields = ['id']
    # Or, for instance: readonly_fields = ['status', 'id', 'progress']
    # If you want to change the order
Community
  • 1
  • 1
Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87
  • I tried the first two solutions but couldn't get it working. The lambda solution just returned the progress for both fields, and the second decorator solution returned the status for both fields. – Mike91 Jul 13 '16 at 08:04
  • Yes obviously, that's because when `get_info()[field]` is called, the loop is already ended and `fields` has still its last value. I fixed the issue. – Antoine Pinsard Jul 13 '16 at 08:24