Refactor Extension and Version: explicit constructors from File #191

Merged
Oleg-Komarov merged 6 commits from refactor-models into main 2024-06-20 14:40:41 +02:00
25 changed files with 317 additions and 507 deletions

View File

@ -2,18 +2,16 @@ import logging
import random
from django.core.management.base import BaseCommand
import faker
from django.db import transaction
from common.tests.factories.extensions import create_approved_version, create_version
from common.tests.factories.files import FileFactory
from common.tests.factories.files import FileFactory, ImageFactory
from common.tests.factories.teams import TeamFactory
from files.models import File
from constants.version_permissions import VERSION_PERMISSION_FILE, VERSION_PERMISSION_NETWORK
from constants.licenses import LICENSE_GPL2, LICENSE_GPL3
from extensions.models import Extension, Tag
_faker = faker.Faker()
FILE_SOURCES = {
"blender-kitsu": {
"file": 'files/ed/ed656b177b01999e6fcd0e37c34ced471ef88c89db578f337e40958553dca5d2.zip',
@ -64,6 +62,7 @@ LICENSES = (LICENSE_GPL2.id, LICENSE_GPL3.id)
class Command(BaseCommand):
help = 'Generate fake data with extensions, users and versions using test factories.'
@transaction.atomic
def handle(self, *args, **options):
verbosity = int(options['verbosity'])
root_logger = logging.getLogger('root')
@ -81,70 +80,54 @@ class Command(BaseCommand):
# Create a fixed example
example_version = create_approved_version(
file__status=File.STATUSES.APPROVED,
# extension__status=Extension.STATUSES.APPROVED,
extension__name='Blender Kitsu',
extension__extension_id='blender_kitsu',
tags=['Development'],
extension__description=EXAMPLE_DESCRIPTION,
extension__support='https://developer.blender.org/',
extension__website='https://studio.blender.org/',
blender_version_min='2.93.0',
version='0.1.5-alpha+f52258de',
file=FileFactory(
type=File.TYPES.BPY,
source=FILE_SOURCES["blender-kitsu"]["file"],
metadata__blender_version_min='2.93.0',
metadata__id='blender_kitsu',
metadata__name='Blender Kitsu',
metadata__support='https://developer.blender.org/',
metadata__tags=['Development'],
metadata__version='0.1.5-alpha+f52258de',
metadata__website='https://studio.blender.org/',
original_hash=FILE_SOURCES["blender-kitsu"]["hash"],
size_bytes=FILE_SOURCES["blender-kitsu"]["size"],
source=FILE_SOURCES["blender-kitsu"]["file"],
status=File.STATUSES.APPROVED,
metadata={'name': 'Blender Kitsu'},
),
extension__previews=[
FileFactory(
type=File.TYPES.IMAGE,
source=source,
status=File.STATUSES.APPROVED,
)
for source in PREVIEW_SOURCES[-2:]
],
)
for preview in [
ImageFactory(source=source, status=File.STATUSES.APPROVED)
for source in PREVIEW_SOURCES[-2:]
]:
example_version.extension.previews.add(preview)
example_version.permissions.add(VERSION_PERMISSION_FILE.id)
example_version.permissions.add(VERSION_PERMISSION_NETWORK.id)
example_version.licenses.add(LICENSE_GPL2.id)
# Create a few publicly listed extensions
for i in range(10):
extension__type = random.choice(Extension.TYPES)[0]
name = _faker.catch_phrase()
type = random.choice(Extension.TYPES)[0]
version = create_approved_version(
file__status=File.STATUSES.APPROVED,
file__metadata={'name': name},
# extension__status=Extension.STATUSES.APPROVED,
extension__name=name,
extension__type=extension__type,
tags=random.sample(tags[extension__type], k=1),
extension__previews=[
FileFactory(
type=File.TYPES.IMAGE,
source=source,
status=File.STATUSES.APPROVED,
)
for source in random.sample(
PREVIEW_SOURCES, k=random.randint(1, len(PREVIEW_SOURCES) - 1)
)
],
metadata__tags=random.sample(tags[type], k=1),
status=File.STATUSES.APPROVED,
type=type,
)
for preview in [
ImageFactory(source=source, status=File.STATUSES.APPROVED)
for source in random.sample(
PREVIEW_SOURCES, k=random.randint(1, len(PREVIEW_SOURCES) - 1)
)
]:
version.extension.previews.add(preview)
for i in range(random.randint(1, len(LICENSES))):
version.licenses.add(LICENSES[i])
# Create a few unlisted extension versions
for i in range(5):
extension__type = random.choice(Extension.TYPES)[0]
type = random.choice(Extension.TYPES)[0]
version = create_version(
file__status=random.choice(
(File.STATUSES.DISABLED, File.STATUSES.DISABLED_BY_AUTHOR)
),
tags=random.sample(tags[extension__type], k=1),
metadata__tags=random.sample(tags[type], k=1),
status=random.choice((File.STATUSES.DISABLED, File.STATUSES.DISABLED_BY_AUTHOR)),
type=type,
)
for i in range(random.randint(1, len(LICENSES))):
version.licenses.add(LICENSES[i])

View File

@ -7,11 +7,11 @@ from django.db.models import signals
import factory
from common.tests.factories.extensions import create_version, RatingFactory
from common.tests.factories.files import FileFactory
from common.tests.factories.files import ImageFactory
from files.models import File
from constants.licenses import LICENSE_GPL2, LICENSE_GPL3
from extensions.models import Extension, Tag
from utils import slugify, chunked
from utils import chunked
import faker
_faker = faker.Faker()
@ -109,31 +109,25 @@ class Command(BaseCommand):
for _ in i_chunked:
name = _faker.catch_phrase() + _faker.bothify('###???')
extension_id = name.replace(' ', '_')
slug = slugify(extension_id)[:50]
version = create_version(
file__status=file_status,
file__metadata={'name': name, 'id': extension_id},
tags=random.sample(tags[_type], k=1),
extension__extension_id=extension_id,
extension__is_listed=extension_status == e_sts.APPROVED,
extension__name=name,
extension__slug=slug,
extension__status=extension_status,
extension__type=_type,
extension__previews=[
FileFactory(
type=File.TYPES.IMAGE,
source=source,
status=file_status,
)
for source in random.sample(
PREVIEW_SOURCES,
k=random.randint(1, len(PREVIEW_SOURCES) - 1),
)
],
# Create these separately
ratings=[],
metadata__id=extension_id,
metadata__name=name,
metadata__tags=random.sample(tags[_type], k=1),
status=file_status,
type=_type,
)
version.extension.is_listed = extension_status == e_sts.APPROVED
version.extension.status = extension_status
version.extension.save(update_fields={'is_listed', 'status'})
for preview in [
ImageFactory(source=source, status=file_status)
for source in random.sample(
PREVIEW_SOURCES,
k=random.randint(1, len(PREVIEW_SOURCES) - 1),
)
]:
version.extension.previews.add(preview)
for i in range(random.randint(1, len(LICENSES))):
version.licenses.add(LICENSES[i])
if version.is_listed:

View File

@ -6,7 +6,8 @@ from mdgen import MarkdownPostProvider
import factory
import factory.fuzzy
from extensions.models import Extension, Version, Tag, Preview, Platform
from common.tests.factories.files import FileFactory
from extensions.models import Extension, Version, Preview
from ratings.models import Rating
fake_markdown = Faker()
@ -56,73 +57,17 @@ class RatingFactory(DjangoModelFactory):
extension = factory.LazyAttribute(lambda o: o.version.extension)
class VersionFactory(DjangoModelFactory):
class Meta:
model = Version
extension = factory.SubFactory(ExtensionFactory)
version = factory.LazyAttribute(
lambda _: f'{random.randint(0, 9)}.{random.randint(0, 9)}.{random.randint(0, 9)}'
)
blender_version_min = factory.fuzzy.FuzzyChoice(
{'2.83.1', '2.93.0', '2.93.8', '3.0.0', '3.2.1'}
)
download_count = factory.Faker('random_int')
tagline = factory.Faker('bs')
file = factory.SubFactory(
'common.tests.factories.files.FileFactory',
metadata=factory.Dict(
{
'name': factory.Faker('name'),
'support': factory.Faker('url'),
'website': factory.Faker('url'),
}
),
)
ratings = factory.RelatedFactoryList(
RatingFactory, size=lambda: random.randint(0, 5), factory_related_name='version'
)
@factory.post_generation
def files(self, create, extracted, **kwargs):
if not create:
return
if not extracted:
self.files.add(self.file)
return
for file in extracted:
self.files.add(file)
@factory.post_generation
def platforms(self, create, extracted, **kwargs):
if not create:
return
if not extracted:
return
platforms = Platform.objects.filter(slug__in=extracted)
self.platforms.add(*platforms)
@factory.post_generation
def tags(self, create, extracted, **kwargs):
if not create:
return
if not extracted:
return
tags = Tag.objects.filter(name__in=extracted)
self.tags.add(*tags)
def create_version(**kwargs) -> 'Version':
version = VersionFactory(**kwargs)
version.extension.authors.add(version.file.user)
return version
extension = kwargs.pop('extension', None)
file = kwargs.pop('file', None)
if not file:
file = FileFactory(**kwargs)
if not extension:
extension = Extension.create_from_file(file)
return extension.create_version_from_file(file)
def create_approved_version(**kwargs) -> 'Version':

View File

@ -1,3 +1,5 @@
import random
from factory.django import DjangoModelFactory
import factory
import factory.fuzzy
@ -5,23 +7,48 @@ import factory.fuzzy
from files.models import File
class ManifestFactory(factory.DictFactory):
name = factory.Faker('name')
id = factory.Faker('slug')
support = factory.Faker('url')
website = factory.Faker('url')
version = factory.LazyAttribute(
lambda _: f'{random.randint(0, 9)}.{random.randint(0, 9)}.{random.randint(0, 9)}',
)
blender_version_min = factory.fuzzy.FuzzyChoice(
{'2.83.1', '2.93.0', '2.93.8', '3.0.0', '3.2.1'}
)
tagline = factory.Faker('bs')
schema_version = '1.0.0'
platforms = []
tags = []
class FileFactory(DjangoModelFactory):
class Meta:
model = File
original_name = factory.LazyAttribute(lambda x: x.source)
original_hash = factory.Faker('lexify', text='fakehash:??????????????????', letters='deadbeef')
hash = factory.Faker('lexify', text='fakehash:??????????????????', letters='deadbeef')
metadata = factory.SubFactory(ManifestFactory)
original_hash = factory.Faker('lexify', text='fakehash:??????????????????', letters='deadbeef')
original_name = factory.LazyAttribute(lambda x: x.source)
size_bytes = factory.Faker('random_int', min=1234)
source = factory.Faker('file_name', extension='zip')
type = File.TYPES.BPY
user = factory.SubFactory('common.tests.factories.users.UserFactory')
metadata = factory.Dict({})
class ImageFactory(FileFactory):
metadata = {}
original_name = factory.Faker('file_name', extension='png')
size_bytes = 1234
source = 'images/de/deadbeef.png'
type = File.TYPES.IMAGE
size_bytes = 1234
class VideoFactory(FileFactory):
metadata = {}
original_name = factory.Faker('file_name', extension='mp4')
size_bytes = 12345678
source = 'images/be/beefcafe.mp4'
type = File.TYPES.VIDEO

View File

@ -58,6 +58,7 @@ def construct_fake_notifications() -> list['NotificationFactory']:
),
Verb.DISMISSED_ABUSE_REPORT: None,
Verb.RATED_EXTENSION: RatingFactory.build(
extension=fake_extension,
text=fake.paragraph(nb_sentences=2),
),
Verb.REPORTED_EXTENSION: None, # TODO: fake action_object

View File

@ -84,6 +84,7 @@ class ExtensionAdmin(admin.ModelAdmin):
'icon',
'featured_image',
'latest_version',
'is_listed',
)
autocomplete_fields = ('team',)
@ -106,6 +107,7 @@ class ExtensionAdmin(admin.ModelAdmin):
('icon', 'featured_image'),
'status',
'latest_version',
'is_listed',
),
},
),

