extensions-website/extensions/views/api.py
Oleg Komarov 71a32c1ad8 API: filter extension versions by blender_version parameter (#86)
As specified in #70:
Use the latest extension version compatible with a given blender_version, as opposed to the previous behavior when only the latest extension version was checked.
Now if there are any compatible versions, the extension will be listed, previously it was listed only if the latest version was compatible.

Also this PR uses an opportunity to optimize the db prefetches to avoid 6*N + 1 query problem.

Reviewed-on: #86
Reviewed-by: Anna Sirota <railla@noreply.localhost>
2024-04-22 15:24:04 +02:00

129 lines
4.4 KiB
Python

import logging
from rest_framework.response import Response
from rest_framework import serializers
from rest_framework.views import APIView
from drf_spectacular.utils import OpenApiParameter, extend_schema
from django.core.exceptions import ValidationError
from common.compare import is_in_version_range, version
from extensions.models import Extension
from extensions.utils import clean_json_dictionary_from_optional_fields
from constants.base import (
EXTENSION_TYPE_SLUGS_SINGULAR,
)
log = logging.getLogger(__name__)
class ListedExtensionsSerializer(serializers.ModelSerializer):
error_messages = {
"invalid_version": "Invalid version: use full semantic versioning like 4.2.0."
}
class Meta:
model = Extension
fields = ()
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
self.blender_version = kwargs.pop('blender_version', None)
self._validate()
super().__init__(*args, **kwargs)
def _validate(self):
if self.blender_version is None:
return
try:
version(self.blender_version)
except ValidationError:
self.fail('invalid_version')
def to_representation(self, instance):
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]
if not matching_version:
return None
data = {
'id': instance.extension_id,
'schema_version': matching_version.schema_version,
'name': instance.name,
'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': matching_version.blender_version_min,
'blender_version_max': matching_version.blender_version_max,
'website': self.request.build_absolute_uri(instance.get_absolute_url()),
# 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 matching_version.tags.all()],
}
return clean_json_dictionary_from_optional_fields(data)
class ExtensionsAPIView(APIView):
serializer_class = ListedExtensionsSerializer
@extend_schema(
parameters=[
OpenApiParameter(
name="blender_version",
description=("Blender version to check for compatibility"),
type=str,
)
]
)
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(
qs, blender_version=blender_version, request=request, many=True
)
data = [e for e in serializer.data if e is not None]
return Response(
{
# TODO implement extension blocking by moderators
'blocklist': [],
'data': data,
'version': 'v1',
}
)