UI: Improve multi OS display #205
@ -19,10 +19,10 @@ class Migration(migrations.Migration):
|
|||||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||||
('date_modified', models.DateTimeField(auto_now=True)),
|
('date_modified', models.DateTimeField(auto_now=True)),
|
||||||
('date_deleted', models.DateTimeField(blank=True, editable=False, null=True)),
|
('date_deleted', models.DateTimeField(blank=True, editable=False, null=True)),
|
||||||
('extension_version', extensions.fields.VersionStringField(blank=True, coerce=False, max_length=64, null=True, partial=False)),
|
('extension_version', extensions.fields.VersionStringField(blank=True, max_length=64, null=True)),
|
||||||
('message', models.TextField(blank=True)),
|
('message', models.TextField(blank=True)),
|
||||||
('reason', models.PositiveSmallIntegerField(choices=[(127, 'Other'), (1, 'Damages computer and/or data'), (2, 'Creates spam or advertising'), (3, "Doesn't work, breaks Blender, or slows it down"), (4, 'Hateful, violent, or illegal content'), (5, "Pretends to be something it's not")], default='Other')),
|
('reason', models.PositiveSmallIntegerField(choices=[(127, 'Other'), (1, 'Damages computer and/or data'), (2, 'Creates spam or advertising'), (3, "Doesn't work, breaks Blender, or slows it down"), (4, 'Hateful, violent, or illegal content'), (5, "Pretends to be something it's not")], default='Other')),
|
||||||
('version', extensions.fields.VersionStringField(blank=True, coerce=False, help_text='Version of Blender affected by this report, if applicable.', max_length=64, null=True, partial=False)),
|
('version', extensions.fields.VersionStringField(blank=True, help_text='Version of Blender affected by this report, if applicable.', max_length=64, null=True)),
|
||||||
('status', models.PositiveSmallIntegerField(choices=[(1, 'Untriaged'), (2, 'Valid'), (3, 'Suspicious')], default=1)),
|
('status', models.PositiveSmallIntegerField(choices=[(1, 'Untriaged'), (2, 'Valid'), (3, 'Suspicious')], default=1)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
|
@ -267,11 +267,6 @@ BLENDER_ID = {
|
|||||||
|
|
||||||
TAGGIT_CASE_INSENSITIVE = True
|
TAGGIT_CASE_INSENSITIVE = True
|
||||||
|
|
||||||
ACTSTREAM_SETTINGS = {
|
|
||||||
'MANAGER': 'users.managers.CustomStreamManager',
|
|
||||||
'FETCH_RELATIONS': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': ('apitokens.authentication.UserTokenAuthentication',),
|
'DEFAULT_AUTHENTICATION_CLASSES': ('apitokens.authentication.UserTokenAuthentication',),
|
||||||
|
@ -28,7 +28,7 @@ class FileFactory(DjangoModelFactory):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = File
|
model = File
|
||||||
|
|
||||||
hash = factory.Faker('lexify', text='fakehash:??????????????????', letters='deadbeef')
|
hash = factory.LazyAttribute(lambda x: x.original_hash)
|
||||||
metadata = factory.SubFactory(ManifestFactory)
|
metadata = factory.SubFactory(ManifestFactory)
|
||||||
original_hash = factory.Faker('lexify', text='fakehash:??????????????????', letters='deadbeef')
|
original_hash = factory.Faker('lexify', text='fakehash:??????????????????', letters='deadbeef')
|
||||||
original_name = factory.LazyAttribute(lambda x: x.source)
|
original_name = factory.LazyAttribute(lambda x: x.source)
|
||||||
|
@ -1,36 +1,16 @@
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from semantic_version.django_fields import VersionField as SemanticVersionField
|
from django.db.models import CharField
|
||||||
from semantic_version import Version
|
from semantic_version import Version
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class VersionStringField(SemanticVersionField):
|
def validate_version_string(version_string):
|
||||||
description = "A field to store serializable semantic versions"
|
|
||||||
|
|
||||||
def to_python(self, value):
|
|
||||||
if isinstance(value, Version):
|
|
||||||
return value
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
try:
|
try:
|
||||||
return str(Version(value))
|
Version.parse(version_string)
|
||||||
except Exception as e:
|
except ValueError as e:
|
||||||
raise ValidationError(e)
|
raise ValidationError(e)
|
||||||
|
|
||||||
def from_db_value(self, value, expression, connection):
|
|
||||||
return self.to_python(value)
|
|
||||||
|
|
||||||
def get_prep_value(self, value):
|
class VersionStringField(CharField):
|
||||||
if value is None:
|
description = "A field to store serializable semantic versions"
|
||||||
return value
|
|
||||||
return str(value)
|
|
||||||
|
|
||||||
def value_to_string(self, obj):
|
default_validators = [validate_version_string]
|
||||||
value = self.value_from_object(obj)
|
|
||||||
return self.get_prep_value(value)
|
|
||||||
|
|
||||||
def from_json(self, json_str):
|
|
||||||
return self.to_python(json.loads(json_str))
|
|
||||||
|
|
||||||
def to_json(self, value):
|
|
||||||
return json.dumps(str(value))
|
|
||||||
|
@ -87,9 +87,9 @@ class Migration(migrations.Migration):
|
|||||||
('date_created', models.DateTimeField(auto_now_add=True)),
|
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||||
('date_modified', models.DateTimeField(auto_now=True)),
|
('date_modified', models.DateTimeField(auto_now=True)),
|
||||||
('date_deleted', models.DateTimeField(blank=True, editable=False, null=True)),
|
('date_deleted', models.DateTimeField(blank=True, editable=False, null=True)),
|
||||||
('version', extensions.fields.VersionStringField(coerce=False, default='0.1.0', help_text='Current (latest) version of extension. The value is taken from <code>"version"</code> in the manifest.', max_length=255, partial=False)),
|
('version', extensions.fields.VersionStringField(default='0.1.0', help_text='Current (latest) version of extension. The value is taken from <code>"version"</code> in the manifest.', max_length=255)),
|
||||||
('blender_version_min', extensions.fields.VersionStringField(coerce=False, default='2.93.0', help_text='Minimum version of Blender this extension is compatible with. The value is taken from <code>"blender_version_min"</code> in the manifest file.', max_length=64, partial=False)),
|
('blender_version_min', extensions.fields.VersionStringField(default='2.93.0', help_text='Minimum version of Blender this extension is compatible with. The value is taken from <code>"blender_version_min"</code> in the manifest file.', max_length=64)),
|
||||||
('blender_version_max', extensions.fields.VersionStringField(coerce=False, help_text='Maximum version of Blender this extension was tested and is compatible with. The value is taken from <code>"blender_version_max"</code> in the manifest file.', max_length=64, null=True, partial=False)),
|
('blender_version_max', extensions.fields.VersionStringField(help_text='Maximum version of Blender this extension was tested and is compatible with. The value is taken from <code>"blender_version_max"</code> in the manifest file.', max_length=64, null=True)),
|
||||||
('release_notes', models.TextField(help_text='\n<p><a href="https://commonmark.org/help/" rel="nofollow" target="_blank">Markdown</a>\nis supported.</p>\n')),
|
('release_notes', models.TextField(help_text='\n<p><a href="https://commonmark.org/help/" rel="nofollow" target="_blank">Markdown</a>\nis supported.</p>\n')),
|
||||||
('average_score', models.FloatField(default=0, max_length=255)),
|
('average_score', models.FloatField(default=0, max_length=255)),
|
||||||
('download_count', models.PositiveIntegerField(default=0)),
|
('download_count', models.PositiveIntegerField(default=0)),
|
||||||
|
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='version',
|
model_name='version',
|
||||||
name='schema_version',
|
name='schema_version',
|
||||||
field=extensions.fields.VersionStringField(coerce=False, default='1.0.0', help_text='Specification version the manifest file is following.', max_length=64, partial=False),
|
field=extensions.fields.VersionStringField(default='1.0.0', help_text='Specification version the manifest file is following.', max_length=64),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='version',
|
model_name='version',
|
||||||
name='blender_version_max',
|
name='blender_version_max',
|
||||||
field=extensions.fields.VersionStringField(blank=True, coerce=False, help_text='Maximum version of Blender this extension was tested and is compatible with. The value is taken from <code>"blender_version_max"</code> in the manifest file.', max_length=64, null=True, partial=False),
|
field=extensions.fields.VersionStringField(blank=True, help_text='Maximum version of Blender this extension was tested and is compatible with. The value is taken from <code>"blender_version_max"</code> in the manifest file.', max_length=64, null=True),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='version',
|
model_name='version',
|
||||||
|
@ -85,10 +85,6 @@ class ExtensionManager(models.Manager):
|
|||||||
is_listed=True,
|
is_listed=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def unlisted(self):
|
|
||||||
return self.exclude(status=self.model.STATUSES.APPROVED)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def blocklisted(self):
|
def blocklisted(self):
|
||||||
return self.filter(status=self.model.STATUSES.BLOCKLISTED)
|
return self.filter(status=self.model.STATUSES.BLOCKLISTED)
|
||||||
@ -222,6 +218,8 @@ class Extension(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
tags = fields.pop('tags', [])
|
tags = fields.pop('tags', [])
|
||||||
version = Version(**fields, extension=self, release_notes=release_notes)
|
version = Version(**fields, extension=self, release_notes=release_notes)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
# make sure to validate all fields passed from manifest using Version validators
|
||||||
|
version.full_clean()
|
||||||
version.save()
|
version.save()
|
||||||
version.files.add(file)
|
version.files.add(file)
|
||||||
version.set_initial_licenses(licenses)
|
version.set_initial_licenses(licenses)
|
||||||
@ -374,18 +372,6 @@ class Extension(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
return [FILE_STATUS_CHOICES.APPROVED]
|
return [FILE_STATUS_CHOICES.APPROVED]
|
||||||
return [FILE_STATUS_CHOICES.AWAITING_REVIEW, FILE_STATUS_CHOICES.APPROVED]
|
return [FILE_STATUS_CHOICES.AWAITING_REVIEW, FILE_STATUS_CHOICES.APPROVED]
|
||||||
|
|
||||||
def can_request_review(self):
|
|
||||||
"""Return whether an add-on can request a review or not."""
|
|
||||||
if self.is_disabled or self.status in (
|
|
||||||
self.STATUSES.APPROVED,
|
|
||||||
self.STATUSES.AWAITING_REVIEW,
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
|
|
||||||
latest_version = self.latest_version
|
|
||||||
|
|
||||||
return latest_version is not None and not latest_version.file.reviewed
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_approved(self) -> bool:
|
def is_approved(self) -> bool:
|
||||||
return self.status == self.STATUSES.APPROVED
|
return self.status == self.STATUSES.APPROVED
|
||||||
@ -398,13 +384,6 @@ class Extension(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.status in (self.STATUSES.DISABLED_BY_AUTHOR, self.STATUSES.DISABLED)
|
return self.status in (self.STATUSES.DISABLED_BY_AUTHOR, self.STATUSES.DISABLED)
|
||||||
|
|
||||||
def should_redirect_to_submit_flow(self):
|
|
||||||
return (
|
|
||||||
self.status == self.STATUSES.DRAFT
|
|
||||||
and not self.has_complete_metadata()
|
|
||||||
and self.latest_version is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
def has_maintainer(self, user) -> bool:
|
def has_maintainer(self, user) -> bool:
|
||||||
"""Return True if given user is listed as a maintainer or is a member of the team."""
|
"""Return True if given user is listed as a maintainer or is a member of the team."""
|
||||||
if user is None or user.is_anonymous:
|
if user is None or user.is_anonymous:
|
||||||
@ -438,7 +417,7 @@ class Extension(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
files = []
|
files = []
|
||||||
for version in self.versions.all():
|
for version in self.versions.all():
|
||||||
for file in version.files.all():
|
for file in version.files.all():
|
||||||
if not file.validation.is_ok:
|
if hasattr(file, 'validation') and not file.validation.is_ok:
|
||||||
files.append(file)
|
files.append(file)
|
||||||
return files
|
return files
|
||||||
|
|
||||||
@ -549,11 +528,7 @@ class Tag(CreatedModifiedMixin, models.Model):
|
|||||||
class VersionManager(models.Manager):
|
class VersionManager(models.Manager):
|
||||||
@property
|
@property
|
||||||
def listed(self):
|
def listed(self):
|
||||||
return self.filter(files__status=FILE_STATUS_CHOICES.APPROVED)
|
return self.filter(files__status=FILE_STATUS_CHOICES.APPROVED).distinct()
|
||||||
|
|
||||||
@property
|
|
||||||
def unlisted(self):
|
|
||||||
return self.exclude(files__status=FILE_STATUS_CHOICES.APPROVED)
|
|
||||||
|
|
||||||
|
|
||||||
class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||||
@ -637,9 +612,6 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def set_initial_permissions(self, _permissions):
|
def set_initial_permissions(self, _permissions):
|
||||||
if not _permissions:
|
if not _permissions:
|
||||||
return
|
return
|
||||||
@ -785,7 +757,7 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
permissions.append({'slug': slug, 'reason': reason, 'name': all_permission_names[slug]})
|
permissions.append({'slug': slug, 'reason': reason, 'name': all_permission_names[slug]})
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
def _get_download_name(self, file) -> str:
|
def get_download_name(self, file) -> str:
|
||||||
"""Return a file name for downloads."""
|
"""Return a file name for downloads."""
|
||||||
parts = [self.extension.type_slug_singular, self.extension.slug, f'v{self.version}']
|
parts = [self.extension.type_slug_singular, self.extension.slug, f'v{self.version}']
|
||||||
if platforms := file.get_platforms():
|
if platforms := file.get_platforms():
|
||||||
@ -793,7 +765,7 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
return f'{"-".join(parts)}.zip'
|
return f'{"-".join(parts)}.zip'
|
||||||
|
|
||||||
def get_download_url(self, file, append_repository_and_compatibility=True) -> str:
|
def get_download_url(self, file, append_repository_and_compatibility=True) -> str:
|
||||||
filename = self._get_download_name(file)
|
filename = self.get_download_name(file)
|
||||||
download_url = reverse(
|
download_url = reverse(
|
||||||
'extensions:download',
|
'extensions:download',
|
||||||
kwargs={
|
kwargs={
|
||||||
@ -820,7 +792,7 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
file = files[0]
|
file = files[0]
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'name': self._get_download_name(file),
|
'name': self.get_download_name(file),
|
||||||
'size': file.size_bytes,
|
'size': file.size_bytes,
|
||||||
'url': self.get_download_url(file),
|
'url': self.get_download_url(file),
|
||||||
}
|
}
|
||||||
@ -838,7 +810,7 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
all_platforms_by_slug = {p.slug: p for p in Platform.objects.all()}
|
all_platforms_by_slug = {p.slug: p for p in Platform.objects.all()}
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'name': self._get_download_name(file),
|
'name': self.get_download_name(file),
|
||||||
'platform': all_platforms_by_slug.get(platform_slug),
|
'platform': all_platforms_by_slug.get(platform_slug),
|
||||||
'size': file.size_bytes,
|
'size': file.size_bytes,
|
||||||
'url': self.get_download_url(file),
|
'url': self.get_download_url(file),
|
||||||
@ -852,7 +824,7 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
platforms = file.get_platforms() or []
|
platforms = file.get_platforms() or []
|
||||||
build_list.append(
|
build_list.append(
|
||||||
{
|
{
|
||||||
'name': self._get_download_name(file),
|
'name': self.get_download_name(file),
|
||||||
'platforms': platforms,
|
'platforms': platforms,
|
||||||
'size': file.size_bytes,
|
'size': file.size_bytes,
|
||||||
'url': self.get_download_url(file),
|
'url': self.get_download_url(file),
|
||||||
|
@ -64,21 +64,28 @@ class ResponseFormatTest(APITestCase):
|
|||||||
json = response.json()
|
json = response.json()
|
||||||
self.assertEqual(len(json['data']), 3)
|
self.assertEqual(len(json['data']), 3)
|
||||||
for v in json['data']:
|
for v in json['data']:
|
||||||
self.assertIn('id', v)
|
extension = Extension.objects.get(extension_id=v['id'])
|
||||||
|
file = extension.versions.first().files.first()
|
||||||
self.assertIn('name', v)
|
self.assertIn('name', v)
|
||||||
self.assertIn('tagline', v)
|
self.assertIn('tagline', v)
|
||||||
self.assertIn('version', v)
|
self.assertIn('version', v)
|
||||||
self.assertIn('type', v)
|
self.assertIn('type', v)
|
||||||
self.assertIn('archive_size', v)
|
self.assertIn('archive_size', v)
|
||||||
self.assertIn('archive_hash', v)
|
|
||||||
self.assertIn('archive_url', v)
|
|
||||||
self.assertIn('blender_version_min', v)
|
self.assertIn('blender_version_min', v)
|
||||||
self.assertIn('maintainer', v)
|
self.assertIn('maintainer', v)
|
||||||
self.assertIn('license', v)
|
self.assertIn('license', v)
|
||||||
self.assertIn('website', v)
|
|
||||||
self.assertIn('schema_version', v)
|
self.assertIn('schema_version', v)
|
||||||
# Blender expects urls in HTML anchors to end with .zip to handle drag&drop
|
# Blender expects urls in HTML anchors to end with .zip to handle drag&drop
|
||||||
self.assertEqual(v['archive_url'][-4:], '.zip')
|
self.assertEqual(v['archive_url'][-4:], '.zip')
|
||||||
|
self.assertEqual(
|
||||||
|
v['archive_url'],
|
||||||
|
'http://testserver'
|
||||||
|
+ extension.versions.first().get_download_url(
|
||||||
|
file, append_repository_and_compatibility=False
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(v['archive_hash'], file.hash)
|
||||||
|
self.assertEqual(v['website'], 'http://testserver' + extension.get_absolute_url())
|
||||||
|
|
||||||
def test_maintaner_is_team(self):
|
def test_maintaner_is_team(self):
|
||||||
version = create_approved_version(metadata__blender_version_min='4.0.1')
|
version = create_approved_version(metadata__blender_version_min='4.0.1')
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from common.admin import get_admin_change_path
|
from common.admin import get_admin_change_path
|
||||||
@ -161,6 +162,26 @@ class VersionTest(TestCase):
|
|||||||
self.assertIsNotNone(version2.get_file_for_platform(None))
|
self.assertIsNotNone(version2.get_file_for_platform(None))
|
||||||
self.assertIsNotNone(version2.get_file_for_platform('macos-x64'))
|
self.assertIsNotNone(version2.get_file_for_platform('macos-x64'))
|
||||||
|
|
||||||
|
def test_version_validation(self):
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
create_version(
|
||||||
|
metadata__version='abc',
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
create_version(
|
||||||
|
metadata__version='1',
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
create_version(
|
||||||
|
metadata__version='1.2',
|
||||||
|
)
|
||||||
|
|
||||||
|
create_version(
|
||||||
|
metadata__version='0.0.5+win-x64',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UpdateMetadataTest(TestCase):
|
class UpdateMetadataTest(TestCase):
|
||||||
fixtures = ['dev', 'licenses']
|
fixtures = ['dev', 'licenses']
|
||||||
|
@ -14,11 +14,6 @@ from extensions.models import Extension, Platform
|
|||||||
from extensions.utils import clean_json_dictionary_from_optional_fields
|
from extensions.utils import clean_json_dictionary_from_optional_fields
|
||||||
from files.forms import FileFormSkipAgreed
|
from files.forms import FileFormSkipAgreed
|
||||||
|
|
||||||
|
|
||||||
from constants.base import (
|
|
||||||
EXTENSION_TYPE_SLUGS_SINGULAR,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -35,9 +30,10 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|||||||
UNKNOWN_PLATFORM = 'unknown-platform-value'
|
UNKNOWN_PLATFORM = 'unknown-platform-value'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.request = kwargs.pop('request', None)
|
|
||||||
self.blender_version = kwargs.pop('blender_version', None)
|
self.blender_version = kwargs.pop('blender_version', None)
|
||||||
self.platform = kwargs.pop('platform', None)
|
self.platform = kwargs.pop('platform', None)
|
||||||
|
self.request = kwargs.pop('request', None)
|
||||||
|
self.scheme_host = "{}://{}".format(self.request.scheme, self.request.get_host())
|
||||||
self._validate()
|
self._validate()
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@ -48,7 +44,6 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|||||||
except ValidationError:
|
except ValidationError:
|
||||||
self.fail('invalid_blender_version')
|
self.fail('invalid_blender_version')
|
||||||
if self.platform:
|
if self.platform:
|
||||||
# FIXME change to an in-memory lookup?
|
|
||||||
try:
|
try:
|
||||||
Platform.objects.get(slug=self.platform)
|
Platform.objects.get(slug=self.platform)
|
||||||
except Platform.DoesNotExist:
|
except Platform.DoesNotExist:
|
||||||
@ -80,9 +75,13 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|||||||
return ([], None)
|
return ([], None)
|
||||||
|
|
||||||
def to_representation(self, instance):
|
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)
|
matching_files, matching_version = self.find_matching_files_and_version(instance)
|
||||||
|
type_slug = instance.type_slug
|
||||||
result = []
|
result = []
|
||||||
for file in matching_files:
|
for file in matching_files:
|
||||||
|
filename = matching_version.get_download_name(file)
|
||||||
data = {
|
data = {
|
||||||
'id': instance.extension_id,
|
'id': instance.extension_id,
|
||||||
'schema_version': matching_version.schema_version,
|
'schema_version': matching_version.schema_version,
|
||||||
@ -91,20 +90,12 @@ class ListedExtensionsSerializer(serializers.ModelSerializer):
|
|||||||
'tagline': matching_version.tagline,
|
'tagline': matching_version.tagline,
|
||||||
'archive_hash': file.original_hash,
|
'archive_hash': file.original_hash,
|
||||||
'archive_size': file.size_bytes,
|
'archive_size': file.size_bytes,
|
||||||
'archive_url': self.request.build_absolute_uri(
|
'archive_url': f'{self.scheme_host}/download/{file.hash}/{filename}',
|
||||||
matching_version.get_download_url(
|
'type': instance.type_slug_singular,
|
||||||
file,
|
|
||||||
append_repository_and_compatibility=False,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
'type': EXTENSION_TYPE_SLUGS_SINGULAR.get(instance.type),
|
|
||||||
'blender_version_min': matching_version.blender_version_min,
|
'blender_version_min': matching_version.blender_version_min,
|
||||||
'blender_version_max': matching_version.blender_version_max,
|
'blender_version_max': matching_version.blender_version_max,
|
||||||
'website': self.request.build_absolute_uri(instance.get_absolute_url()),
|
'website': f'{self.scheme_host}/{type_slug}/{matching_version.extension.slug}/',
|
||||||
# avoid triggering additional db queries, reuse the prefetched queryset
|
'maintainer': maintainer,
|
||||||
'maintainer': (
|
|
||||||
instance.team and instance.team.name or str(instance.authors.all()[0])
|
|
||||||
),
|
|
||||||
'license': [license_iter.slug for license_iter in matching_version.licenses.all()],
|
'license': [license_iter.slug for license_iter in matching_version.licenses.all()],
|
||||||
'permissions': file.metadata.get('permissions'),
|
'permissions': file.metadata.get('permissions'),
|
||||||
'platforms': file.get_platforms(),
|
'platforms': file.get_platforms(),
|
||||||
|
@ -19,10 +19,6 @@ class FileManager(models.Manager):
|
|||||||
def listed(self):
|
def listed(self):
|
||||||
return self.filter(status=self.model.STATUSES.APPROVED)
|
return self.filter(status=self.model.STATUSES.APPROVED)
|
||||||
|
|
||||||
@property
|
|
||||||
def unlisted(self):
|
|
||||||
return self.exclude(status=self.model.STATUSES.APPROVED)
|
|
||||||
|
|
||||||
|
|
||||||
def file_upload_to(instance, filename):
|
def file_upload_to(instance, filename):
|
||||||
prefix = 'files/'
|
prefix = 'files/'
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<section>
|
<section>
|
||||||
<div class="card pb-3 pt-4 px-4 mb-3 ext-detail-download-danger">
|
<div class="card pb-3 pt-4 px-4 mb-3 ext-detail-download-danger">
|
||||||
<h3>⚠ {% trans "Suspicious upload" %}</h3>
|
<h3>⚠ {% trans "Suspicious upload" %}</h3>
|
||||||
{% blocktrans asvar alert_text %}Scan of the {{ suspicious_files.0 }} indicates malicious content.{% endblocktrans %}
|
{% blocktrans asvar alert_text with file=suspicious_files.0 %}Scan of the {{ file }} indicates malicious content.{% endblocktrans %}
|
||||||
<h4>
|
<h4>
|
||||||
{{ alert_text }}
|
{{ alert_text }}
|
||||||
{% if perms.files.view_file %}{# Moderators don't necessarily have access to the admin #}
|
{% if perms.files.view_file %}{# Moderators don't necessarily have access to the admin #}
|
||||||
|
@ -351,6 +351,6 @@ class UtilsTest(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
validate_wheels(test_file_path, ['wheels/1.whl']).get('wheels/1.whl'),
|
validate_wheels(test_file_path, ['wheels/1.whl']).get('wheels/1.whl'),
|
||||||
'digest in archive=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
'sha256 in archive=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
||||||
', digest on pypi=blahblah',
|
', sha256 on pypi=blahblah',
|
||||||
)
|
)
|
||||||
|
@ -18,10 +18,6 @@ class RatingManager(models.Manager):
|
|||||||
def listed(self):
|
def listed(self):
|
||||||
return self.filter(status=self.model.STATUSES.APPROVED)
|
return self.filter(status=self.model.STATUSES.APPROVED)
|
||||||
|
|
||||||
@property
|
|
||||||
def unlisted(self):
|
|
||||||
return self.exclude(status=self.models.STATUSES.APPROVED)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def listed_texts(self):
|
def listed_texts(self):
|
||||||
return self.listed.filter(text__isnull=False)
|
return self.listed.filter(text__isnull=False)
|
||||||
|
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('date_released_on', models.DateField(verbose_name='Released on')),
|
('date_released_on', models.DateField(verbose_name='Released on')),
|
||||||
('date_supported_until', models.DateField(verbose_name='Supported until')),
|
('date_supported_until', models.DateField(verbose_name='Supported until')),
|
||||||
('version', extensions.fields.VersionStringField(coerce=False, max_length=64, partial=False, unique=True)),
|
('version', extensions.fields.VersionStringField(max_length=64, unique=True)),
|
||||||
('is_active', models.BooleanField(default=True, help_text='Is this release currently actively developed or supported?<br>Controls whether or not this release shows up in <code>blender_version_max</code> dropdown.')),
|
('is_active', models.BooleanField(default=True, help_text='Is this release currently actively developed or supported?<br>Controls whether or not this release shows up in <code>blender_version_max</code> dropdown.')),
|
||||||
('is_lts', models.BooleanField(default=False, help_text='Is this a Long-term Support release?', verbose_name='Is LTS')),
|
('is_lts', models.BooleanField(default=False, help_text='Is this a Long-term Support release?', verbose_name='Is LTS')),
|
||||||
('release_notes_url', models.URLField(verbose_name='Release notes URL')),
|
('release_notes_url', models.URLField(verbose_name='Release notes URL')),
|
||||||
|
Loading…
Reference in New Issue
Block a user