View File

@ -185,6 +185,55 @@ class Extension(CreatedModifiedMixin, TrackChangesMixin, models.Model):
]
ordering = ['-average_score', '-date_created', 'name']
@classmethod
def create_from_file(cls, file: File) -> 'Extension':
extension = cls(**file.parsed_extension_fields)
with transaction.atomic():
extension.save()
extension.authors.add(file.user)
return extension
def create_version_from_file(self, file: File, release_notes='') -> 'Version':
"""This should be the only method to create a new Version object.
Side effects:
- updates file status and adds an approval queue comment when extension is listed
- updates extension.latest_version
"""
fields = file.parsed_version_fields
licenses = fields.pop('licenses', [])
permissions = fields.pop('permissions', {})
if permissions:
permissions = list(permissions.keys())
platforms = fields.pop('platforms', [])
tags = fields.pop('tags', [])
version = Version(**fields, extension=self, release_notes=release_notes, file=file)
with transaction.atomic():
version.save()
version.files.add(file)
version.set_initial_licenses(licenses)
version.set_initial_permissions(permissions)
version.set_initial_platforms(platforms)
version.set_initial_tags(tags)
# auto approving our file if extension is already listed (i.e. have been approved)
if self.is_listed:
args = {'f_id': file.pk, 'pk': version.pk, 's': file.source.name}
log.info('Auto-approving file pk=%(f_id)s of Version pk=%(pk)s source=%(s)s', args)
file.status = File.STATUSES.APPROVED
file.save(update_fields={'status', 'date_modified'})
ApprovalActivity(
type=ApprovalActivity.ActivityType.UPLOADED_NEW_VERSION,
user=file.user,
extension=self,
message=f'uploaded new version: {version}',
).save()
self.update_latest_version()
return version
def __str__(self):
return f'{self.get_type_display()} "{self.name}"'
@ -486,28 +535,6 @@ class VersionManager(models.Manager):
def unlisted(self):
return self.exclude(file__status=FILE_STATUS_CHOICES.APPROVED)
def update_or_create(self, *args, **kwargs):
# Stash the ManyToMany to be created after the Version has a valid ID already
licenses = kwargs.pop('licenses', [])
permissions = kwargs.pop('permissions', {})
# FIXME: ideally this dict->list transformation should be a caller's responsibility
# but we plan to change the model soon, so getting a dict as an input here makes sense
if permissions:
permissions = list(permissions.keys())
platforms = kwargs.pop('platforms', [])
tags = kwargs.pop('tags', [])
file = kwargs.get('file') # FIXME legacy_version: replace get with pop
version, result = super().update_or_create(*args, **kwargs)
# Add the ManyToMany to the already initialized Version
version.files.add(file)
version.set_initial_licenses(licenses)
version.set_initial_permissions(permissions)
version.set_initial_platforms(platforms)
version.set_initial_tags(tags)
return version, result
class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
track_changes_to_fields = {
@ -725,32 +752,12 @@ class Version(CreatedModifiedMixin, TrackChangesMixin, models.Model):
@transaction.atomic
def save(self, *args, **kwargs):
is_new = self.pk is None
update_fields = kwargs.get('update_fields', None)
was_changed, old_state = self.pre_save_record(update_fields=update_fields)
self.record_status_change(was_changed, old_state, **kwargs)
super().save(*args, **kwargs)
if not is_new:
return
# auto approving our file if extension is already listed (i.e. have been approved)
if self.extension.is_listed:
args = {'f_id': self.file.pk, 'pk': self.pk, 's': self.file.source.name}
log.info('Auto-approving file pk=%(f_id)s of Version pk=%(pk)s source=%(s)s', args)
self.file.status = File.STATUSES.APPROVED
self.file.save(update_fields={'status', 'date_modified'})
ApprovalActivity(
type=ApprovalActivity.ActivityType.UPLOADED_NEW_VERSION,
user=self.file.user,
extension=self.extension,
message=f'uploaded new version: {self.version}',
).save()
self.extension.update_latest_version()
@transaction.atomic
def delete(self, *args, **kwargs):
if self == self.extension.latest_version:

View File

@ -79,7 +79,7 @@ class ResponseFormatTest(APITestCase):
self.assertEqual(v['archive_url'][-4:], '.zip')
def test_maintaner_is_team(self):
version = create_approved_version(blender_version_min='4.0.1')
version = create_approved_version(metadata__blender_version_min='4.0.1')
team = Team(name='test team', slug='test-team')
team.save()
version.extension.team = team
@ -107,9 +107,9 @@ class ResponseFormatTest(APITestCase):
class FiltersTest(APITestCase):
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')
create_approved_version(metadata__blender_version_min='4.0.1')
create_approved_version(metadata__blender_version_min='4.1.1')
create_approved_version(metadata__blender_version_min='4.2.1')
url = reverse('extensions:api')
json = self.client.get(
@ -131,8 +131,8 @@ class FiltersTest(APITestCase):
self.assertEqual(len(json3['data']), 3)
def test_platform_filter(self):
create_approved_version(platforms=['windows-x64'])
create_approved_version(platforms=['windows-arm64'])
create_approved_version(metadata__platforms=['windows-x64'])
create_approved_version(metadata__platforms=['windows-arm64'])
create_approved_version()
url = reverse('extensions:api')
@ -151,27 +151,30 @@ class FiltersTest(APITestCase):
self.assertEqual(len(json['data']), 1)
def test_blender_version_filter_latest_not_max_version(self):
version = create_approved_version(blender_version_min='4.0.1')
version.date_created
version = create_approved_version(metadata__blender_version_min='4.0.1')
date_created = version.date_created
extension = version.extension
create_approved_version(
blender_version_min='4.2.1',
version = create_approved_version(
extension=extension,
date_created=version.date_created + timedelta(days=1),
version='2.0.0',
metadata__blender_version_min='4.2.1',
metadata__version='2.0.0',
)
version.date_created = date_created + timedelta(days=1)
version.save(update_fields={'date_created'})
create_approved_version(
blender_version_min='3.0.0',
extension=extension,
date_created=version.date_created + timedelta(days=2),
version='1.0.1',
metadata__blender_version_min='3.0.0',
metadata__version='1.0.1',
)
version.date_created = date_created + timedelta(days=2)
version.save(update_fields={'date_created'})
create_approved_version(
blender_version_min='4.2.1',
extension=extension,
date_created=version.date_created + timedelta(days=3),
version='2.0.1',
metadata__blender_version_min='4.2.1',
metadata__version='2.0.1',
)
version.date_created = date_created + timedelta(days=3)
version.save(update_fields={'date_created'})
url = reverse('extensions:api')
json = self.client.get(
@ -190,9 +193,9 @@ class VersionUploadAPITest(APITestCase):
self.client = APIClient()
self.version = create_approved_version(
extension__extension_id="amaranth",
version="1.0.7",
file__user=self.user,
metadata__id="amaranth",
metadata__version="1.0.7",
user=self.user,
)
self.extension = self.version.extension
self.file_path = TEST_FILES_DIR / "amaranth-1.0.8.zip"
@ -218,7 +221,7 @@ class VersionUploadAPITest(APITestCase):
def test_version_upload_extension_not_maintained_by_user(self):
other_user = UserFactory()
other_extension = create_approved_version(
extension__extension_id='other_extension', file__user=other_user
metadata__id='other_extension', user=other_user
).extension
with open(self.file_path, 'rb') as version_file:

View File

@ -1,5 +1,4 @@
from pathlib import Path
import factory
import json
from django.contrib.admin.models import LogEntry, DELETION
@ -27,25 +26,23 @@ class DeleteTest(TestCase):
original_name='extension_feature_image.png',
source='images/b0/b03fa981527593fbe15b28cf37c020220c3d83021999eab036b87f3bca9c9168.png',
)
version = create_version(
file__status=files.models.File.STATUSES.AWAITING_REVIEW,
ratings=[],
extension__icon=FileFactory(
type=files.models.File.TYPES.IMAGE,
original_name='extension_icon_final.png',
source='images/8a/8a01102de8573d50bbc90033f55f232b7cacc4f1eb3e3c3d851615841d2956e1.png',
),
extension__featured_image=reused_image,
extension__previews=[
reused_image,
FileFactory(
type=files.models.File.TYPES.IMAGE,
original_name='extension_preview_001.png',
source='images/b0/b03fa981527593fbe15b28cf37c020220c3d83021999eab036b87f3bca9c9168.png',
),
],
)
version = create_version(status=files.models.File.STATUSES.AWAITING_REVIEW)
extension = version.extension
extension.featured_image = reused_image
extension.icon = FileFactory(
type=files.models.File.TYPES.IMAGE,
original_name='extension_icon_final.png',
source='images/8a/8a01102de8573d50bbc90033f55f232b7cacc4f1eb3e3c3d851615841d2956e1.png',
)
extension.save(update_fields={'featured_image', 'icon'})
extension.previews.add(reused_image)
extension.previews.add(
FileFactory(
type=files.models.File.TYPES.IMAGE,
original_name='extension_preview_001.png',
source='images/b0/b03fa981527593fbe15b28cf37c020220c3d83021999eab036b87f3bca9c9168.png',
)
)
version_file = version.file
icon = extension.icon
featured_image = extension.featured_image
@ -152,7 +149,7 @@ class DeleteTest(TestCase):
# TODO: check that files were deleted from storage (create a temp one prior to the check)
def test_publicly_listed_extension_cannot_be_deleted(self):
version = create_approved_version(ratings=[])
version = create_approved_version()
self.assertTrue(version.is_listed)
extension = version.extension
self.assertTrue(extension.is_listed)
@ -169,12 +166,8 @@ class DeleteTest(TestCase):
self.assertEqual(response.status_code, 403)
def test_rated_extension_cannot_be_deleted(self):
version = create_version(
file__status=files.models.File.STATUSES.AWAITING_REVIEW,
ratings=factory.RelatedFactoryList(
RatingFactory, size=1, factory_related_name='version'
),
)
version = create_version(status=files.models.File.STATUSES.AWAITING_REVIEW)
RatingFactory(version=version)
self.assertFalse(version.is_listed)
extension = version.extension
self.assertFalse(extension.is_listed)

View File

@ -1,10 +1,8 @@
from django.test import TestCase
from django.urls import reverse_lazy
from django.core.exceptions import ValidationError
import factory
from common.tests.factories.extensions import create_approved_version
from common.tests.factories.files import FileFactory
from common.tests.factories.users import UserFactory
from constants.licenses import LICENSE_GPL3
@ -56,20 +54,10 @@ class CreateFileTest(TestCase):
def _create_valid_extension(self, extension_id):
return create_approved_version(
extension__name='Blender Kitsu',
extension__extension_id=extension_id,
version='0.1.5-alpha+f52258de',
file=FileFactory(
type=File.TYPES.BPY,
status=File.STATUSES.APPROVED,
metadata=factory.Dict(
{
'name': 'Blender Kitsu',
'support': factory.Faker('url'),
'website': factory.Faker('url'),
}
),
),
metadata__id=extension_id,
metadata__name='Blender Kitsu',
metadata__version='0.1.5-alpha+f52258de',
status=File.STATUSES.APPROVED,
)
def _create_file_from_data(self, filename, file_data, user):
@ -258,20 +246,10 @@ class ValidateManifestTest(CreateFileTest):
# Create another unrelated extension with the same target version we will use.
create_approved_version(
extension__name='Another Extension',
extension__extension_id='another_extension',
version='0.1.6',
file=FileFactory(
type=File.TYPES.BPY,
status=File.STATUSES.APPROVED,
metadata=factory.Dict(
{
'name': factory.Faker('name'),
'support': factory.Faker('url'),
'website': factory.Faker('url'),
}
),
),
metadata__id='another_extension',
metadata__name='Another Extension',
metadata__version='0.1.6',
status=File.STATUSES.APPROVED,
)
# The same author is to send a new version to thte same extension
@ -713,9 +691,6 @@ class VersionPermissionsTest(CreateFileTest):
)
self.assertEqual(response.status_code, 302)
# Although this creates the file, the version is not created until
# we click on Submit for Review.
# Check step 2: finalise new version and send to review
url = response['Location']
response = self.client.post(

View File

@ -6,7 +6,6 @@ from common.admin import get_admin_change_path
from common.log_entries import entries_for
from common.tests.factories.extensions import create_version
from common.tests.factories.users import UserFactory
from extensions.models import Extension
class ExtensionTest(TestCase):
@ -16,17 +15,9 @@ class ExtensionTest(TestCase):
def setUp(self):
super().setUp()
self.extension = create_version(
file__size_bytes=123,
extension__description='Extension description',
extension__website='https://example.com/',
extension__name='Extension name',
extension__status=Extension.STATUSES.DRAFT,
extension__support='https://example.com/',
file__metadata={
'name': 'Extension name',
'support': 'https://example.com/',
'website': 'https://example.com/',
},
metadata__name='Extension name',
metadata__support='https://example.com/',
metadata__website='https://example.com/',
).extension
self.assertEqual(entries_for(self.extension).count(), 0)
self.assertIsNone(self.extension.date_approved)
@ -47,7 +38,7 @@ class ExtensionTest(TestCase):
'new_state': {'status': 'Approved'},
'object': '<Extension: Add-on "Extension name">',
'old_state': {
'description': 'Extension description',
'description': '',
'website': 'https://example.com/',
'name': 'Extension name',
'status': 1,
@ -57,13 +48,6 @@ class ExtensionTest(TestCase):
},
)
def test_status_change_updates_date_creates_log_entry(self):
self.extension.approve()
self.assertIsNotNone(self.extension.date_approved)
self.assertIsNotNone(self.extension.date_status_changed)
self._check_change_message()
def test_status_change_updates_date_creates_log_entry_with_update_fields(self):
self.extension.approve()
@ -89,42 +73,16 @@ class VersionTest(TestCase):
def setUp(self):
super().setUp()
self.version = create_version(
blender_version_min='2.83.1',
version='1.1.2',
extension__description='Extension description',
extension__website='https://example.com/',
extension__name='Extension name',
extension__status=Extension.STATUSES.DRAFT,
extension__support='https://example.com/',
metadata__blender_version_min='2.83.1',
metadata__name='Extension name',
metadata__support='https://example.com/',
metadata__version='1.1.2',
metadata__website='https://example.com/',
)
self.assertEqual(entries_for(self.version).count(), 0)
# FIXME remove when dropping Version.file field
self.assertEqual(self.version.file, self.version.files.first())
def _check_change_message(self):
entries = entries_for(self.version)
self.assertEqual(entries.count(), 1)
log_entry = entries.first()
change_message = json.loads(log_entry.change_message)
self.assertEqual(len(change_message), 1)
self.assertDictEqual(
change_message[0],
{
'changed': {
'fields': ['status'],
'name': 'version',
'new_state': {'status': 'Approved'},
'object': '<Version: Add-on "Extension name" v1.1.2>',
'old_state': {
'blender_version_max': None,
'blender_version_min': '4.2.0',
'status': 1,
'version': '1.1.2',
},
}
},
)
def test_admin_change_view(self):
path = get_admin_change_path(obj=self.version)
self.assertEqual(path, '/admin/extensions/version/1/change/')
@ -142,27 +100,18 @@ class UpdateMetadataTest(TestCase):
def setUp(self):
super().setUp()
self.first_version = create_version(
extension__description='Extension description',
extension__name='name',
extension__status=Extension.STATUSES.DRAFT,
extension__support='https://example.com/',
extension__website='https://example.com/',
file__metadata={
'name': 'name',
'support': 'https://example.com/',
'website': 'https://example.com/',
},
metadata__name='name',
metadata__support='https://example.com/',
metadata__website='https://example.com/',
)
self.extension = self.first_version.extension
def test_version_create_and_delete(self):
second_version = create_version(
extension=self.extension,
file__metadata={
'name': 'new name',
'support': 'https://example.com/new',
'website': 'https://example.com/new',
},
metadata__name='new name',
metadata__support='https://example.com/new',
metadata__website='https://example.com/new',
)
self.extension.refresh_from_db()
self.assertEqual(self.extension.name, 'new name')
@ -178,26 +127,16 @@ class UpdateMetadataTest(TestCase):
def test_old_name_taken(self):
second_version = create_version(
extension=self.extension,
file__metadata={
'name': 'new name',
'support': 'https://example.com/new',
'website': 'https://example.com/new',
},
metadata__name='new name',
metadata__support='https://example.com/new',
metadata__website='https://example.com/new',
)
# another extension uses old name
create_version(
extension__description='Extension description',
extension__extension_id='lalalala',
extension__name='name',
extension__status=Extension.STATUSES.DRAFT,
extension__support='https://example.com/',
extension__website='https://example.com/',
file__metadata={
'name': 'name',
'support': 'https://example.com/',
'website': 'https://example.com/',
},
metadata__name='name',
metadata__support='https://example.com/',
metadata__website='https://example.com/',
)
second_version.delete()

View File

@ -294,16 +294,7 @@ class SubmitFinaliseTest(CheckFilePropertiesMixin, TestCase):
hash=file_data['file_hash'],
metadata=file_data['metadata'],
)
self.version = create_version(
file=self.file,
extension__name=file_data['metadata']['name'],
extension__slug=file_data['metadata']['id'].replace("_", "-"),
extension__website=None,
tagline=file_data['metadata']['tagline'],
version=file_data['metadata']['version'],
blender_version_min=file_data['metadata']['blender_version_min'],
schema_version=file_data['metadata']['schema_version'],
)
self.version = create_version(file=self.file)
def test_get_finalise_addon_redirects_if_anonymous(self):
response = self.client.post(self.file.legacy_version.extension.get_draft_url(), {})
@ -492,7 +483,7 @@ class NewVersionTest(TestCase):
fixtures = ['licenses']
def setUp(self):
self.version = create_version(extension__extension_id='amaranth')
self.version = create_version(metadata__id='amaranth')
self.extension = self.version.extension
self.url = self.extension.get_new_version_url()
@ -541,6 +532,7 @@ class NewVersionTest(TestCase):
def test_upload_new_file_and_finalise_new_version(self):
self.client.force_login(self.version.file.user)
self.extension.approve()
# Check step 1: upload a new file
with open(TEST_FILES_DIR / 'amaranth-1.0.8.zip', 'rb') as fp:
@ -552,17 +544,22 @@ class NewVersionTest(TestCase):
response['Location'],
f'/add-ons/{self.extension.slug}/manage/versions/new/{file.pk}/',
)
self.assertEqual(self.extension.versions.count(), 1)
self.extension.approve()
# now a file upload creates a corresponding version object immediately
self.assertEqual(self.extension.versions.count(), 2)
new_version = self.extension.versions.order_by('date_created').last()
self.assertEqual(new_version.version, '1.0.8')
self.assertEqual(new_version.blender_version_min, '4.2.0')
self.assertEqual(new_version.schema_version, '1.0.0')
self.assertEqual(new_version.file.get_status_display(), 'Approved')
self.assertEqual(new_version.release_notes, '')
self.assertEqual(
ApprovalActivity.objects.filter(
extension=self.extension,
type=ApprovalActivity.ActivityType.UPLOADED_NEW_VERSION,
).count(),
0,
1,
)
# Check step 2: finalise new version and send to review
url = response['Location']
response = self.client.post(
url,
@ -572,28 +569,17 @@ class NewVersionTest(TestCase):
)
self.assertEqual(response.status_code, 302)
self.assertEqual(self.extension.versions.count(), 2)
self.assertEqual(response['Location'], f'/add-ons/{self.extension.slug}/manage/versions/')
new_version = self.extension.versions.order_by('date_created').last()
self.assertEqual(new_version.version, '1.0.8')
self.assertEqual(new_version.blender_version_min, '4.2.0')
self.assertEqual(new_version.schema_version, '1.0.0')
self.assertEqual(self.extension.versions.count(), 2)
new_version.refresh_from_db()
self.assertEqual(new_version.release_notes, 'new version')
self.assertEqual(new_version.file.get_status_display(), 'Approved')
self.assertEqual(
ApprovalActivity.objects.filter(
extension=self.extension,
type=ApprovalActivity.ActivityType.UPLOADED_NEW_VERSION,
).count(),
1,
)
class DraftsWarningTest(TestCase):
fixtures = ['licenses']
def test_page_contains_warning(self):
version = create_version(extension__extension_id='draft_warning')
version = create_version()
extension = version.extension
self.assertEqual(extension.status, Extension.STATUSES.DRAFT)
self.client.force_login(extension.authors.all()[0])

