From 3ec5044a980f05acfdfd4371aecce9347414af74 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Mon, 22 Apr 2024 13:56:06 +0200 Subject: [PATCH 1/2] Filter version based on blender_version query argument Get the latest extension version compatible with a specified blender_version. Prevent N+1 queries in the api endpoint --- extensions/views/api.py | 73 ++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/extensions/views/api.py b/extensions/views/api.py index 89d2fc93..9ea768c3 100644 --- a/extensions/views/api.py +++ b/extensions/views/api.py @@ -42,38 +42,51 @@ class ListedExtensionsSerializer(serializers.ModelSerializer): self.fail('invalid_version') def to_representation(self, instance): - blender_version_min = instance.latest_version.blender_version_min - blender_version_max = instance.latest_version.blender_version_max + matching_version = None + # avoid triggering additional db queries, reuse the prefetched queryset + versions = [ + v + for v in instance.versions.all() + if v.file and v.file.status in instance.valid_file_statuses + ] + if not versions: + return None + versions = sorted(versions, key=lambda v: v.date_created, reverse=True) + if self.blender_version: + for v in versions: + if is_in_version_range( + self.blender_version, + v.blender_version_min, + v.blender_version_max, + ): + matching_version = v + break + else: + # same as latest_version, but without triggering a new queryset + matching_version = versions[0] - # TODO: get the latest valid version - # For now we skip the extension if the latest version is not in a valid range. - if self.blender_version and not is_in_version_range( - self.blender_version, blender_version_min, blender_version_max - ): - return {} + if not matching_version: + return None data = { 'id': instance.extension_id, - 'schema_version': instance.latest_version.schema_version, + 'schema_version': matching_version.schema_version, 'name': instance.name, - 'version': instance.latest_version.version, - 'tagline': instance.latest_version.tagline, - 'archive_hash': instance.latest_version.file.original_hash, - 'archive_size': instance.latest_version.file.size_bytes, - 'archive_url': self.request.build_absolute_uri(instance.latest_version.download_url), + 'version': matching_version.version, + 'tagline': matching_version.tagline, + 'archive_hash': matching_version.file.original_hash, + 'archive_size': matching_version.file.size_bytes, + 'archive_url': self.request.build_absolute_uri(matching_version.download_url), 'type': EXTENSION_TYPE_SLUGS_SINGULAR.get(instance.type), - 'blender_version_min': instance.latest_version.blender_version_min, - 'blender_version_max': instance.latest_version.blender_version_max, + 'blender_version_min': matching_version.blender_version_min, + 'blender_version_max': matching_version.blender_version_max, 'website': self.request.build_absolute_uri(instance.get_absolute_url()), - 'maintainer': str(instance.authors.first()), - 'license': [ - license_iter.slug for license_iter in instance.latest_version.licenses.all() - ], - 'permissions': [ - permission.slug for permission in instance.latest_version.permissions.all() - ], + # avoid triggering additional db queries, reuse the prefetched queryset + 'maintainer': str(instance.authors.all()[0]), + 'license': [license_iter.slug for license_iter in matching_version.licenses.all()], + 'permissions': [permission.slug for permission in matching_version.permissions.all()], # TODO: handle copyright - 'tags': [str(tag) for tag in instance.latest_version.tags.all()], + 'tags': [str(tag) for tag in matching_version.tags.all()], } return clean_json_dictionary_from_optional_fields(data) @@ -93,10 +106,18 @@ class ExtensionsAPIView(APIView): ) def get(self, request): blender_version = request.GET.get('blender_version') + qs = Extension.objects.listed.prefetch_related( + 'authors', + 'versions', + 'versions__file', + 'versions__licenses', + 'versions__permissions', + 'versions__tags', + ).all() serializer = self.serializer_class( - Extension.objects.listed, blender_version=blender_version, request=request, many=True + qs, blender_version=blender_version, request=request, many=True ) - data = serializer.data + data = [e for e in serializer.data if e is not None] return Response( { # TODO implement extension blocking by moderators -- 2.30.2 From 75213d0b3ea7f2ff948e9bde6fe7ebf6552842cf Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Mon, 22 Apr 2024 15:06:12 +0200 Subject: [PATCH 2/2] tests --- extensions/tests/test_views.py | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/extensions/tests/test_views.py b/extensions/tests/test_views.py index 91cc1866..a93ae542 100644 --- a/extensions/tests/test_views.py +++ b/extensions/tests/test_views.py @@ -1,3 +1,5 @@ +from datetime import timedelta + from django.test import TestCase from django.urls import reverse @@ -80,6 +82,64 @@ class PublicViewsTest(_BaseTestCase): self.assertTemplateUsed(response, 'extensions/home.html') +class ApiViewsTest(_BaseTestCase): + def test_blender_version_filter(self): + create_approved_version(blender_version_min='4.0.1') + create_approved_version(blender_version_min='4.1.1') + create_approved_version(blender_version_min='4.2.1') + url = reverse('extensions:api') + + json = self.client.get( + url + '?blender_version=4.1.1', + HTTP_ACCEPT='application/json', + ).json() + self.assertEqual(len(json['data']), 2) + + json2 = self.client.get( + url + '?blender_version=3.0.1', + HTTP_ACCEPT='application/json', + ).json() + self.assertEqual(len(json2['data']), 0) + + json3 = self.client.get( + url + '?blender_version=4.3.1', + HTTP_ACCEPT='application/json', + ).json() + self.assertEqual(len(json3['data']), 3) + + def test_blender_version_filter_latest_not_max_version(self): + version = create_approved_version(blender_version_min='4.0.1') + version.date_created + extension = version.extension + create_approved_version( + blender_version_min='4.2.1', + extension=extension, + date_created=version.date_created + timedelta(days=1), + version='2.0.0', + ) + create_approved_version( + blender_version_min='3.0.0', + extension=extension, + date_created=version.date_created + timedelta(days=2), + version='1.0.1', + ) + create_approved_version( + blender_version_min='4.2.1', + extension=extension, + date_created=version.date_created + timedelta(days=3), + version='2.0.1', + ) + url = reverse('extensions:api') + + json = self.client.get( + url + '?blender_version=4.1.1', + HTTP_ACCEPT='application/json', + ).json() + self.assertEqual(len(json['data']), 1) + # we are expecting the latest matching, not the maximum version + self.assertEqual(json['data'][0]['version'], '1.0.1') + + class ExtensionDetailViewTest(_BaseTestCase): def test_cannot_view_unlisted_extension_anonymously(self): extension = _create_extension() -- 2.30.2