260 lines
9.3 KiB
Python
260 lines
9.3 KiB
Python
import logging
|
|
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import transaction
|
|
from django.http import JsonResponse
|
|
from django.utils.decorators import method_decorator
|
|
from django.views.decorators.cache import cache_page
|
|
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|
from rest_framework.permissions import AllowAny
|
|
from rest_framework.response import Response
|
|
from rest_framework import serializers, status
|
|
from rest_framework.views import APIView
|
|
from rest_framework.permissions import IsAuthenticated
|
|
|
|
from common.compare import is_in_version_range, version
|
|
from extensions.models import Extension, Platform
|
|
from extensions.utils import clean_json_dictionary_from_optional_fields
|
|
from files.forms import FileFormSkipAgreed
|
|
from utils import absolutify
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|
error_messages = {
|
|
"invalid_blender_version": "Invalid blender_version: use full semantic versioning like "
|
|
"4.2.0.",
|
|
}
|
|
|
|
class Meta:
|
|
model = Extension
|
|
fields = ()
|
|
|
|
UNKNOWN_PLATFORM = 'unknown-platform-value'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.blender_version = kwargs.pop('blender_version', None)
|
|
self.platform = kwargs.pop('platform', None)
|
|
self.request = kwargs.pop('request', None)
|
|
if self.request:
|
|
self.scheme_host = "{}://{}".format(self.request.scheme, self.request.get_host())
|
|
self._validate()
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def _validate(self):
|
|
if self.blender_version:
|
|
try:
|
|
version(self.blender_version)
|
|
except ValidationError:
|
|
self.fail('invalid_blender_version')
|
|
if self.platform:
|
|
try:
|
|
Platform.objects.get(slug=self.platform)
|
|
except Platform.DoesNotExist:
|
|
self.platform = self.UNKNOWN_PLATFORM
|
|
|
|
def find_matching_files_and_version(self, instance):
|
|
# avoid triggering additional db queries, reuse the prefetched queryset
|
|
versions = sorted(
|
|
[v for v in instance.versions.all() if v.is_listed],
|
|
key=lambda v: v.date_created,
|
|
reverse=True,
|
|
)
|
|
if not versions:
|
|
return ([], None)
|
|
|
|
for v in versions:
|
|
if self.blender_version and not is_in_version_range(
|
|
self.blender_version,
|
|
v.blender_version_min,
|
|
v.blender_version_max,
|
|
):
|
|
continue
|
|
if self.platform:
|
|
if file := v.get_file_for_platform(self.platform):
|
|
return ([file], v)
|
|
else:
|
|
return (v.files.all(), v)
|
|
|
|
return ([], None)
|
|
|
|
def to_representation(self, instance):
|
|
# avoid triggering additional db queries, reuse the prefetched authors queryset
|
|
maintainer = instance.team and instance.team.name or str(instance.authors.all()[0])
|
|
matching_files, matching_version = self.find_matching_files_and_version(instance)
|
|
type_slug = instance.type_slug
|
|
result = []
|
|
for file in matching_files:
|
|
filename = matching_version.get_download_name(file)
|
|
data = {
|
|
'id': instance.extension_id,
|
|
'schema_version': matching_version.schema_version,
|
|
'name': instance.name,
|
|
'version': matching_version.version,
|
|
'tagline': matching_version.tagline,
|
|
'archive_hash': file.original_hash,
|
|
'archive_size': file.size_bytes,
|
|
'archive_url': f'{self.scheme_host}/download/{file.hash}/{filename}',
|
|
'type': instance.type_slug_singular,
|
|
'blender_version_min': matching_version.blender_version_min,
|
|
'blender_version_max': matching_version.blender_version_max,
|
|
'website': f'{self.scheme_host}/{type_slug}/{matching_version.extension.slug}/',
|
|
'maintainer': maintainer,
|
|
'license': [license_iter.slug for license_iter in matching_version.licenses.all()],
|
|
'permissions': file.metadata.get('permissions'),
|
|
'platforms': file.get_platforms(),
|
|
# TODO: handle copyright
|
|
'tags': [str(tag) for tag in matching_version.tags.all()],
|
|
}
|
|
result.append(clean_json_dictionary_from_optional_fields(data))
|
|
return result
|
|
|
|
|
|
class ExtensionsAPIView(APIView):
|
|
"""Extension Listing API
|
|
https://developer.blender.org/docs/features/extensions/api_listing/
|
|
"""
|
|
|
|
permission_classes = [AllowAny]
|
|
serializer_class = ListedExtensionsSerializer
|
|
|
|
@extend_schema(
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="blender_version",
|
|
description=("Blender version to check for compatibility"),
|
|
type=str,
|
|
),
|
|
OpenApiParameter(
|
|
name="platform",
|
|
description=("Platform to check for compatibility"),
|
|
type=str,
|
|
),
|
|
]
|
|
)
|
|
@method_decorator(cache_page(60))
|
|
def get(self, request):
|
|
blender_version = request.GET.get('blender_version')
|
|
platform = request.GET.get('platform')
|
|
qs = Extension.objects.listed.prefetch_related(
|
|
'authors',
|
|
'team',
|
|
'versions',
|
|
'versions__files',
|
|
'versions__licenses',
|
|
'versions__permissions',
|
|
'versions__platforms',
|
|
'versions__tags',
|
|
).all()
|
|
serializer = self.serializer_class(
|
|
qs,
|
|
blender_version=blender_version,
|
|
platform=platform,
|
|
request=request,
|
|
many=True,
|
|
)
|
|
data = []
|
|
for entry in serializer.data:
|
|
data.extend(entry)
|
|
|
|
return Response(
|
|
{
|
|
'blocklist': Extension.objects.blocklisted.values_list('extension_id', flat=True),
|
|
'data': data,
|
|
'version': 'v1',
|
|
}
|
|
)
|
|
|
|
|
|
class ExtensionVersionSerializer(serializers.Serializer):
|
|
version_file = serializers.FileField()
|
|
release_notes = serializers.CharField(max_length=1024, required=False)
|
|
|
|
|
|
class UploadExtensionVersionView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
@extend_schema(
|
|
request=ExtensionVersionSerializer,
|
|
responses={201: 'Extension version uploaded successfully!'},
|
|
)
|
|
def post(self, request, extension_id, *args, **kwargs):
|
|
serializer = ExtensionVersionSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
user = request.user
|
|
version_file = serializer.validated_data['version_file']
|
|
release_notes = serializer.validated_data.get('release_notes', '')
|
|
|
|
extension = Extension.objects.filter(extension_id=extension_id).first()
|
|
if not extension:
|
|
return Response(
|
|
{
|
|
'message': f'Extension "{extension_id}" not found',
|
|
},
|
|
status=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
|
|
if not extension.has_maintainer(user):
|
|
return Response(
|
|
{
|
|
'message': f'Extension "{extension_id}" not maintained by user "{user}"',
|
|
},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
form = FileFormSkipAgreed(
|
|
data={},
|
|
extension=extension,
|
|
request=request,
|
|
)
|
|
form.fields['source'].initial = version_file
|
|
|
|
if not form.is_valid():
|
|
if 'source' in form.errors:
|
|
form.errors['version_file'] = form.errors.pop('source')
|
|
return Response({'message': form.errors}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
with transaction.atomic():
|
|
# Create the file instance
|
|
file_instance = form.save(commit=False)
|
|
file_instance.user = user
|
|
file_instance.save()
|
|
|
|
manifest_version = file_instance.metadata['version']
|
|
if version := extension.versions.filter(version=manifest_version).first():
|
|
version.add_file(file_instance)
|
|
version.release_notes = release_notes
|
|
version.save(update_fields={'release_notes'})
|
|
else:
|
|
version = extension.create_version_from_file(
|
|
file=file_instance,
|
|
release_notes=release_notes,
|
|
)
|
|
|
|
return Response(
|
|
{
|
|
'message': 'Extension version uploaded successfully!',
|
|
'extension_id': extension_id,
|
|
'version_file': version_file.name,
|
|
'release_notes': version.release_notes,
|
|
},
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
|
|
def extensions_awaiting_review(request):
|
|
response = []
|
|
for extension in Extension.objects.filter(status=Extension.STATUSES.AWAITING_REVIEW):
|
|
version = extension.latest_version
|
|
for file in version.files.all():
|
|
response.append(
|
|
{
|
|
'download_url': absolutify(version.get_download_url(file)),
|
|
'extension_id': extension.extension_id,
|
|
}
|
|
)
|
|
return JsonResponse(response, safe=False)
|