feat(sources): Sammel-Bearbeitung fuer Vorschlaege im Aufgaben-Reiter

Die Vorschlags-Tabelle hat jetzt eine Checkbox-Spalte mit Alle-auswaehlen
(wirkt auf die gefilterten Zeilen) und zwei Knoepfe Ausgewaehlte annehmen /
Ausgewaehlte ablehnen. Noetig geworden, weil ein Pruefaluf hunderte
Vorschlaege erzeugen kann und Einzelklicks mit Dialog dann nicht praktikabel
sind. Neuer Endpoint PUT /suggestions/bulk nutzt exakt dieselbe
Entscheidungslogik wie der Einzel-Endpoint (gemeinsamer Helper
_apply_suggestion_decision, inkl. Auto-Reject und Duplikat-Schutz),
bereits erledigte IDs werden uebersprungen statt als Fehler gewertet.
Ein Audit-Eintrag pro Sammelaktion mit Zaehlern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-25 18:54:43 +00:00
Ursprung c5f63b1d14
Commit ae4fb106ad
4 geänderte Dateien mit 145 neuen und 23 gelöschten Zeilen

Datei anzeigen

@@ -856,32 +856,19 @@ class SuggestionAction(BaseModel):
accept: bool
@router.put("/suggestions/{suggestion_id}")
async def update_suggestion(
suggestion_id: int,
action: SuggestionAction,
request: Request,
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Vorschlag annehmen oder ablehnen."""
async def _apply_suggestion_decision(db: aiosqlite.Connection, suggestion: dict, accept: bool):
"""Führt die Annahme/Ablehnung EINES pending-Vorschlags aus (ohne commit/Audit).
Liefert (new_status, result_action). Wird vom Einzel- und vom
Sammel-Endpoint identisch genutzt.
"""
import json as _json
cursor = await db.execute(
"SELECT * FROM source_suggestions WHERE id = ?", (suggestion_id,)
)
suggestion = await cursor.fetchone()
if not suggestion:
raise HTTPException(status_code=404, detail="Vorschlag nicht gefunden")
suggestion = dict(suggestion)
if suggestion["status"] != "pending":
raise HTTPException(status_code=400, detail=f"Vorschlag bereits {suggestion['status']}")
new_status = "accepted" if action.accept else "rejected"
suggestion_id = suggestion["id"]
new_status = "accepted" if accept else "rejected"
result_action = None
if action.accept:
if accept:
stype = suggestion["suggestion_type"]
data = _json.loads(suggestion["suggested_data"]) if suggestion["suggested_data"] else {}
@@ -944,6 +931,75 @@ async def update_suggestion(
"UPDATE source_suggestions SET status = ?, reviewed_at = CURRENT_TIMESTAMP WHERE id = ?",
(new_status, suggestion_id),
)
return new_status, result_action
class BulkSuggestionAction(BaseModel):
suggestion_ids: list[int] = Field(min_length=1, max_length=1000)
accept: bool
@router.put("/suggestions/bulk")
async def bulk_update_suggestions(
action: BulkSuggestionAction,
request: Request,
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Mehrere Vorschläge auf einmal annehmen oder ablehnen.
Nicht mehr pendende IDs (schon bearbeitet, per Auto-Reject erledigt
oder unbekannt) werden übersprungen und gezählt, nicht als Fehler gewertet.
"""
accepted = 0
rejected = 0
skipped = 0
for sid in action.suggestion_ids:
cursor = await db.execute("SELECT * FROM source_suggestions WHERE id = ?", (sid,))
row = await cursor.fetchone()
if not row:
skipped += 1
continue
suggestion = dict(row)
if suggestion["status"] != "pending":
skipped += 1
continue
new_status, _ = await _apply_suggestion_decision(db, suggestion, action.accept)
if new_status == "accepted":
accepted += 1
else:
rejected += 1
await db.commit()
await log_action(
db, admin, get_client_ip(request),
action="update", resource_type="source",
before={"suggestion_bulk": len(action.suggestion_ids), "accept": action.accept},
after={"accepted": accepted, "rejected": rejected, "skipped": skipped},
)
return {"accepted": accepted, "rejected": rejected, "skipped": skipped}
@router.put("/suggestions/{suggestion_id}")
async def update_suggestion(
suggestion_id: int,
action: SuggestionAction,
request: Request,
admin: dict = Depends(get_current_admin),
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Vorschlag annehmen oder ablehnen."""
cursor = await db.execute(
"SELECT * FROM source_suggestions WHERE id = ?", (suggestion_id,)
)
suggestion = await cursor.fetchone()
if not suggestion:
raise HTTPException(status_code=404, detail="Vorschlag nicht gefunden")
suggestion = dict(suggestion)
if suggestion["status"] != "pending":
raise HTTPException(status_code=400, detail=f"Vorschlag bereits {suggestion['status']}")
new_status, result_action = await _apply_suggestion_decision(db, suggestion, action.accept)
await db.commit()
await log_action(
db, admin, get_client_ip(request),