Django - why can't I access dynamically-genera

2019-07-05 17:55发布

问题:

Here is models.py:

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

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

    class Meta:
        abstract = True


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

In my admin.py I would like to configure the admin for Child to have the name attribute displayed, so I have the following:

class ChildAdmin(admin.ModelAdmin):
    model = Child

    def get_list_display(self, request):
        return ('id', 'name', 'magnitude')


admin.site.register(Child, ChildAdmin)

I have to have list_display generated on the fly from the get_list_display method because otherwise Django throws an error on startup complaining that name is not defined in the Child model. However, when running, name should be available as it is set in the __init__ method whenever the object is instantiated from the database.

However, when I try to load the admin page I get the error:

Unable to lookup 'name' on Child or ChildAdmin

What gives?

回答1:

<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.



回答2:

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.