Candidates: inline rename (fix a name typo without reject + re-add)
A staged candidate could only be renamed by rejecting and re-adding it, which
churns the queue and discards the preview just to fix a typo. Add an inline
Rename on each candidate: a "Rename" pill swaps the name for an input
(Enter saves · Esc cancels), POST /api/admin/candidates/{id}/rename →
sources.rename_candidate(). Empty clears the name (promote then derives one
from the feed host). Preview is preserved; the fixed name carries into promotion.
Tests: test_candidate_rename (rename in place keeps preview, promotes with the
new name, gated + 404). 225 pytest + 11 vitest green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -444,6 +444,10 @@ class CandidatePromoteBody(BaseModel):
|
||||
poll_interval_minutes: int = 180
|
||||
|
||||
|
||||
class CandidateRenameBody(BaseModel):
|
||||
name: str = ""
|
||||
|
||||
|
||||
class DigestBody(BaseModel):
|
||||
enabled: bool = True
|
||||
|
||||
@@ -1197,6 +1201,16 @@ def create_app() -> FastAPI:
|
||||
row = sources.save_candidate(conn, url, preview=preview)
|
||||
return _candidate_dict(row)
|
||||
|
||||
@app.post("/api/admin/candidates/{cid}/rename")
|
||||
def admin_candidate_rename(cid: int, body: CandidateRenameBody, request: Request) -> dict:
|
||||
with get_conn() as conn:
|
||||
_require_admin(conn, request)
|
||||
try:
|
||||
row = sources.rename_candidate(conn, cid, (body.name or "").strip() or None)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="candidate not found")
|
||||
return _candidate_dict(row)
|
||||
|
||||
@app.post("/api/admin/candidates/{cid}/promote")
|
||||
def admin_candidate_promote(cid: int, body: CandidatePromoteBody, request: Request) -> dict:
|
||||
with get_conn() as conn:
|
||||
|
||||
@@ -153,6 +153,19 @@ def list_candidates(conn: sqlite3.Connection, status: str | None = None) -> list
|
||||
return conn.execute("SELECT * FROM source_candidates ORDER BY updated_at DESC").fetchall()
|
||||
|
||||
|
||||
def rename_candidate(conn: sqlite3.Connection, candidate_id: int, name: str | None) -> sqlite3.Row:
|
||||
"""Fix a staged candidate's display name without re-fetching it. An empty
|
||||
name clears it (promote then derives one from the feed host)."""
|
||||
cur = conn.execute(
|
||||
"UPDATE source_candidates SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
(name or None, candidate_id),
|
||||
)
|
||||
conn.commit()
|
||||
if cur.rowcount == 0:
|
||||
raise ValueError(f"no candidate with id {candidate_id}")
|
||||
return conn.execute("SELECT * FROM source_candidates WHERE id = ?", (candidate_id,)).fetchone()
|
||||
|
||||
|
||||
def reject_candidate(conn: sqlite3.Connection, candidate_id: int) -> bool:
|
||||
cur = conn.execute(
|
||||
"UPDATE source_candidates SET status = 'rejected', updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
|
||||
Reference in New Issue
Block a user