[Django]-Django – why can't I access dynamically-generated attributes in classes in my models.py from admin.py?

6👍

<class 'app.admin.ChildAdmin'>: (admin.E108) The value of 'list_display[1]' 
refers to 'name', which is not a callable, an attribute of 'ChildAdmin', 
or an attribute or method on 'app.Child'.

The above is more than likely the error message that you are receiving. Abstract classes do not allow you to inherit instance attributes from the abstract class like that. It is looking for self.name on the Child class, which does not exist.

The parts of the error we want to look at is:

…which is not a callable

Nope. It is not a callable, it is an attribute.

…an attribute of ‘ChildAdmin’,

Nope. It isn’t an attribute of the ChildAdmin class.

…or an attribute or method on ‘app.Child’.

This is the part that is tripping you up. “What gives” is that it isn’t an attribute or method of the Child class, but it is for the Parent class.

What you want to do is:

class Parent(models.Model):
    id = models.CharField(max_length=14, primary_key=True)
    json_dump = models.TextField(null=False)

    class Meta:
        abstract = True

    @property
    def name(self):
        return json.loads(self.json_dump)['name']

class Child(Parent):
    magnitude  = models.IntegerField()

Doing this will make the attribute available to the Child class from parent. Alternatively, instead of using the @property decorator, you could create a function definition called get_name. I find the first to be simpler.

The caveat to this method is that it does not save the name at runtime. If you want to do that, you may want to consider Django’s signals to do a post_save hook to retrieve the name value and add a name = models.CharField(...) to your model.

For clarification, Django does not support this. On startup, the following code runs a check on the list_display property:

def _check_list_display_item(self, cls, model, item, label):
    """
    cls=<class 'app.admin.ChildAdmin'>
    model=<class 'app.models.Child'>
    item='name'
    """
    # 'name' is not callable
    if callable(item):  
        return []
    # <class 'app.admin.ChildAdmin'> does not have the class attribute 'name'
    elif hasattr(cls, item):
        return []
    # <class 'app.models.Child'> does not have the class attribute 'name'
    elif hasattr(model, item): 
        ...
    else:
        try:
            # <class 'app.models.Child'>.Meta does not have a field called 'name'
            model._meta.get_field(item)
        except models.FieldDoesNotExist:
            # This is where you end up.
            return [
                # This is a deliberate repeat of E108; there's more than one path
                # required to test this condition.
                checks.Error(
                    "The value of '%s' refers to '%s', which is not a callable, an attribute of '%s', or an attribute or method on '%s.%s'." % (
                        label, item, cls.__name__, model._meta.app_label, model._meta.object_name
                    ),
                    hint=None,
                    obj=cls,
                    id='admin.E108',
                )
            ]

As you can see, I have added comments to the code that is run to help you understand what is happening. You are right that the INSTANCE of Child does have name, but that isn’t what Django is looking for. It’s looking for a class attribute, not instance attribute.

So, another way you can tackle this (you won’t like this, either) is by doing:

class Parent(models.Model):
    id         = models.CharField(max_length=14, primary_key=True)
    json_dump  = models.TextField(null=False)
    name       = ''
    other_item = ''
    this_too   = ''
    and_this   = ''

    class Meta:
        abstract = True

    def __init__(self, *args, **kwargs):
        super(Parent, self).__init__(*args, **kwargs)
        setattr(self, 'name', json.loads(self.json_dump)['name'])
        setattr(self, 'other_item', json.loads(self.json_dump)['other_item'])
        setattr(self, 'this_too', json.loads(self.json_dump)['this_too'])
        setattr(self, 'and_this', json.loads(self.json_dump)['and_this'])

This works. I just tested it. Django will find the attribute on the class.

0👍

As mentioned in doc, change Parent model like this:

class Parent(models.Model):
    id        = models.CharField(max_length=14, primary_key=True)
    json_dump = models.TextField(null=False)

    def name(self):
        return json.loads(self.json_dump)['name']

    class Meta:
        abstract = True

So name attribute will be shown.

Leave a comment