Skip to content

Instantly share code, notes, and snippets.

@tim-schilling
Last active November 11, 2022 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tim-schilling/a58c0f57ac4c6fa7a188513d78c607d7 to your computer and use it in GitHub Desktop.
Save tim-schilling/a58c0f57ac4c6fa7a188513d78c607d7 to your computer and use it in GitHub Desktop.
Django admin annotate the page's objects rather than the queryset
# There will come a time when you want to show more information on the admin
# list view, but performing the annotation on the entire queryset kills
# performance. These classes will allow you to apply a QuerySet annotation
# to only those objects that are rendered on the current page.
# Constraints:
# - This adds one additional query to your request.
# - The annotated columns can't be used in ordering.
# - You need to define functions to access the annotated fields on the object.
from django.contrib import admin
from django.core.paginator import Paginator, Page
from django.db.models import Count, Exists, OuterRef
from myapp.models import MyModel
class AnnotatedPaginator(Paginator):
"""
Apply a dictionary of annotations to the page for the paginator.
"""
def __init__(self, *args, annotations=None, **kwargs):
self.annotations = annotations
super().__init__(*args, **kwargs)
def _get_page(self, object_list, *args, **kwargs):
"""
Return an instance of a single page.
This will change the object_list into an actual list.
It will make an additional query to the database to look up
the values to be manually set on the object.
"""
objects = list(object_list)
if objects and self.annotations:
# Make another query for this model type to gather the annotated fields.
annotated_queryset = (
objects[0]
._meta.model.objects.filter(id__in=[obj.id for obj in objects])
.annotate(**self.annotations)
)
# Create a map to associate the original objects to the annotated values.
annotated_maps = {
annotated_map.pop("id"): annotated_map
for annotated_map in annotated_queryset.values(
"id", *self.annotations.keys()
)
}
# Associated the annotated values to the original objects.
for obj in objects:
for key, value in annotated_maps[obj.id].items():
setattr(obj, key, value)
return Page(objects, *args, **kwargs)
class AdminAnnotatedPageMixin:
"""
Extend the ModelAdmin functionality to utilize AnnotatedPaginator
"""
paginator = AnnotatedPaginator
page_annotations = None
def get_paginator(
self, request, queryset, per_page, orphans=0, allow_empty_first_page=True
):
return self.paginator(
queryset,
per_page,
orphans,
allow_empty_first_page,
annotations=self.page_annotations,
)
@admin.site.register(MyModel)
class MyModelAdmin(AdminAnnotatedPageMixin, admin.ModelAdmin):
page_annotations = {
"related_field_count": Count("related_field"),
"special_case_exists": Exists(
OtherModel.objects.filter(relation_id=OuterRef("id"))[:1]
),
}
@admin.display(description="Related Count")
def related_field_count(self, obj):
return obj.related_field_count
@admin.display(description="Is Special", boolean=True)
def special_case_exists(self, obj):
return obj.special_case_exists
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment