phase 1 backup - support for multi-file versioned upload

This commit is contained in:
2026-08-03 22:46:22 -05:00
parent 2fdd926513
commit cff8ab9f02
14 changed files with 375 additions and 91 deletions
+97 -13
View File
@@ -20,6 +20,7 @@ from .models import (
TagList,
TempUpload,
Version,
VersionFile,
)
@@ -113,7 +114,7 @@ class UGCProjectTests(UGCMediaTestCase, UGCGatedTestCase):
self.assertEqual(project.versions.count(), 1)
self.assertIsNotNone(project.thumbnail)
self.assertTrue(project.thumbnail.stored_path.startswith('project_'))
self.assertTrue(project.versions.first().file.stored_path.startswith('project_'))
self.assertTrue(project.versions.first().files.first().file.stored_path.startswith('project_'))
# Temp uploads marked used.
self.assertEqual(
TempUpload.objects.filter(user=self.alice, status='used').count(), 2
@@ -138,23 +139,31 @@ class UGCProjectTests(UGCMediaTestCase, UGCGatedTestCase):
self.assertEqual(resp.status_code, 200)
self.assertFalse(Project.objects.filter(slug='no-file').exists())
def test_create_adopts_media_and_caption(self):
def test_create_adopts_media_and_captions(self):
self.gate()
self.client.login(username='Alice', password='pw')
self._upload_temp('thumbnail', 'thumb.png', make_png_bytes(), 'image/png')
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip')
self._upload_temp('media', 'pic1.png', make_png_bytes(), 'image/png')
self._upload_temp('media', 'pic2.png', make_png_bytes(), 'image/png')
media = TempUpload.objects.filter(kind='media').order_by('created_at')
caption_payload = {
f'caption_{media[0].uuid}': 'Showcase',
f'caption_{media[1].uuid}': 'Second angle',
}
resp = self.client.post(reverse('library:project_create'), {
'title': 'Media Pack',
'category': 'skin',
'version_name': '1.0.0',
'caption': 'Showcase',
**caption_payload,
})
self.assertRedirects(resp, reverse('library:project_detail', args=['media-pack']))
project = Project.objects.get(slug='media-pack')
self.assertEqual(project.assets.count(), 2)
self.assertEqual(project.assets.first().caption, 'Showcase')
# Per-file captions stored individually.
captions = dict(project.assets.values_list('file__original_filename', 'caption'))
self.assertEqual(captions['pic1.png'], 'Showcase')
self.assertEqual(captions['pic2.png'], 'Second angle')
for asset in project.assets.all():
self.assertTrue(asset.file.stored_path.startswith('project_'))
@@ -207,13 +216,39 @@ class UGCVersionTests(UGCMediaTestCase, UGCGatedTestCase):
self.assertEqual(self.project.versions.count(), 1)
version = self.project.versions.first()
self.assertEqual(version.version_name, '2.0.0')
self.assertTrue(version.file_id)
self.assertEqual(version.files.count(), 1)
# Adopted file moved under project_<pk>/versions/ and temp marked used.
self.assertTrue(version.file.stored_path.startswith('project_'))
self.assertTrue(version.files.first().file.stored_path.startswith('project_'))
self.assertTrue(
TempUpload.objects.filter(user=self.alice, kind='version', status='used').exists()
)
def test_upload_version_multiple_files(self):
self.gate()
self.client.login(username='Alice', password='pw')
for name in ('skin1.png', 'skin2.png'):
self.client.post(
reverse('library:api_upload_temp'),
{'kind': 'version', 'file': SimpleUploadedFile(name, make_png_bytes(), content_type='image/png')},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
resp = self.client.post(
reverse('library:version_upload', args=['test-pack']),
{'version_name': '3.0.0'},
)
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
version = self.project.versions.first()
self.assertEqual(version.version_name, '3.0.0')
# One version, several files, PNGs classified as png kind.
self.assertEqual(version.files.count(), 2)
self.assertEqual(
set(version.files.values_list('kind', flat=True)), {'png'}
)
self.assertEqual(
set(version.files.values_list('file__original_filename', flat=True)),
{'skin1.png', 'skin2.png'},
)
def test_upload_version_requires_pending_file(self):
self.gate()
self.client.login(username='Alice', password='pw')
@@ -259,7 +294,8 @@ class UGCVersionTests(UGCMediaTestCase, UGCGatedTestCase):
SimpleUploadedFile('pack.zip', make_zip_bytes(), content_type='application/zip'),
self.project.pk, subdir='versions',
)
version = Version.objects.create(project=self.project, version_name='1.0.0', file=index)
version = Version.objects.create(project=self.project, version_name='1.0.0')
VersionFile.objects.create(version=version, file=index, kind='release')
self.gate()
resp = self.client.get(reverse('library:version_download', args=['test-pack', version.pk]))
self.assertEqual(resp.status_code, 302)
@@ -270,6 +306,29 @@ class UGCVersionTests(UGCMediaTestCase, UGCGatedTestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(b''.join(resp.streaming_content), make_zip_bytes())
def test_per_file_download(self):
from library.storage import store_file
index = store_file(
self.alice, 'version',
SimpleUploadedFile('pack.zip', make_zip_bytes(), content_type='application/zip'),
self.project.pk, subdir='versions',
)
version = Version.objects.create(project=self.project, version_name='1.0.0')
vf = VersionFile.objects.create(version=version, file=index, kind='release')
self.gate()
resp = self.client.get(
reverse('library:version_file_download', args=['test-pack', version.pk, index.uuid])
)
self.assertEqual(resp.status_code, 302)
version.refresh_from_db()
self.assertEqual(version.downloads, 1)
self.assertEqual(resp.url, reverse('library:file_request', args=[index.uuid]) + '?download=1')
# The parent version_download route still works (first file).
resp = self.client.get(reverse('library:version_download', args=['test-pack', version.pk]))
self.assertEqual(resp.status_code, 302)
self.assertTrue(vf)
class UGCGalleryTests(UGCMediaTestCase, UGCGatedTestCase):
def setUp(self):
@@ -282,7 +341,7 @@ class UGCGalleryTests(UGCMediaTestCase, UGCGatedTestCase):
slug='test-pack', title='Test Pack', category='skin', owner=self.alice,
)
def test_upload_image_asset(self):
def test_upload_image_asset_with_caption(self):
self.gate()
self.client.login(username='Alice', password='pw')
self.client.post(
@@ -290,13 +349,38 @@ class UGCGalleryTests(UGCMediaTestCase, UGCGatedTestCase):
{'kind': 'media', 'file': SimpleUploadedFile('pic.png', make_png_bytes(), content_type='image/png')},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
resp = self.client.post(reverse('library:asset_upload', args=['test-pack']), {'caption': 'Preview'})
temp = TempUpload.objects.get(kind='media')
resp = self.client.post(
reverse('library:asset_upload', args=['test-pack']),
{f'caption_{temp.uuid}': 'Preview'},
)
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
asset = ProjectAsset.objects.get()
self.assertEqual(asset.caption, 'Preview')
self.assertEqual(asset.uploaded_by, self.alice)
self.assertFalse(asset.is_video)
def test_asset_captions_are_per_file(self):
self.gate()
self.client.login(username='Alice', password='pw')
self.client.post(
reverse('library:api_upload_temp'),
{'kind': 'media', 'file': SimpleUploadedFile('a.png', make_png_bytes(color='red'), content_type='image/png')},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
self.client.post(
reverse('library:api_upload_temp'),
{'kind': 'media', 'file': SimpleUploadedFile('b.png', make_png_bytes(color='blue'), content_type='image/png')},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
temps = list(TempUpload.objects.filter(kind='media').order_by('created_at'))
self.client.post(reverse('library:asset_upload', args=['test-pack']), {
f'caption_{temps[0].uuid}': 'First',
f'caption_{temps[1].uuid}': 'Second',
})
captions = dict(ProjectAsset.objects.values_list('file__original_filename', 'caption'))
self.assertEqual(captions, {'a.png': 'First', 'b.png': 'Second'})
def test_reject_non_media_file(self):
self.gate()
self.client.login(username='Alice', password='pw')
@@ -311,7 +395,7 @@ class UGCGalleryTests(UGCMediaTestCase, UGCGatedTestCase):
def test_gallery_upload_requires_pending_media(self):
self.gate()
self.client.login(username='Alice', password='pw')
resp = self.client.post(reverse('library:asset_upload', args=['test-pack']), {'caption': 'none'})
resp = self.client.post(reverse('library:asset_upload', args=['test-pack']))
self.assertEqual(resp.status_code, 200)
self.assertEqual(ProjectAsset.objects.count(), 0)
@@ -427,7 +511,7 @@ class UGCDraftTests(UGCMediaTestCase, UGCGatedTestCase):
self.assertEqual(resp.status_code, 400)
self.assertEqual(TempUpload.objects.count(), 0)
def test_thumbnail_and_version_are_single_slot(self):
def test_thumbnail_single_slot_version_multi(self):
self.gate()
self.client.login(username='Alice', password='pw')
self.client.post(
@@ -450,12 +534,12 @@ class UGCDraftTests(UGCMediaTestCase, UGCGatedTestCase):
{'kind': 'version', 'file': SimpleUploadedFile('v2.zip', make_zip_bytes(), content_type='application/zip')},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
# Only the latest thumbnail + version remain; media unaffected.
# Thumbnail is a single slot; version accepts multiple files.
self.assertEqual(
TempUpload.objects.filter(kind='thumbnail', status='pending').count(), 1
)
self.assertEqual(
TempUpload.objects.filter(kind='version', status='pending').count(), 1
TempUpload.objects.filter(kind='version', status='pending').count(), 2
)
def test_temp_upload_delete(self):