View File

@ -293,7 +293,8 @@ class UpdateTest(CheckFilePropertiesMixin, TestCase):
source='file/original_image_source.jpg',
)
extension = create_approved_version().extension
another_extension = create_approved_version(extension__previews=[file]).extension
another_extension = create_approved_version().extension
another_extension.previews.add(file)
images_count_before = File.objects.filter(type=File.TYPES.IMAGE).count()
self.assertEqual(extension.previews.count(), 0)
self.assertEqual(another_extension.previews.count(), 1)
@ -440,11 +441,9 @@ class UpdateTest(CheckFilePropertiesMixin, TestCase):
)
def test_update_icon_changes_expected_file_fields(self):
extension = create_approved_version(
extension__icon=ImageFactory(
original_name='old_icon.png',
),
).extension
extension = create_approved_version().extension
extension.icon = ImageFactory(original_name='old_icon.png')
extension.save(update_fields={'icon'})
self._test_file_properties(
extension.icon,
content_type='image/png',
@ -516,7 +515,9 @@ class UpdateTest(CheckFilePropertiesMixin, TestCase):
source='test_icon_0001.png',
)
extension = create_approved_version().extension
another_extension = create_approved_version(extension__icon=file).extension
another_extension = create_approved_version().extension
another_extension.icon = file
another_extension.save(update_fields={'icon'})
images_count_before = File.objects.filter(type=File.TYPES.IMAGE).count()
self.assertIsNone(extension.icon)
self.assertEqual(another_extension.icon_id, file.pk)
@ -542,11 +543,9 @@ class UpdateTest(CheckFilePropertiesMixin, TestCase):
self.assertEqual(file.user, old_user)
def test_update_featured_image_changes_expected_file_fields(self):
extension = create_approved_version(
extension__featured_image=ImageFactory(
original_name='old_featured_image.png',
),
).extension
extension = create_approved_version().extension
extension.featured_image = ImageFactory(original_name='old_featured_image.png')
extension.save(update_fields={'featured_image'})
self._test_file_properties(
extension.featured_image,
content_type='image/png',
@ -582,8 +581,10 @@ class UpdateTest(CheckFilePropertiesMixin, TestCase):
)
def test_convert_to_draft(self):
version = create_version(extension__status=Extension.STATUSES.AWAITING_REVIEW)
version = create_version()
extension = version.extension
extension.status = Extension.STATUSES.AWAITING_REVIEW
extension.save(update_fields={'status'})
url = extension.get_manage_url()
user = extension.authors.first()
self.client.force_login(user)
@ -607,9 +608,8 @@ class UpdateTest(CheckFilePropertiesMixin, TestCase):
self.assertEqual(response3['Location'], extension.get_draft_url())
def test_team_field_in_draft_form(self):
version = create_version(
extension__status=Extension.STATUSES.DRAFT,
)
# default status is DRAFT
version = create_version()
extension = version.extension
author = extension.authors.first()
self.client.force_login(author)
@ -670,10 +670,10 @@ class UpdateTest(CheckFilePropertiesMixin, TestCase):
def test_team_field_in_update_form(self):
"""This test is a copy-paste of the one above, only status, url and form data differ."""
version = create_version(
extension__status=Extension.STATUSES.APPROVED,
)
version = create_version()
extension = version.extension
extension.status = Extension.STATUSES.APPROVED
extension.save(update_fields={'status'})
author = extension.authors.first()
self.client.force_login(author)

