mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
feat(candidates): список отобранных между кандидатами и опубликованными
- новый статус selected; в кандидатах только «⭐ Отобрать» (single+bulk) - /selected — отдельный список: вся обработка (классификация/переписывание/комментарий/публикация) - классификация только по кнопке (токены на кандидатов не тратятся) - рядом с оригиналом — поле «✍️ Мой пост» (rewritten_text всегда виден), туда ложится черновик «Переписать» - все обработчики учитывают src (candidates|selected) для редиректов
This commit is contained in:
+128
-33
@@ -141,8 +141,9 @@ def _row(r):
|
||||
return _Row(dict(r)) if r is not None else None
|
||||
|
||||
|
||||
def _cand_back(group_by: str = "", direction: str = "", own: str = "", q: str = "", **extra):
|
||||
"""URL возврата в /candidates с сохранением группировки и фильтров."""
|
||||
def _cand_back(group_by: str = "", direction: str = "", own: str = "", q: str = "",
|
||||
src: str = "candidates", **extra):
|
||||
"""URL возврата в список (src: candidates|selected) с сохранением группировки и фильтров."""
|
||||
parts = []
|
||||
if group_by:
|
||||
parts.append(f"group_by={quote(group_by)}")
|
||||
@@ -155,16 +156,17 @@ def _cand_back(group_by: str = "", direction: str = "", own: str = "", q: str =
|
||||
for k, v in extra.items():
|
||||
if v:
|
||||
parts.append(f"{k}={quote(str(v))}")
|
||||
return "/candidates?" + "&".join(parts) if parts else "/candidates"
|
||||
return (f"/{src}?" + "&".join(parts)) if parts else f"/{src}"
|
||||
|
||||
|
||||
STATUS_LABELS = {
|
||||
"new": "💎 Новые",
|
||||
"selected": "⭐ Отобранные",
|
||||
"rejected": "🗑 Отклонённые",
|
||||
"published": "✅ Опубликованные",
|
||||
"": "Все",
|
||||
}
|
||||
STATUS_COLORS = {"new": "success", "rejected": "danger", "published": "primary"}
|
||||
STATUS_COLORS = {"new": "success", "selected": "warning", "rejected": "danger", "published": "primary"}
|
||||
|
||||
|
||||
def _group_label(group_by: str, key: str) -> str:
|
||||
@@ -331,6 +333,59 @@ def candidates(request: Request, direction: str = "", status: str = "", own: str
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
@app.get("/selected", response_class=HTMLResponse)
|
||||
def selected(request: Request, direction: str = "", own: str = "", q: str = "",
|
||||
group_by: str = "date", selected: int = 0, error: str = ""):
|
||||
"""Отобранные: только status='selected' (внешние). Полная обработка постов."""
|
||||
_require_auth(request)
|
||||
conn = _db()
|
||||
# отобранные = внешние посты со статусом selected
|
||||
grouped, counts, _ = _fetch_candidates(conn, direction, "selected", own, q, group_by)
|
||||
counts = {}
|
||||
sel = None
|
||||
if grouped:
|
||||
sel = grouped[0]["posts"][0]
|
||||
if selected:
|
||||
for g in grouped:
|
||||
for it in g["posts"]:
|
||||
if it["id"] == selected:
|
||||
sel = it
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
if sel is not None and sel["id"]:
|
||||
conn.execute("UPDATE posts SET is_read=1 WHERE id=? AND is_read=0", (sel["id"],))
|
||||
sel["is_read"] = 1
|
||||
conn.commit()
|
||||
conn.close()
|
||||
qs = []
|
||||
for k, v in (("direction", direction), ("own", own), ("q", q)):
|
||||
if v:
|
||||
qs.append(f"{k}={quote(v)}")
|
||||
if not selected:
|
||||
selected = sel["id"] if sel else 0
|
||||
filters_qs = "&".join(qs)
|
||||
html = tpl.get_template("selected.html").render(
|
||||
posts=grouped, counts=counts, status="selected", direction=direction, own=own,
|
||||
q=q, group_by=group_by, selected=sel, error=error, filters_qs=filters_qs,
|
||||
directions=DIRECTIONS, STATUS_LABELS=STATUS_LABELS, STATUS_COLORS=STATUS_COLORS,
|
||||
)
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
@app.post("/posts/{post_id}/select")
|
||||
def post_select(post_id: int, request: Request,
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form("")):
|
||||
"""Отобрать кандидата (new → selected). Затем переходим в отобранные."""
|
||||
_require_auth(request)
|
||||
conn = _db()
|
||||
conn.execute("UPDATE posts SET status='selected' WHERE id=? AND status='new' AND is_own=0", (post_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src="selected", selected=post_id), status_code=302)
|
||||
|
||||
|
||||
@app.post("/candidates/select")
|
||||
def candidates_select(request: Request, id: int = Form(...), direction: str = Form(""),
|
||||
status: str = Form(""), own: str = Form(""), q: str = Form(""), group_by: str = Form("date")):
|
||||
@@ -346,23 +401,30 @@ 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(default_factory=list),
|
||||
direction: str = Form(""), status: str = Form(""), own: str = Form(""),
|
||||
q: str = Form(""), group_by: str = Form("date")):
|
||||
"""Bulk-операции: approve/reject/reject-old/process по отмеченным ids."""
|
||||
q: str = Form(""), group_by: str = Form("date"), src: str = Form("candidates")):
|
||||
"""Bulk-операции: select/reject/reject-old/process/approve по отмеченным ids.
|
||||
src — откуда вызывались (candidates|selected), туда и возвращаемся."""
|
||||
_require_auth(request)
|
||||
back = f"/candidates?group_by={group_by}"
|
||||
back = f"/{src}?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=back + "&error=no_selection", status_code=302)
|
||||
conn = _db()
|
||||
ph = ",".join("?" * len(ids))
|
||||
ph = ", ".join("?" * len(ids))
|
||||
rows = conn.execute(f"SELECT * FROM posts WHERE id IN ({ph})", ids).fetchall()
|
||||
if action == "select":
|
||||
# отбор кандидатов (new → selected)
|
||||
conn.execute("UPDATE posts SET status='selected' WHERE id IN ({}) AND status='new' AND is_own=0".format(ph), ids)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src="selected"), status_code=302)
|
||||
if action == "reject":
|
||||
conn.execute(f"UPDATE posts SET status='rejected' WHERE id IN ({ph})", ids)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# остаёмся на кандидатах (текущая группировка), не уходим в отклонённые
|
||||
# остаёмся в текущем списке (кандидаты/отобранные), не уходим в отклонённые
|
||||
return RedirectResponse(url=back, status_code=302)
|
||||
if action == "reject-old":
|
||||
# все выбранные, кроме самых свежих (по 1 от источника) — «почистить старьё»
|
||||
@@ -372,11 +434,10 @@ def candidates_bulk(request: Request, action: str = Form(...), ids: list[int] =
|
||||
keep.setdefault(key, r["id"])
|
||||
kill = [r["id"] for r in rows if r["id"] not in keep.values()]
|
||||
if kill:
|
||||
ph2 = ",".join("?" * len(kill))
|
||||
ph2 = ", ".join("?" * len(kill))
|
||||
conn.execute(f"UPDATE posts SET status='rejected' WHERE id IN ({ph2})", kill)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# остаёмся на кандидатах (текущая группировка), не уходим в отклонённые
|
||||
return RedirectResponse(url=back, status_code=302)
|
||||
conn.close()
|
||||
if action == "process":
|
||||
@@ -390,8 +451,37 @@ def candidates_bulk(request: Request, action: str = Form(...), ids: list[int] =
|
||||
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=f"/{src}?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)
|
||||
if action == "rewrite":
|
||||
# сгенерировать черновик своего поста для каждого выбранного
|
||||
errs = []
|
||||
done = 0
|
||||
for r in rows:
|
||||
try:
|
||||
from classifier.classify import call_ollama
|
||||
d = dict(r)
|
||||
text_ = (d.get("text") or "")[:2000]
|
||||
prompt = (
|
||||
"Ты — редактор новостей. Перепиши новость СВОИМИ словами от первого лица\n"
|
||||
"(как автор канала «Дед в АйТи»), без копипасты и без канцелярита. Сохрани смысл,\n"
|
||||
"факты и ссылку на оригинал. Верни ТОЛЬКО текст пересказа, без пояснений.\n\n"
|
||||
f"Оригинал:\n{text_}"
|
||||
)
|
||||
res = call_ollama(prompt, 0, 0, str(d.get("direction") or ""))
|
||||
draft = (res.get("text") or res.get("content") or "").strip() or res.get("summary") or ""
|
||||
if draft:
|
||||
conn = _db()
|
||||
conn.execute("UPDATE posts SET rewritten_text=? WHERE id=?", (draft, int(r["id"])))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
done += 1
|
||||
else:
|
||||
errs.append(f"{r['id']}(empty)")
|
||||
except Exception as e:
|
||||
errs.append(f"{r['id']}({e})")
|
||||
errq = ("&error=rewrite_partial:" + ",".join(map(str, errs))[:120]) if errs else ""
|
||||
return RedirectResponse(url=back + "&error=rewritten:" + str(done) + errq, status_code=302)
|
||||
# approve — каждый через существующую логику (публикация + бандл)
|
||||
errs = []
|
||||
for r in rows:
|
||||
@@ -401,49 +491,52 @@ def candidates_bulk(request: Request, action: str = Form(...), ids: list[int] =
|
||||
if err:
|
||||
errs.append(f"{r['id']}({err})")
|
||||
if errs:
|
||||
return RedirectResponse(url=f"/candidates?error=bulk_partial:{','.join(map(str,errs))[:120]}&group_by={group_by}", status_code=302)
|
||||
return RedirectResponse(url=f"/{src}?error=bulk_partial:{','.join(map(str,errs))[:120]}&group_by={group_by}", status_code=302)
|
||||
return RedirectResponse(url="/published", status_code=302)
|
||||
|
||||
|
||||
@app.post("/posts/{post_id}/rewrite-save")
|
||||
async def post_rewrite_save(post_id: int, request: Request,
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form("")):
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form(""),
|
||||
src: str = Form("candidates")):
|
||||
"""Сохраняет отредактированный пользователем пересказ (rewritten_text)."""
|
||||
_require_auth(request)
|
||||
conn = _db()
|
||||
p = conn.execute("SELECT id FROM posts WHERE id=?", (post_id,)).fetchone()
|
||||
if not p:
|
||||
conn.close()
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, error="notfound"), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src, error="notfound"), status_code=302)
|
||||
form = await request.form()
|
||||
draft = str(form.get("rewritten_text") or "").strip()
|
||||
conn.execute("UPDATE posts SET rewritten_text=? WHERE id=?", (draft or None, post_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src, selected=post_id), status_code=302)
|
||||
|
||||
|
||||
@app.post("/posts/{post_id}/comment")
|
||||
def post_comment(post_id: int, request: Request, comment: str = Form(""),
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form("")):
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form(""),
|
||||
src: str = Form("candidates")):
|
||||
_require_auth(request)
|
||||
conn = _db()
|
||||
conn.execute("UPDATE posts SET comment=? WHERE id=?", (comment.strip(), post_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src, selected=post_id), status_code=302)
|
||||
|
||||
|
||||
@app.post("/posts/{post_id}/reclassify")
|
||||
def post_reclassify(post_id: int, request: Request,
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form("")):
|
||||
"""Переклассификация кандидата локальной моделью (qwen)."""
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form(""),
|
||||
src: str = Form("candidates")):
|
||||
"""Переклассификация локальной моделью (qwen)."""
|
||||
_require_auth(request)
|
||||
try:
|
||||
impl_reclassify(post_id)
|
||||
except Exception as e:
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id, error=f"reclassify:{e}"), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src, selected=post_id, error=f"reclassify:{e}"), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src, selected=post_id), status_code=302)
|
||||
|
||||
|
||||
def impl_reclassify(post_id: int):
|
||||
@@ -465,14 +558,15 @@ def impl_reclassify(post_id: int):
|
||||
|
||||
@app.post("/posts/{post_id}/rewrite")
|
||||
def post_rewrite(post_id: int, request: Request,
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form("")):
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form(""),
|
||||
src: str = Form("candidates")):
|
||||
_require_auth(request)
|
||||
from classifier.classify import call_ollama
|
||||
conn = _db()
|
||||
p = conn.execute("SELECT * FROM posts WHERE id=?", (post_id,)).fetchone()
|
||||
if not p:
|
||||
conn.close()
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, error="notfound"), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src, error="notfound"), status_code=302)
|
||||
d = dict(p)
|
||||
text_ = (d.get("text") or "")[:2000]
|
||||
prompt = (
|
||||
@@ -488,11 +582,11 @@ def post_rewrite(post_id: int, request: Request,
|
||||
draft = r.get("summary") or ""
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id, error=f"rewrite:{e}"), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src, selected=post_id, error=f"rewrite:{e}"), status_code=302)
|
||||
conn.execute("UPDATE posts SET rewritten_text=? WHERE id=?", (draft, post_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src, selected=post_id), status_code=302)
|
||||
|
||||
|
||||
@app.get("/published", response_class=HTMLResponse)
|
||||
@@ -574,7 +668,8 @@ def impl_approve(post_id: int, comment: str = "", dirs_selected: list[str] | Non
|
||||
|
||||
@app.post("/posts/{post_id}/approve")
|
||||
async def approve(post_id: int, request: Request,
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form("")):
|
||||
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form(""),
|
||||
src: str = Form("candidates")):
|
||||
"""Подтверждение черновика → публикация в TG (fan-out по направлениям) + создание бандла + статус published."""
|
||||
_require_auth(request)
|
||||
# направления fan-out: из формы (чекбоксы) — по умолчанию направления классификации или направление поста
|
||||
@@ -588,22 +683,22 @@ async def approve(post_id: int, request: Request,
|
||||
pass
|
||||
err = impl_approve(post_id, comment, dirs_selected)
|
||||
if err:
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id, error=err), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src, selected=post_id, error=err), status_code=302)
|
||||
# после публикации остаёмся в своей группировке, смотрим опубликованные
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, status="published"), status_code=302)
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src, status="published"), status_code=302)
|
||||
|
||||
|
||||
@app.post("/posts/{post_id}/reject")
|
||||
def reject(post_id: int, request: Request, group_by: str = Form(""), direction: str = Form(""),
|
||||
own: str = Form(""), q: str = Form("")):
|
||||
own: str = Form(""), q: str = Form(""), src: str = Form("candidates")):
|
||||
_require_auth(request)
|
||||
conn = _db()
|
||||
conn.execute("UPDATE posts SET status='rejected' WHERE id=?", (post_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# после отклонения остаёмся на странице кандидатов (та же группировка/фильтры),
|
||||
# чтобы продолжить отбор; список отклонённых — через фильтр «🗑 Отклонённые»
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q), status_code=302)
|
||||
# после отклонения остаёмся на странице, откуда пришли (кандидаты/отобранные),
|
||||
# в той же группировке/фильтрах; список отклонённых — через фильтр «🗑 Отклонённые»
|
||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src=src), status_code=302)
|
||||
|
||||
|
||||
@app.get("/metrics", response_class=HTMLResponse)
|
||||
|
||||
Reference in New Issue
Block a user