Skip to content

Commit 9f6a92c

Browse files
committed
feat: ability to discard all reviews and clean history
1 parent d6cb719 commit 9f6a92c

7 files changed

Lines changed: 149 additions & 11 deletions

File tree

server/routers/review.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,15 @@ def decide_bulk(body: ReviewBulkDecideBody) -> dict:
146146
else:
147147
applied = storage.accept_rule_fixes(body.dataset_id, body.rule_id)
148148
return {"accepted": applied}
149+
150+
151+
@router.post("/findings/reject_all")
152+
def reject_all(body: ReviewBulkDecideBody) -> dict:
153+
"""Reject every pending finding of a dataset (captions untouched)."""
154+
return {"rejected": storage.reject_all_findings(body.dataset_id)}
155+
156+
157+
@router.post("/findings/clear_history")
158+
def clear_history(body: ReviewBulkDecideBody) -> dict:
159+
"""Delete the decided findings (history); pending ones stay."""
160+
return {"cleared": storage.clear_review_history(body.dataset_id)}

src/sqlite_store/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@
223223
pending_for_media,
224224
rebase_finding,
225225
decide_finding,
226+
reject_all_pending,
227+
clear_decided_findings,
226228
reopen_finding,
227229
clear_dataset_findings,
228230
clear_media_findings,
@@ -356,6 +358,8 @@
356358
"pending_for_media",
357359
"rebase_finding",
358360
"decide_finding",
361+
"reject_all_pending",
362+
"clear_decided_findings",
359363
"reopen_finding",
360364
"clear_dataset_findings",
361365
"clear_media_findings",

src/sqlite_store/review_queue.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,40 @@ def decide_finding(
313313
)
314314

315315

