feat(backend): per-file review status with DSL filter and bulk endpoint
Replaces the old "untagged" sentinel tag with a proper per-file workflow status: needs_review starts true on upload/import and is cleared by an explicit action (no auto-clear on tagging). Surfaced as a filter token (r=1 needs review, r=0 done) so it combines with tag/MIME conditions, and toggled via POST /files/bulk/review (single id or many, edit-ACL enforced, audit-logged as file_review). needs_review lives on data.files (column added to the original 003 migration, partial index in 006, action type seeded in 007). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -1339,6 +1340,58 @@ func TestImportOrdersByMtime(t *testing.T) {
|
||||
assert.Less(t, idx["b_middle.jpg"], idx["a_newest.jpg"], "middle should be processed before newest")
|
||||
}
|
||||
|
||||
// TestFileReviewStatus verifies the per-file "needs review" flag: new uploads
|
||||
// start as needs_review=true, POST /files/bulk/review clears it, and the DSL
|
||||
// filter r=0/r=1 selects reviewed/unreviewed files (also combined with others).
|
||||
func TestFileReviewStatus(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
h := setupSuite(t)
|
||||
tok := h.login("admin", "admin")
|
||||
|
||||
file := h.uploadJPEG(tok, "review-me.jpg")
|
||||
fileID := file["id"].(string)
|
||||
assert.Equal(t, true, file["needs_review"], "new upload should need review")
|
||||
|
||||
getReview := func() bool {
|
||||
r := h.doJSON("GET", "/files/"+fileID, nil, tok)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, r.String())
|
||||
var obj map[string]any
|
||||
r.decode(t, &obj)
|
||||
return obj["needs_review"].(bool)
|
||||
}
|
||||
listIDs := func(dsl string) []string {
|
||||
r := h.doJSON("GET", "/files?filter="+url.QueryEscape(dsl), nil, tok)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, r.String())
|
||||
var page map[string]any
|
||||
r.decode(t, &page)
|
||||
ids := []string{}
|
||||
for _, it := range page["items"].([]any) {
|
||||
ids = append(ids, it.(map[string]any)["id"].(string))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// Before marking done: appears under r=1 (needs review), not under r=0.
|
||||
assert.True(t, getReview())
|
||||
assert.Contains(t, listIDs("{r=1}"), fileID)
|
||||
assert.NotContains(t, listIDs("{r=0}"), fileID)
|
||||
|
||||
// Mark as reviewed (done) via the bulk endpoint (single-element list).
|
||||
resp := h.doJSON("POST", "/files/bulk/review", map[string]any{
|
||||
"file_ids": []string{fileID},
|
||||
"needs_review": false,
|
||||
}, tok)
|
||||
require.Equal(t, http.StatusNoContent, resp.StatusCode, resp.String())
|
||||
|
||||
// Now reviewed: appears under r=0, not r=1; combines with a MIME predicate.
|
||||
assert.False(t, getReview(), "file should no longer need review")
|
||||
assert.Contains(t, listIDs("{r=0}"), fileID)
|
||||
assert.NotContains(t, listIDs("{r=1}"), fileID)
|
||||
assert.Contains(t, listIDs("{r=0,&,m~image/%}"), fileID)
|
||||
}
|
||||
|
||||
// TestContentRangeRequests verifies the original-content endpoint answers a
|
||||
// byte-range request with 206 Partial Content (so the browser can seek within
|
||||
// audio/video) rather than streaming the whole body.
|
||||
|
||||
Reference in New Issue
Block a user