Last active
November 11, 2022 16:38
-
-
Save tim-schilling/a58c0f57ac4c6fa7a188513d78c607d7 to your computer and use it in GitHub Desktop.
Django admin annotate the page's objects rather than the queryset
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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