View File

@ -4,26 +4,20 @@ from django.urls import reverse
from common.tests.factories.extensions import create_version, create_approved_version
from common.tests.factories.teams import TeamFactory
from common.tests.factories.users import UserFactory, create_moderator
from extensions.models import Extension
from teams.models import TeamsUsers
def _create_extension():
return create_version(
version='1.3.4',
blender_version_min='4.2.0',
extension__name='Test Add-on',
extension__description='**Description in bold**',
extension__support='https://example.com/issues/',
extension__website='https://example.com/',
extension__status=Extension.STATUSES.DRAFT,
extension__average_score=2.5,
file__metadata={
'name': 'Test Add-on',
'support': 'https://example.com/issues/',
'website': 'https://example.com/',
},
extension = create_version(
metadata__blender_version_min='4.2.0',
metadata__name='Test Add-on',
metadata__support='https://example.com/issues/',
metadata__version='1.3.4',
metadata__website='https://example.com/',
).extension
extension.description = '**Description in bold**'
extension.save(update_fields={'description'})
return extension
class _BaseTestCase(TestCase):
@ -59,7 +53,8 @@ class PublicViewsTest(_BaseTestCase):
self.assertTemplateUsed(response, 'extensions/home.html')
def test_no_one_can_view_extension_page_when_not_listed_404(self):
extension = create_version(extension__is_listed=False).extension
# not listed by default
extension = create_version().extension
moderator = create_moderator()
staff = UserFactory(is_staff=True)

View File

@ -10,7 +10,7 @@ from django.core.exceptions import ValidationError
from django.db import transaction
from common.compare import is_in_version_range, version
from extensions.models import Extension, Platform, Version
from extensions.models import Extension, Platform
from extensions.utils import clean_json_dictionary_from_optional_fields
from extensions.views.manage import NewVersionView
from files.forms import FileFormSkipAgreed
@ -217,13 +217,10 @@ class UploadExtensionVersionView(APIView):
file_instance.user = user
file_instance.save()
# Create the version from the file
version = Version.objects.update_or_create(
extension=extension,
version = extension.create_version_from_file(
file=file_instance,
release_notes=release_notes,
**file_instance.parsed_version_fields,
)[0]
)
return Response(
{

View File

@ -219,6 +219,12 @@ class NewVersionView(
kwargs['extension'] = self.extension
return kwargs
@transaction.atomic
def form_valid(self, form):
response = super().form_valid(form)
self.extension.create_version_from_file(self.object)
return response
def get_success_url(self):
return reverse(
'extensions:new-version-finalise',
@ -236,17 +242,11 @@ class NewVersionFinalizeView(LoginRequiredMixin, OwnsFileMixin, CreateView):
template_name = 'extensions/new_version_finalise.html'
form_class = VersionForm
def _get_extension(self) -> 'Extension':
return get_object_or_404(Extension, slug=self.kwargs['slug'])
def _get_version(self, extension) -> 'Version':
return Version.objects.update_or_create(
extension=extension, file=self.file, **self.file.parsed_version_fields
)[0]
def get_form_kwargs(self):
form_kwargs = super().get_form_kwargs()
form_kwargs['instance'] = self._get_version(self.extension)
# this lookup via VersionFiles ManyToManyManager returns the version that was created on
# the previous step by create_version_from_file
form_kwargs['instance'] = self.file.version.first()
return form_kwargs
def get_initial(self):
@ -257,7 +257,7 @@ class NewVersionFinalizeView(LoginRequiredMixin, OwnsFileMixin, CreateView):
return initial
def get_success_url(self):
return self.extension.get_manage_versions_url()
return self.object.extension.get_manage_versions_url()
class UpdateVersionView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):

View File

@ -8,7 +8,6 @@ from files.models import File
class OwnsFileMixin(UserPassesTestMixin):
def dispatch(self, *args, **kwargs):
self.file = get_object_or_404(File, pk=self.kwargs['pk'])
self.extension = self._get_extension()
return super().dispatch(*args, **kwargs)
def test_func(self) -> bool:

View File

@ -4,7 +4,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction
from django.views.generic.edit import CreateView
from extensions.models import Version, Extension
from extensions.models import Extension
from files.forms import FileForm
from files.models import File
@ -35,37 +35,7 @@ class UploadFileView(LoginRequiredMixin, CreateView):
@transaction.atomic
def form_valid(self, form):
"""Create an extension and a version already, associated with the user."""
self.file = form.instance
parsed_extension_fields = self.file.parsed_extension_fields
if parsed_extension_fields:
# Try to look up extension by the same author and file info
extension = (
Extension.objects.authored_by(self.request.user)
.filter(type=self.file.type, **parsed_extension_fields)
.first()
)
if extension:
logger.warning(
'Found existing extension pk=%s for file pk=%s',
extension.pk,
self.file.pk,
)
return False
# Make sure an extension has a user associated to it from the beginning, otherwise
# it will prevent it from being re-uploaded and yet not show on My Extensions.
self.extension = Extension.objects.update_or_create(
type=self.file.type, **parsed_extension_fields
)[0]
self.extension.authors.add(self.request.user)
self.extension.save()
# Need to save the form to be able to use the file to create the version.
self.object = self.file = form.save()
Version.objects.update_or_create(
extension=self.extension, file=self.file, **self.file.parsed_version_fields
)[0]
file = form.save()
self.extension = Extension.create_from_file(file)
self.extension.create_version_from_file(file)
return super().form_valid(form)

View File

@ -169,9 +169,11 @@ class File(CreatedModifiedMixin, TrackChangesMixin, models.Model):
extension_id = data.get('id')
name = data.get('name', self.original_name)
return {
'extension_id': extension_id,
'name': name,
'slug': utils.slugify(extension_id),
'extension_id': extension_id,
'support': data.get('support'),
'type': self.type,
'website': data.get('website'),
}

View File

@ -18,10 +18,11 @@ class FileTest(TestCase):
def setUp(self):
super().setUp()
self.file = FileFactory(
status=File.STATUSES.AWAITING_REVIEW,
original_name='test.zip',
hash='foobar',
metadata={},
original_name='test.zip',
size_bytes=7149,
status=File.STATUSES.AWAITING_REVIEW,
)
self.assertEqual(entries_for(self.file).count(), 0)
self.assertIsNone(self.file.date_approved)

View File

@ -4,7 +4,7 @@ import logging
from django.test import TestCase, override_settings
from common.tests.factories.files import FileFactory, ImageFactory
from common.tests.factories.files import FileFactory, ImageFactory, VideoFactory
from files.tasks import make_thumbnails
import files.models
@ -96,9 +96,7 @@ class TasksTest(TestCase):
@patch('files.utils.Image')
@patch('files.utils.FFmpeg')
def test_make_thumbnails_for_video(self, mock_ffmpeg, mock_image, mock_resize_image):
file = FileFactory(
hash='deadbeef', source='file/path.mp4', type=files.models.File.TYPES.VIDEO
)
file = VideoFactory(hash='deadbeef')
files.models.FileValidation.objects.create(file=file, is_ok=True, results={})
self.assertIsNone(file.thumbnail.name)
self.assertEqual(file.metadata, {})

View File

@ -9,7 +9,7 @@ class RatingsViewTest(TestCase):
fixtures = ['dev', 'licenses']
def test_get_anonymous(self):
version = create_approved_version(ratings=[])
version = create_approved_version()
[
RatingFactory(
version=version, text='this rating is rejected', status=Rating.STATUSES.REJECTED
@ -40,7 +40,7 @@ class RatingsViewTest(TestCase):
self.assertNotContains(response, 'this rating is deleted', html=True)
def test_get_logged_in_can_see_own_unlisted_rating(self):
version = create_approved_version(ratings=[])
version = create_approved_version()
user = UserFactory()
[
RatingFactory(
@ -73,7 +73,7 @@ class AddRatingViewTest(TestCase):
fixtures = ['dev', 'licenses']
def test_get_anonymous_redirects_to_login(self):
version = create_approved_version(ratings=[])
version = create_approved_version()
url = version.extension.get_rate_url()
response = self.client.get(url)
@ -82,7 +82,7 @@ class AddRatingViewTest(TestCase):
self.assertTrue(response['Location'].startswith('/oauth/login'))
def test_get_logged_in_as_maintainer_cant_rate(self):
version = create_approved_version(ratings=[])
version = create_approved_version()
url = version.extension.get_rate_url()
self.client.force_login(version.extension.authors.first())
@ -93,7 +93,7 @@ class AddRatingViewTest(TestCase):
def test_get_logged_in_can_rate(self):
user = UserFactory()
version = create_approved_version(ratings=[])
version = create_approved_version()
url = version.extension.get_rate_url()
self.client.force_login(user)
@ -104,7 +104,7 @@ class AddRatingViewTest(TestCase):
def test_post_logged_in_validation_errors(self):
user = UserFactory()
version = create_approved_version(ratings=[])
version = create_approved_version()
url = version.extension.get_rate_url()
self.client.force_login(user)
@ -143,7 +143,7 @@ class AddRatingViewTest(TestCase):
def test_post_logged_in_adds_new_rating(self):
user = UserFactory()
version = create_approved_version(ratings=[])
version = create_approved_version()
extension = version.extension
self.assertEqual(Rating.objects.count(), 0)
self.assertEqual(extension.ratings.count(), 0)
@ -165,7 +165,7 @@ class AddRatingViewTest(TestCase):
self.assertEqual(rating.text, text)
def test_reply(self):
version = create_approved_version(ratings=[])
version = create_approved_version()
rating = RatingFactory(version=version, text='some text', status=Rating.STATUSES.APPROVED)
random_user = UserFactory()

View File

@ -10,7 +10,7 @@ class CommentsViewTest(TestCase):
fixtures = ['licenses']
def setUp(self):
version = create_version(file__status=File.STATUSES.AWAITING_REVIEW)
version = create_version(status=File.STATUSES.AWAITING_REVIEW)
self.default_version = version
ApprovalActivity(
type=ApprovalActivity.ActivityType.COMMENT,

View File

@ -6,17 +6,13 @@ from django.test import TestCase
from common.tests.factories.extensions import create_approved_version
from stats.models import ExtensionView, ExtensionDownload, ExtensionCountedStat
# TODO: tests for VersionFactory Version download_count
class WriteStatsCommandTest(TestCase):
fixtures = ['dev', 'licenses']
def test_command_updates_extensions_view_counters(self):
out = StringIO()
extension = create_approved_version(
extension__view_count=0, extension__download_count=0
).extension
extension = create_approved_version().extension
ExtensionView.objects.bulk_create(
[
ExtensionView(extension_id=extension.pk, ip_address='192.19.10.10'),
@ -44,9 +40,7 @@ class WriteStatsCommandTest(TestCase):
def test_command_updates_extensions_download_counters(self):
out = StringIO()
extension = create_approved_version(
extension__view_count=0, extension__download_count=0
).extension
extension = create_approved_version().extension
ExtensionDownload.objects.bulk_create(
[
ExtensionDownload(extension_id=extension.pk, ip_address='192.19.10.10'),
@ -74,9 +68,9 @@ class WriteStatsCommandTest(TestCase):
def test_command_adds_extensions_view_counters(self):
out = StringIO()
extension = create_approved_version(
extension__view_count=10, extension__download_count=0
).extension
extension = create_approved_version().extension
extension.view_count = 10
extension.save(update_fields={'view_count'})
ExtensionView.objects.bulk_create(
[
ExtensionView(extension_id=extension.pk, ip_address='192.19.10.10'),
@ -104,9 +98,9 @@ class WriteStatsCommandTest(TestCase):
def test_command_adds_extensions_download_counters(self):
out = StringIO()
extension = create_approved_version(
extension__view_count=0, extension__download_count=10
).extension
extension = create_approved_version().extension
extension.download_count = 10
extension.save(update_fields={'download_count'})
ExtensionDownload.objects.bulk_create(
[
ExtensionDownload(extension_id=extension.pk, ip_address='192.19.10.10'),
@ -134,9 +128,10 @@ class WriteStatsCommandTest(TestCase):
def test_command_updates_extensions_both_download_and_view_counters(self):
out = StringIO()
extension = create_approved_version(
extension__view_count=4, extension__download_count=5
).extension
extension = create_approved_version().extension
extension.download_count = 5
extension.view_count = 4
extension.save(update_fields={'download_count', 'view_count'})
ExtensionView.objects.bulk_create(
[
ExtensionView(extension_id=extension.pk, ip_address='192.19.10.10'),
@ -174,10 +169,10 @@ class WriteStatsCommandTest(TestCase):
def test_command_updates_extensions_both_download_and_view_counters_uses_last_seen_id(self):
out = StringIO()
initial_view_count, initial_download_count = 4, 5
extension = create_approved_version(
extension__download_count=initial_download_count,
extension__view_count=initial_view_count,
).extension
extension = create_approved_version().extension
extension.download_count = initial_download_count
extension.view_count = initial_view_count
extension.save(update_fields={'download_count', 'view_count'})
download_count_starts_here = ExtensionDownload(
extension_id=extension.pk, ip_address='192.19.10.15'
)

View File

@ -33,14 +33,12 @@ class TestTasks(TestCase):
# Abuse reported by someone else ABOUT this account:
self.report3 = AbuseReportFactory(user=user)
self.report4 = AbuseReportFactory(user=user)
self.authored_unlisted_extension = create_version(
file__user=user, extension__is_listed=False
).extension
self.authored_unlisted_extension = create_version(user=user).extension
self.assertFalse(self.authored_unlisted_extension.is_listed)
def create_account_data_that_cannot_be_deleted(self, user):
"""Create objects which prevent account deletion but allow anonymisation."""
self.authored_listed_extension = create_approved_version(file__user=user).extension
self.authored_listed_extension = create_approved_version(user=user).extension
self.assertTrue(self.authored_listed_extension.is_listed)
def test_handle_deletion_request_anonymized(self):