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:
@@ -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),
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren