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