316+
def reject_all_pending(dataset_id: int) -> int:
317+
"""Reject every pending finding of a dataset; return how many.
318+
319+
Rejecting never touches a caption, so one UPDATE is enough — no
320+
per-finding apply pass like the bulk accepts.
321+
"""
322+
with closing(db.connect()) as conn:
323+
with conn:
324+
cursor = conn.execute(
325+
"UPDATE review_finding SET status = ?, "
326+
"decided_at = datetime('now') "
327+
"WHERE status = ? AND run_id IN "
328+
"(SELECT id FROM review_run WHERE dataset_id = ?)",
329+
(STATUS_REJECTED, STATUS_PENDING, dataset_id),
330+
)
331+
return cursor.rowcount
332+
333+
334+
def clear_decided_findings(dataset_id: int) -> int:
335+
"""Delete a dataset's decided findings (the history); return how many.
336+
337+
Pending findings stay; accepted captions keep their revisions (this
338+
only clears the queue's history, never a caption).
339+
"""
340+
with closing(db.connect()) as conn:
341+
with conn:
342+
cursor = conn.execute(
343+
"DELETE FROM review_finding WHERE status != ? AND run_id IN "
344+
"(SELECT id FROM review_run WHERE dataset_id = ?)",
345+
(STATUS_PENDING, dataset_id),
346+
)
347+
return cursor.rowcount
348+
349+
316350
def reopen_finding(finding_id: int) -> None:
317351
"""Return a decided finding to ``pending`` (the undo path)."""
318352
_write(

src/storage.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -956,11 +956,8 @@ def record_review_finding(
956956
def _apply_accept(finding: dict, caption: str = None) -> None:
957957
"""Write the accepted caption as a new revision for a finding's media.
958958
959-
The finding's fix is merged into the *live* caption
960-
(:func:`src.caption_judge.apply_fix`), never written verbatim: accepting
961-
a second finding must not resurrect the run-time caption and erase the
962-
first accept (or a manual edit made since the run). An explicit
963-
``caption`` (inline edit before accept) still wins as-is.
959+
The fix is merged into the *live* caption (never written verbatim) so a
960+
second accept keeps the first; an explicit ``caption`` wins as-is.
964961
"""
965962
type_row = store.get_caption_type(finding["caption_type_id"])
966963
caption_type = type_row["name"] if type_row else ""
@@ -980,8 +977,7 @@ def _apply_accept(finding: dict, caption: str = None) -> None:
980977
finding["id"], store.STATUS_ACCEPTED, applied_caption=final
981978
)
982979
# Rebase the media's other pending findings onto the new caption: their
983-
# "original" then shows this accept applied, and their proposal is the
984-
# same fix re-applied on top — the next accept diffs against reality.
980+
# "original" shows this accept applied, their proposal the fix on top.
985981
for sibling in store.pending_for_media(
986982
finding["dataset_id"],
987983
finding["media_id"],
@@ -996,10 +992,7 @@ def _apply_accept(finding: dict, caption: str = None) -> None:
996992
def decide_review_finding(
997993
finding_id: int, action: str, caption: str = None
998994
) -> dict:
999-
"""Accept (write a new revision) or reject one finding; return it.
1000-
1001-
``caption`` overrides the proposed text (an inline edit before accept).
1002-
"""
995+
"""Accept (``caption`` = inline-edit override) or reject one finding."""
1003996
finding = store.get_finding(finding_id)
1004997
if finding is None:
1005998
return {}
@@ -1050,6 +1043,20 @@ def accept_rule_fixes(dataset_ref, rule_id: int) -> int:
10501043
return len(findings)
10511044

10521045

1046+
def reject_all_findings(dataset_ref) -> int:
1047+
"""Reject every pending finding of a dataset; return the count."""
1048+
dataset_id = _dataset_id(dataset_ref)
1049+
return 0 if dataset_id is None else store.reject_all_pending(dataset_id)
1050+
1051+
1052+
def clear_review_history(dataset_ref) -> int:
1053+
"""Delete the dataset's decided findings (history); return the count."""
1054+
dataset_id = _dataset_id(dataset_ref)
1055+
if dataset_id is None:
1056+
return 0
1057+
return store.clear_decided_findings(dataset_id)
1058+
1059+
10531060
def media_path(dataset_ref, key: str) -> str | None:
10541061
"""Return the effective media file path for a key, or None when unset.
10551062

tests/test_review_queue.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,25 @@ def test_second_accept_merges_with_the_first(self, dataset):
163163
)
164164
assert caption == "a crimson ball on the lawn."
165165

166+
def test_reject_all_then_clear_history(self, dataset):
167+
"""Reject-all rejects every pending row; clear drops the decided."""
168+
_run_det(dataset["dataset_id"])
169+
assert storage.reject_all_findings(dataset["dataset_id"]) == 1
170+
counts = store.findings_counts(dataset["dataset_id"])
171+
assert counts["pending"] == 0
172+
assert counts["rejected"] == 1
173+
# The caption was never touched by the rejection.
174+
caption = storage.read_caption(
175+
dataset["dataset_id"], dataset["key"], "txt"
176+
)
177+
assert caption == "a red ball on the grass."
178+
assert storage.clear_review_history(dataset["dataset_id"]) == 1
179+
assert store.findings_counts(dataset["dataset_id"]) == {
180+
"pending": 0,
181+
"accepted": 0,
182+
"rejected": 0,
183+
}
184+
166185
def test_undo_restores_caption(self, dataset):
167186
"""Undo restores the original caption and reopens the finding."""
168187
_run_det(dataset["dataset_id"])

web/src/api/hooks.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,26 @@ export function useUndoFinding() {
908908
});
909909
}
910910

911+
/** Reject every pending finding of the dataset (captions untouched). */
912+
export function useRejectAll() {
913+
const invalidate = useReviewInvalidator();
914+
return useMutation({
915+
mutationFn: (vars: { dataset_id: number }) =>
916+
api.post<{ rejected: number }>("/review/findings/reject_all", vars),
917+
onSuccess: (_data, vars) => invalidate(vars.dataset_id),
918+
});
919+
}
920+
921+
/** Delete the decided findings (the history); pending ones stay. */
922+
export function useClearReviewHistory() {
923+
const invalidate = useReviewInvalidator();
924+
return useMutation({
925+
mutationFn: (vars: { dataset_id: number }) =>
926+
api.post<{ cleared: number }>("/review/findings/clear_history", vars),
927+
onSuccess: (_data, vars) => invalidate(vars.dataset_id),
928+
});
929+
}
930+
911931
/** Accept every safe finding, or every pending finding of one rule. */
912932
export function useDecideBulk() {
913933
const invalidate = useReviewInvalidator();

web/src/components/organisms/ReviewView.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import { useEffect, useMemo, useState } from "react";
1313
import { useQueryClient } from "@tanstack/react-query";
1414
import {
1515
useCreateReviewRule,
16+
useClearReviewHistory,
1617
useDecideBulk,
1718
useDecideFinding,
19+
useRejectAll,
1820
useDeleteReviewRule,
1921
useProfiles,
2022
useReviewFindings,
@@ -441,6 +443,9 @@ function Queue({
441443
onOpenWizard: (index: number) => void;
442444
}) {
443445
const decideBulk = useDecideBulk();
446+
const rejectAll = useRejectAll();
447+
const clearHistory = useClearReviewHistory();
448+
const decidedCount = counts.accepted + counts.rejected;
444449
const safeCount = list.filter(
445450
(f) => f.status === "pending" && SAFE_KINDS.has(f.rule_kind),
446451
).length;
@@ -485,6 +490,43 @@ function Queue({
485490
<CountChip n={counts.accepted} label="accepted" color={colors.ok} />
486491
<CountChip n={counts.rejected} label="rejected" color={colors.danger} />
487492
<div style={{ flex: 1 }} />
493+
{decidedCount > 0 && (
494+
<Button
495+
variant="ghost"
496+
onClick={() => {
497+
if (
498+
window.confirm(
499+
`Clear the review history (${decidedCount} decided ` +
500+
"findings)? Captions are not touched.",
501+
)
502+
) {
503+
clearHistory.mutate({ dataset_id: datasetId });
504+
}
505+
}}
506+
loading={clearHistory.isPending}
507+
>
508+
🗑 Clear history · {decidedCount}
509+
</Button>
510+
)}
511+
{counts.pending > 0 && (
512+
<Button
513+
variant="ghost"
514+
style={{ color: colors.danger, borderColor: colors.danger }}
515+
onClick={() => {
516+
if (
517+
window.confirm(
518+
`Reject all ${counts.pending} pending findings? ` +
519+
"Captions are not touched.",
520+
)
521+
) {
522+
rejectAll.mutate({ dataset_id: datasetId });
523+
}
524+
}}
525+
loading={rejectAll.isPending}
526+
>
527+
✕ Reject all · {counts.pending}
528+
</Button>
529+
)}
488530
{safeCount > 0 && (
489531
<Button
490532
variant="ghost"

0 commit comments

Comments
 (0)