From d29275bcb70f5ead4950102cee2ce33520d36baa Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 20 Nov 2024 14:23:50 -0500 Subject: [PATCH] glamr/views: speed up checks if related data export links should be active * new ExportMixin.get_export_remote_field() will get the relation's remote field, then get_export queryset() and get_export_link() (re-named from get_export_hfref_data()) pick that up and also get the related model from it. * get_export_link() uses a specialized exist query that's very fast The previous implementation was quite slow in the relevant cases. --- mibios/glamr/views.py | 62 +++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/mibios/glamr/views.py b/mibios/glamr/views.py index 483740e1..f7e2e90d 100644 --- a/mibios/glamr/views.py +++ b/mibios/glamr/views.py @@ -12,7 +12,7 @@ from django.contrib import messages from django.core.exceptions import FieldDoesNotExist from django.db import OperationalError, connection -from django.db.models import Count, Field, Prefetch, URLField +from django.db.models import Count, Exists, Field, OuterRef, Prefetch, URLField from django.http import Http404, HttpResponse from django.urls import reverse from django.utils.functional import cached_property, classproperty @@ -193,22 +193,34 @@ def get_export_queryset(self, export_option): """ if export_option is self.EXPORT_TABLE: return self.get_queryset() + else: + # Try for related data export (or get 404 if this fails), other + # more intricate options would need to be implemented by inheriting + # views. + remote_field = self.get_export_remote_field(export_option) + f = {remote_field.name + '__in': self.get_queryset()} + return remote_field.model.objects.filter(**f) + + def get_export_remote_field(self, export_option): + """ + Get remote field of the to-be-exported reverse to-many relation. + + export_option: + Expected to be the field name of a to-many relation. - # Try for related data export, other more intricate options would need - # to be implemented by inheriting views. + Raises 404 for illegal export options, since those may come in via the + GET query string. + """ try: - # try exporting related data - field = \ - self.model._meta.get_field(export_option) + field = self.model._meta.get_field(export_option) except FieldDoesNotExist: raise Http404(f'export option not implemented: {export_option}') - else: - if (relmodel := field.related_model) is None: - # not a relation - raise Http404(f'invalid export option: {export_option}') - f = {field.remote_field.name + '__in': self.get_queryset()} - return relmodel.objects.filter(**f) + if field.related_model is None: + # not a relation + raise Http404(f'invalid export option: {export_option}') + + return field.remote_field def get_export_table(self): """ @@ -239,7 +251,7 @@ def get_values(self): """ yield from self.get_export_table().as_values() - def get_export_href_data(self, option): + def get_export_link(self, option): """ Get URL and href text for a given export option @@ -255,16 +267,20 @@ def get_export_href_data(self, option): link_txt += ' (this table)' is_active = True else: - try: - field = self.model._meta.get_field(option) - except FieldDoesNotExist: - raise ValueError(f'unsupported export option value: {option}') - if not field.one_to_many or field.many_to_many: - raise ValueError(f'field {field} is not *-to-many') - link_txt = field.related_name \ - or field.related_model._meta.verbose_name_plural + remote_field = self.get_export_remote_field(option) + link_txt = remote_field.remote_field.related_name \ + or remote_field.model._meta.verbose_name_plural - is_active = self.get_export_queryset(option).exists() + # Check if any export data would exist + rel_data = remote_field.model.objects.filter( + **{remote_field.name: OuterRef('pk')} + ) + is_active = ( + self.get_queryset() + .annotate(has_rel_data=Exists(rel_data)) + .filter(has_rel_data=True) + .exists() + ) if not settings.INTERNAL_DEPLOYMENT: # currently impractical, use too much resources @@ -283,7 +299,7 @@ def get_export_href_data(self, option): def get_context_data(self, **ctx): ctx = super().get_context_data(**ctx) ctx['export_options'] = [ - self.get_export_href_data(i) + self.get_export_link(i) for i in self.export_options ] return ctx