Reputation: 765
I'd like to add "annotation count" field into list_display in Django Admin.
models.py
class Log(models.Model):
...
ip_address = models.GenericIPAddressField(
verbose_name=_('IP address'),
db_index=True,
)
...
admin.py
class LogAdmin(admin.ModelAdmin):
list_display = (..., 'ip_address', 'ip_address_count', ...)
def ip_address_count(self, instance):
return models.Log.objects \
.filter(ip_address=instance.ip_address) \
.count()
ip_address_count.short_description = _('IP Address Count')
It works fine with "LOTS OF" SQL queries.
I'd like to improve my admin.py
like this:
class Log(models.Model):
...
def get_queryset(self, request):
qs = super(LogAdmin, self).get_queryset(request)
qs = qs.values('ip_address') \
.annotate(ip_address_count=Count('ip_address'))
return qs
def ip_address_count(self, instance):
return instance.ip_address_count
However, I encountered the error messages:
'dict' object has no attribute '_meta'
It seems that I found the reason why the error occurs:
Django Admin, can't groupe by: Exception Value: 'dict' object has no attribute '_meta'
However, it does not help me to solve the problem.
Thank you for your answers.
Edit: It works wrong if I don't append .values('ip_address)
.
It does not group by ip_address
. It group by
all the fields group by field1, field2, ip_address, ...
Therefore, it results in "1" all the cases.
Upvotes: 4
Views: 2593
Reputation: 121
Here is how you can do natively. Django knows how to handle this (see the documentation about the admin and list_display).
class LogAdmin(admin.ModelAdmin):
list_display = (..., "field1", "field2"... "quantity_of_things",)
def get_queryset(self, request):
return (super(LogAdmin, self).get_queryset(request).annotate(quantity_of_things=Count("xxxxxx")))
def quantity_of_things(self, obj):
return obj.quantity_of_things
quantity_of_things.short_description = "Number of xxxxxx"
You annotate the queryset with anything you need (a Count
for instance). As you can see, you just have to use the same name 3 times (quantity_of_things
in my example)
quantity_of_things.short_description
is for the column titleUpvotes: 7
Reputation: 1152
That is because the moment you apply values
to the queryset
, it returns a list of dictionary, whereas the admin is expecting a queryset of objects
.
I have been in this situation before, so I did something like this:
def changelist_view(self, request, extra_context=None):
response = super().changelist_view(request, extra_context)
try:
# this is the final queryset to be rendered on the page after pagination.
_cl = response.context_data['cl']
qs = _cl.result_list._clone()
# since in my case mysql 5.5 does'nt support subquery with LIMIT
# fetch all the ips on that page
ips = list(set([obj.ip_address for obj in qs]))
result_qs = models.Log.objects.values('ip_address') \
.filter(ip_address__in=ips) \
.annotate(ip_address_count=Count('ip_address'))
result = {_r['ip_address']: _r['ip_address_count'] for _r in result_qs}
setattr(self, '_ip_addr_count', result)
except:
pass
return response
def ip_address_count(self, instance):
# fetch the count from the dict we set above
return self._ip_addr_count.get(instance.ip_address)
Hence you see what I did is, fetch the ips from the final queryset and then using that to query the count. By doing this you will query the database only once per page.
I hope you get the basic underlying idea.Please suit it according to your need.
Upvotes: 1