candidates UI: full width, action and bulk buttons

- base.html: container -> container-fluid (nav and content full width)
- Right pane: large buttons Publish / Process with local model /
  Rewrite / Reject (for new candidates)
- Left pane bottom: bulk operations panel with 4 visible buttons
  (Publish / Process / Reject / Stale) instead of hidden select
- bulk: action 'process' reclassifies selected with local model
  (impl_reclassify extracted); ids optional (empty selection ->
  no_selection redirect instead of 422); redirect preserves filters
- Tests: full width, buttons present, bulk empty/process, published/metrics — green
This commit is contained in:
kpa39l
2026-09-13 21:01:30 +00:00
parent 60617de0cb
commit 0e487f672a
3 changed files with 69 additions and 42 deletions
+35 -13
View File
@@ -280,13 +280,17 @@ def candidates_select(request: Request, id: int = Form(...), direction: str = Fo
@app.post("/candidates/bulk")
def candidates_bulk(request: Request, action: str = Form(...), ids: list[int] = Form(...),
def candidates_bulk(request: Request, action: str = Form(...), ids: list[int] = Form(default_factory=list),
direction: str = Form(""), status: str = Form(""), own: str = Form(""),
q: str = Form(""), group_by: str = Form("source")):
"""Bulk-операции: approve/reject/reject-old по отмеченным ids."""
"""Bulk-операции: approve/reject/reject-old/process по отмеченным ids."""
_require_auth(request)
back = f"/candidates?group_by={group_by}"
for k, v in (("direction", direction), ("status", status), ("own", own), ("q", q)):
if v:
back += f"&{k}={quote(v)}"
if not ids:
return RedirectResponse(url="/candidates?error=no_selection", status_code=302)
return RedirectResponse(url=back + "&error=no_selection", status_code=302)
conn = _db()
ph = ",".join("?" * len(ids))
rows = conn.execute(f"SELECT * FROM posts WHERE id IN ({ph})", ids).fetchall()
@@ -294,7 +298,7 @@ def candidates_bulk(request: Request, action: str = Form(...), ids: list[int] =
conn.execute(f"UPDATE posts SET status='rejected' WHERE id IN ({ph})", ids)
conn.commit()
conn.close()
return RedirectResponse(url=f"/candidates?status=rejected&group_by={group_by}", status_code=302)
return RedirectResponse(url=back + "&status=rejected", status_code=302)
if action == "reject-old":
# все выбранные, кроме самых свежих (по 1 от источника) — «почистить старьё»
keep = {}
@@ -307,9 +311,22 @@ def candidates_bulk(request: Request, action: str = Form(...), ids: list[int] =
conn.execute(f"UPDATE posts SET status='rejected' WHERE id IN ({ph2})", kill)
conn.commit()
conn.close()
return RedirectResponse(url=f"/candidates?status=rejected&group_by={group_by}", status_code=302)
# approve — каждый через существующую логику (публикация + бандл)
return RedirectResponse(url=back + "&status=rejected", status_code=302)
conn.close()
if action == "process":
# обработка локальной моделью (переклассификация) по выбранным
errs = []
done = 0
for r in rows:
try:
impl_reclassify(int(r["id"]))
done += 1
except Exception as e:
errs.append(f"{r['id']}({e})")
if errs:
return RedirectResponse(url=f"/candidates?error=process_partial:{','.join(map(str,errs))[:120]}&group_by={group_by}", status_code=302)
return RedirectResponse(url=back + "&error=processed:" + str(done), status_code=302)
# approve — каждый через существующую логику (публикация + бандл)
errs = []
for r in rows:
if r["status"] == "published":
@@ -334,25 +351,30 @@ def post_comment(post_id: int, request: Request, comment: str = Form("")):
@app.post("/posts/{post_id}/reclassify")
def post_reclassify(post_id: int, request: Request):
"""Переклассификация кандидата локальной моделью (qwen)."""
_require_auth(request)
try:
impl_reclassify(post_id)
except Exception as e:
return RedirectResponse(url=f"/candidates?selected={post_id}&error=reclassify:{e}", status_code=302)
return RedirectResponse(url=f"/candidates?selected={post_id}", status_code=302)
def impl_reclassify(post_id: int):
"""Перезапуск классификации локальной моделью. Бросает исключение при неудаче."""
from classifier.classify import classify_text
conn = _db()
p = conn.execute("SELECT * FROM posts WHERE id=?", (post_id,)).fetchone()
if not p:
conn.close()
return RedirectResponse(url="/candidates?error=notfound", status_code=302)
try:
res = classify_text(dict(p))
except Exception as e:
conn.close()
return RedirectResponse(url=f"/candidates?selected={post_id}&error=reclassify:{e}", status_code=302)
raise ValueError("notfound")
res = classify_text(dict(p))
conn.execute(
"UPDATE posts SET direction=?, relevance=?, interest=?, summary=?, classified=? WHERE id=?",
(res["direction"], res["relevance"], res["interest"], res["summary"], 1 if res["classified"] else 0, post_id),
)
conn.commit()
conn.close()
return RedirectResponse(url=f"/candidates?selected={post_id}", status_code=302)
@app.post("/posts/{post_id}/rewrite")