Unpaginate staged uploads and make big upload batches visible

The upload board partitions /api/uploads/ into Pending / Visual similarity /
Auto-uploaded, but the endpoint was paginated at 48 — a 69-file batch
silently lost 21 entries, and the similarity sweep (which reads the same
list back after uploading) only ever saw the first page. The staged-upload
list is now unpaginated: it is a transient per-user set, still limited to
the caller's rows and the uploader role. The page takes a plain array.

Watching progress with dozens of files was also poor:
- the queue uploads three files at a time instead of strictly one at a time;
- the Uploads section now shows a batch bar and 'n/m uploaded · x%' next to
  the count, so the overall progress never scrolls out of sight;
- entries are ordered active-first (uploading, queued, failed, done) so the
  file being uploaded is always at the top of the grid;
- tiles are larger (4 columns at lg instead of 5);
- the header reads 'Uploading n/m…' and 'Checking n file(s) against IQDB…'
  instead of a bare spinner.

Tests: staged-upload list unpaginated past 48, per-user, uploader-only
(3 new; full suite 49 green). Live-checked the bare-array response.
This commit is contained in:
2026-09-18 19:58:17 -05:00
parent 1adb761c8d
commit 39307cb141
3 changed files with 168 additions and 32 deletions
@@ -0,0 +1,75 @@
"""The staged-upload list is a complete set, not a page.
The upload board partitions it into Pending / Visual similarity / Auto-uploaded
columns; with the standard 48-item pagination a 69-file batch silently lost 21
entries from the board (and from the similarity sweep that reads it back).
"""
import hashlib
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from rest_framework.authtoken.models import Token
from apps.library.models import TempUpload
User = get_user_model()
class TempUploadListTests(TestCase):
def setUp(self):
self.uploader = User.objects.create_user(
username="upload-user", password="upload-pass-123456"
)
self.uploader.role = "uploader"
self.uploader.save(update_fields=["role"])
self.other = User.objects.create_user(
username="upload-other", password="upload-pass-123456"
)
self.other.role = "uploader"
self.other.save(update_fields=["role"])
def api_client(self, user):
client = Client()
client.defaults["HTTP_AUTHORIZATION"] = (
f"Token {Token.objects.create(user=user).key}"
)
return client
def test_list_is_not_paginated_past_48(self):
TempUpload.objects.bulk_create(
[
TempUpload(
user=self.uploader,
original_filename=f"file_{index:03d}.png",
md5=hashlib.md5(f"file-{index}".encode()).hexdigest(),
size=index,
)
for index in range(69)
]
)
response = self.api_client(self.uploader).get("/api/uploads/")
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIsInstance(data, list, "the list must not be paginated")
self.assertEqual(len(data), 69)
def test_list_only_contains_the_callers_uploads(self):
TempUpload.objects.create(
user=self.uploader,
original_filename="mine.png",
md5=hashlib.md5(b"mine").hexdigest(),
size=1,
)
TempUpload.objects.create(
user=self.other,
original_filename="theirs.png",
md5=hashlib.md5(b"theirs").hexdigest(),
size=1,
)
mine = self.api_client(self.uploader).get("/api/uploads/").json()
self.assertEqual([row["original_filename"] for row in mine], ["mine.png"])
def test_anonymous_cannot_list(self):
self.assertEqual(Client().get("/api/uploads/").status_code, 401)