Drafts system implemented

This commit is contained in:
2026-08-05 17:16:09 -05:00
parent 203ff88af1
commit 49c4ff681f
13 changed files with 419 additions and 75 deletions
+147 -25
View File
@@ -79,18 +79,21 @@ class UGCProjectTests(UGCMediaTestCase, UGCGatedTestCase):
self.bob = User.objects.create_user(username='Bob', password='pw')
UserProfile.objects.get_or_create(user=self.bob)
def _upload_temp(self, kind, name, content, content_type):
def _upload_temp(self, kind, name, content, content_type, draft_uuid=None):
data = {'kind': kind, 'file': SimpleUploadedFile(name, content, content_type=content_type)}
if draft_uuid is not None:
data['draft_uuid'] = str(draft_uuid)
return self.client.post(
reverse('library:api_upload_temp'),
{'kind': kind, 'file': SimpleUploadedFile(name, content, content_type=content_type)},
reverse('library:api_upload_temp'), data,
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
def _create_project(self, user, title='Test Pack', tags='general:furry content:threesome'):
self.gate()
self.client.login(username=user.username, password='pw')
self._upload_temp('thumbnail', 'thumb.png', make_png_bytes(), 'image/png')
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip')
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
self._upload_temp('thumbnail', 'thumb.png', make_png_bytes(), 'image/png', draft_uuid=uuid)
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip', draft_uuid=uuid)
data = {
'title': title,
'summary': 'A test pack.',
@@ -100,11 +103,11 @@ class UGCProjectTests(UGCMediaTestCase, UGCGatedTestCase):
'version_name': '1.0.0',
'changelog': 'First release.',
}
return self.client.post(reverse('library:project_create'), data)
return self.client.post(reverse('library:project_create', args=[uuid]), data)
def test_create_requires_login(self):
self.gate()
resp = self.client.get(reverse('library:project_create'))
resp = self.client.get(reverse('library:project_create_new'))
self.assertEqual(resp.status_code, 302)
def test_create_project(self):
@@ -135,7 +138,8 @@ class UGCProjectTests(UGCMediaTestCase, UGCGatedTestCase):
def test_create_requires_version_file(self):
self.gate()
self.client.login(username='Alice', password='pw')
resp = self.client.post(reverse('library:project_create'), {
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
resp = self.client.post(reverse('library:project_create', args=[uuid]), {
'title': 'No File',
'category': 'mod',
'version_name': '1.0.0',
@@ -146,16 +150,17 @@ class UGCProjectTests(UGCMediaTestCase, UGCGatedTestCase):
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')
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
self._upload_temp('thumbnail', 'thumb.png', make_png_bytes(), 'image/png', draft_uuid=uuid)
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip', draft_uuid=uuid)
self._upload_temp('media', 'pic1.png', make_png_bytes(), 'image/png', draft_uuid=uuid)
self._upload_temp('media', 'pic2.png', make_png_bytes(), 'image/png', draft_uuid=uuid)
media = TempUpload.objects.filter(kind='media', draft_uuid=uuid).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'), {
resp = self.client.post(reverse('library:project_create', args=[uuid]), {
'title': 'Media Pack',
'category': 'skin',
'version_name': '1.0.0',
@@ -192,6 +197,119 @@ class UGCProjectTests(UGCMediaTestCase, UGCGatedTestCase):
self.assertEqual(resp.status_code, 404)
class UGCProjectDraftTests(UGCMediaTestCase, UGCGatedTestCase):
"""Multiple per-user drafts, keyed by UUID, with upload scoping."""
def setUp(self):
User = get_user_model()
self.alice = User.objects.create_user(username='Alice', password='pw')
UserProfile.objects.get_or_create(user=self.alice)
self.bob = User.objects.create_user(username='Bob', password='pw')
UserProfile.objects.get_or_create(user=self.bob)
def _auth(self, user):
self.client.force_login(user)
self.gate()
def _upload_temp(self, kind, name, content, content_type, draft_uuid=None):
data = {'kind': kind, 'file': SimpleUploadedFile(name, content, content_type=content_type)}
if draft_uuid is not None:
data['draft_uuid'] = str(draft_uuid)
return self.client.post(
reverse('library:api_upload_temp'), data,
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
def _new_draft(self, user=None):
self._auth(user or self.alice)
resp = self.client.get(reverse('library:project_create_new'))
uuid = resp.url.rstrip('/').split('/')[-1]
return uuid, resp
def test_new_project_creates_draft_and_redirects(self):
uuid, resp = self._new_draft()
self.assertRedirects(
resp, reverse('library:project_create', args=[uuid]),
fetch_redirect_response=False,
)
self.assertTrue(ProjectDraft.objects.filter(uuid=uuid, user=self.alice).exists())
def test_my_projects_lists_projects_and_drafts(self):
project = Project.objects.create(
slug='mine', title='Mine', category='mod', owner=self.alice,
)
uuid, _ = self._new_draft()
self._auth(self.alice)
resp = self.client.get(reverse('library:my_projects'))
self.assertContains(resp, 'Mine')
self.assertContains(resp, 'Untitled draft')
self.assertEqual(Project.objects.get(pk=project.pk).title, 'Mine')
def test_wrong_user_cannot_access_draft(self):
uuid, _ = self._new_draft()
self._auth(self.bob)
resp = self.client.get(reverse('library:project_create', args=[uuid]))
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp.url, reverse('library:my_projects'))
# API save also refused.
resp = self.client.post(
reverse('library:api_draft'),
data=json.dumps({'draft': uuid, 'data': {'title': 'hack'}}),
content_type='application/json',
)
self.assertEqual(resp.status_code, 404)
draft = ProjectDraft.objects.get(uuid=uuid)
self.assertNotEqual(draft.data.get('title'), 'hack')
def test_autosave_scopes_to_its_draft(self):
uuid_a, _ = self._new_draft()
uuid_b, _ = self._new_draft()
self._auth(self.alice)
self.client.post(
reverse('library:api_draft'),
data=json.dumps({'draft': uuid_a, 'data': {'title': 'Draft A'}}),
content_type='application/json',
)
self.assertEqual(ProjectDraft.objects.get(uuid=uuid_a).data.get('title'), 'Draft A')
self.assertEqual(ProjectDraft.objects.get(uuid=uuid_b).data.get('title'), None)
def test_uploads_scoped_per_draft(self):
uuid_a, _ = self._new_draft()
uuid_b, _ = self._new_draft()
self._auth(self.alice)
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip', draft_uuid=uuid_a)
self.assertEqual(TempUpload.objects.filter(draft_uuid=uuid_a).count(), 1)
self.assertEqual(TempUpload.objects.filter(draft_uuid=uuid_b).count(), 0)
self.assertEqual(TempUpload.objects.filter(draft_uuid=uuid_a, status='pending').count(), 1)
self.assertEqual(TempUpload.objects.filter(draft_uuid=uuid_b, status='pending').count(), 0)
def test_submit_consumes_draft_uploads_and_deletes_draft(self):
uuid, _ = self._new_draft()
self._auth(self.alice)
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip', draft_uuid=uuid)
resp = self.client.post(
reverse('library:project_create', args=[uuid]),
{
'title': 'From Draft', 'summary': 's', 'description': 'd',
'category': 'mod', 'tags': 'general:test',
'version_name': '1.0.0', 'changelog': 'c',
},
)
self.assertRedirects(resp, reverse('library:project_detail', args=['from-draft']))
self.assertTrue(Project.objects.filter(title='From Draft', owner=self.alice).exists())
self.assertFalse(ProjectDraft.objects.filter(uuid=uuid).exists())
self.assertEqual(TempUpload.objects.filter(draft_uuid=uuid, status='used').count(), 1)
def test_draft_delete_purges_uploads(self):
uuid, _ = self._new_draft()
self._auth(self.alice)
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip', draft_uuid=uuid)
resp = self.client.post(reverse('library:project_draft_delete', args=[uuid]))
self.assertEqual(resp.status_code, 302)
self.assertFalse(ProjectDraft.objects.filter(uuid=uuid).exists())
self.assertFalse(TempUpload.objects.filter(draft_uuid=uuid).exists())
class UGCVersionTests(UGCMediaTestCase, UGCGatedTestCase):
def setUp(self):
User = get_user_model()
@@ -585,7 +703,8 @@ class UGCDraftTests(UGCMediaTestCase, UGCGatedTestCase):
def test_draft_save_and_restore(self):
self.gate()
self.client.login(username='Alice', password='pw')
self._post_json(reverse('library:api_draft'), {'data': {
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
self._post_json(reverse('library:api_draft'), {'draft': uuid, 'data': {
'title': 'Draft Title',
'summary': 'Draft summary',
'category': 'mod',
@@ -593,34 +712,36 @@ class UGCDraftTests(UGCMediaTestCase, UGCGatedTestCase):
'tags': 'species:dragon',
'version_name': '2.0.0',
}})
draft = ProjectDraft.objects.get(user=self.alice)
draft = ProjectDraft.objects.get(uuid=uuid, user=self.alice)
self.assertEqual(draft.data['title'], 'Draft Title')
resp = self.client.get(reverse('library:api_draft'))
resp = self.client.get(reverse('library:api_draft'), {'draft': uuid})
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json()['data']['title'], 'Draft Title')
def test_create_form_prefills_draft(self):
self.gate()
self.client.login(username='Alice', password='pw')
self._post_json(reverse('library:api_draft'), {'data': {'title': 'My Draft Pack'}})
resp = self.client.get(reverse('library:project_create'))
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
self._post_json(reverse('library:api_draft'), {'draft': uuid, 'data': {'title': 'My Draft Pack'}})
resp = self.client.get(reverse('library:project_create', args=[uuid]))
self.assertContains(resp, 'value="My Draft Pack"')
def test_create_clears_draft(self):
self.gate()
self.client.login(username='Alice', password='pw')
self._post_json(reverse('library:api_draft'), {'data': {'title': 'My Draft Pack'}})
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
self._post_json(reverse('library:api_draft'), {'draft': uuid, 'data': {'title': 'My Draft Pack'}})
self.client.post(
reverse('library:api_upload_temp'),
{'kind': 'version', 'file': SimpleUploadedFile('pack.zip', make_zip_bytes(), content_type='application/zip')},
{'kind': 'version', 'file': SimpleUploadedFile('pack.zip', make_zip_bytes(), content_type='application/zip'), 'draft_uuid': uuid},
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
resp = self.client.post(reverse('library:project_create'), {
resp = self.client.post(reverse('library:project_create', args=[uuid]), {
'title': 'My Draft Pack', 'category': 'mod', 'version_name': '1.0.0',
})
self.assertRedirects(resp, reverse('library:project_detail', args=['my-draft-pack']))
self.assertFalse(ProjectDraft.objects.filter(user=self.alice).exists())
self.assertFalse(ProjectDraft.objects.filter(uuid=uuid).exists())
class UGCPermissionTests(UGCMediaTestCase, UGCGatedTestCase):
@@ -1825,10 +1946,11 @@ class UGCAnimationIdTests(UGCMediaTestCase, UGCGatedTestCase):
def test_create_injects_defaults(self):
self.gate()
self.client.login(username='Alice', password='pw')
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
self.client.post(reverse('library:api_upload_temp'),
{'kind': 'version', 'file': SimpleUploadedFile('pack.zip', _logical_zip(), content_type='application/zip')},
{'kind': 'version', 'file': SimpleUploadedFile('pack.zip', _logical_zip(), content_type='application/zip'), 'draft_uuid': uuid},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
resp = self.client.post(reverse('library:project_create'), {
resp = self.client.post(reverse('library:project_create', args=[uuid]), {
'title': 'New Pack', 'category': 'non_pack', 'version_name': '1.0.0',
})
self.assertRedirects(resp, reverse('library:project_detail', args=['new-pack']))