diff --git a/tests/test_import_flow.py b/tests/test_import_flow.py index 2e8b838..74ba814 100644 --- a/tests/test_import_flow.py +++ b/tests/test_import_flow.py @@ -1270,3 +1270,27 @@ def test_connections_hear_about_committed_rows(client, monkeypatch): calls.clear() client.delete(f"/api/requests/{req_id}") assert calls == [("POST", "http://hook/in")] # nothing new on disk, no scan + + +def test_bulk_move_delete_and_count(client): + def lib(name, media_type): + return client.post("/api/libraries", json={ + "name": name, "media_type": media_type, + "root_path": str(client.tmp_path / name)}).json()["id"] + + a, b, audio = lib("a", "ebook"), lib("b", "ebook"), lib("audio", "audiobook") + ids = [client.post("/api/requests", json={"library_id": a, "title": f"T{i}"}).json()["id"] + for i in range(3)] + assert client.get("/api/requests/count?status=missing").json() == {"count": 3} + + assert client.put("/api/requests/bulk", json={ + "ids": ids[:2], "library_id": audio}).status_code == 400 + assert client.put("/api/requests/bulk", json={ + "ids": ids[:2], "library_id": b}).json() == {"updated": 2} + assert [r["id"] for r in client.get(f"/api/requests?library_id={b}").json()] \ + == sorted(ids[:2], reverse=True) + + assert client.request("DELETE", "/api/requests/bulk", + json={"ids": ids[1:]}).json() == {"deleted": 2} + assert client.get("/api/requests/count?status=missing").json() == {"count": 1} + assert [r["event"] for r in client.get("/api/history").json()] == ["deleted"] * 2 diff --git a/wordarr/api/requests.py b/wordarr/api/requests.py index 7b4d221..093026f 100644 --- a/wordarr/api/requests.py +++ b/wordarr/api/requests.py @@ -2,7 +2,7 @@ from pathlib import Path from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from pydantic import BaseModel, ConfigDict -from sqlalchemy import select +from sqlalchemy import func, select from sqlalchemy.orm import Session from .. import config, connect @@ -176,6 +176,51 @@ def create_bulk_items(data: BulkItemsIn, session: Session = Depends(get_session) return {"created": [_out(r) for r in created], "skipped": skipped} +@router.get("/count") +def count_requests(status: str | None = None, session: Session = Depends(get_session)): + stmt = select(func.count()).select_from(BookRequest) + if status: + stmt = stmt.where(BookRequest.status == status) + return {"count": session.scalar(stmt)} + + +class BulkIdsIn(BaseModel): + ids: list[int] + + +class BulkUpdateIn(BulkIdsIn): + library_id: int + + +# registered before the /{request_id} routes, which would try "bulk" as an id +@router.put("/bulk") +def update_bulk(data: BulkUpdateIn, session: Session = Depends(get_session)): + """Move requests to another library. Imported files stay where they are + until the request is relocated.""" + lib = session.get(Library, data.library_id) + if not lib: + raise HTTPException(404, "library not found") + reqs = session.scalars(select(BookRequest).where(BookRequest.id.in_(data.ids))).all() + if any(r.media_type != lib.media_type for r in reqs): + raise HTTPException(400, "library has a different media type") + for r in reqs: + r.library_id = lib.id + session.commit() + return {"updated": len(reqs)} + + +@router.delete("/bulk") +def delete_bulk(data: BulkIdsIn, background: BackgroundTasks, + session: Session = Depends(get_session)): + reqs = session.scalars(select(BookRequest).where(BookRequest.id.in_(data.ids))).all() + rows = [record(session, "deleted", r, source=r.imported_path) for r in reqs] + for r in reqs: + session.delete(r) + session.commit() + connect.schedule(background, rows) + return {"deleted": len(reqs)} + + @router.put("/{request_id}", response_model=RequestOut) def update_request(request_id: int, data: RequestIn, session: Session = Depends(get_session)): req = session.get(BookRequest, request_id)