mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
fix(candidates): сохранять группировку после действий, чинить пустой ?status=rejected
- _fetch_candidates: жёсткое status='new' только при пустом status; явный статус (rejected/published) — единственный фильтр статуса → ?status=rejected больше не даёт пустой список - формы approve/reclassify/rewrite/reject/comment: скрытые поля group_by/direction/own/q — контекст страницы - POST-роуты: редирект через _cand_back() сохраняет группировку и фильтры (reject → ?status=rejected&group_by=...; approve → ?status=published&...)
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
# VESTI — Статус
|
# VESTI — Статус
|
||||||
|
|
||||||
Обновлено: 2026-09-14 (кандидаты — только внешние new)
|
Обновлено: 2026-09-14 (кандидаты: сохраняем группировку после действий)
|
||||||
<!--
|
<!--
|
||||||
История обновлений:
|
История обновлений:
|
||||||
|
2026-09-14 — кандидаты: после reject/approve/comment/reclassify/rewrite сохраняется group_by и фильтры (скрытые поля форм + _cand_back); ?status=rejected больше не даёт пустой список (жёсткое status='new' только по умолчанию)
|
||||||
2026-09-14 — кандидаты: в списке только внешние new-посты (is_own=0 AND status='new' в _fetch_candidates); свои посты канала (is_own=1) и published/rejected больше не показываются как кандидаты
|
2026-09-14 — кандидаты: в списке только внешние new-посты (is_own=0 AND status='new' в _fetch_candidates); свои посты канала (is_own=1) и published/rejected больше не показываются как кандидаты
|
||||||
2026-09-14 — фикс published-страницы: заголовок перекрывался fixed-top меню (padding-top 1.5rem→4.5rem в base.html); на карточках добавлен ID поста (#{{p.id}} · направление)
|
2026-09-14 — фикс published-страницы: заголовок перекрывался fixed-top меню (padding-top 1.5rem→4.5rem в base.html); на карточках добавлен ID поста (#{{p.id}} · направление)
|
||||||
2026-09-14 — фикс бага: статус группы в _fetch_candidates брался от первого поста → при group_by=date группа «Ранее на этой неделе» выглядела опубликованной; теперь доминирующий по группе (new > rejected > published)
|
2026-09-14 — фикс бага: статус группы в _fetch_candidates брался от первого поста → при group_by=date группа «Ранее на этой неделе» выглядела опубликованной; теперь доминирующий по группе (new > rejected > published)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
| 2026-09-14 | Фикс: статус группы при group_by=date брался от первого поста → группы с первым published выглядели опубликованными | ✅ закрыта | change fix-date-group-status |
|
| 2026-09-14 | Фикс: статус группы при group_by=date брался от первого поста → группы с первым published выглядели опубликованными | ✅ закрыта | change fix-date-group-status |
|
||||||
| 2026-09-14 | Фикс /published: заголовок перекрывался fixed-top меню; на карточках нет ID поста | ✅ закрыта | change fix-published-page |
|
| 2026-09-14 | Фикс /published: заголовок перекрывался fixed-top меню; на карточках нет ID поста | ✅ закрыта | change fix-published-page |
|
||||||
| 2026-09-14 | Кандидаты: в списке не должно быть одобренных/своих постов; при группировке по дате всё показывалось как «свои» | ✅ закрыта | change candidates-only-external-new (в списке только внешние new: is_own=0 AND status='new') |
|
| 2026-09-14 | Кандидаты: в списке не должно быть одобренных/своих постов; при группировке по дате всё показывалось как «свои» | ✅ закрыта | change candidates-only-external-new (в списке только внешние new: is_own=0 AND status='new') |
|
||||||
|
| 2026-09-14 | Кандидаты: после reject сбрасывается группировка (date→source), ?status=rejected показывает пустой список | ✅ закрыта | change keep-candidates-context (сохранение group_by/фильтров через скрытые поля + _cand_back; явный статус — единственный фильтр) |
|
||||||
| 2026-09-08 | Артефакты change: proposal, design, specs (5), tasks | ✅ закрыта | сессия старта |
|
| 2026-09-08 | Артефакты change: proposal, design, specs (5), tasks | ✅ закрыта | сессия старта |
|
||||||
| 2026-09-08 | PRD.md — требования, архитектура, стек, DoD | ✅ закрыта | сессия старта |
|
| 2026-09-08 | PRD.md — требования, архитектура, стек, DoD | ✅ закрыта | сессия старта |
|
||||||
| 2026-09-08 | Каркас проекта: каталоги, venv, requirements.txt | ✅ закрыта | сессия старта |
|
| 2026-09-08 | Каркас проекта: каталоги, venv, requirements.txt | ✅ закрыта | сессия старта |
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-09-14
|
||||||
|
skip_specs: true
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
## Design
|
||||||
|
|
||||||
|
### 1. `_fetch_candidates` — статус по умолчанию vs явный
|
||||||
|
|
||||||
|
Текущее (строки ~151-166):
|
||||||
|
```python
|
||||||
|
where.append("p.is_own=0")
|
||||||
|
where.append("p.status='new'")
|
||||||
|
...
|
||||||
|
if status and status != "new":
|
||||||
|
where.append("p.status=?")
|
||||||
|
params.append(status)
|
||||||
|
```
|
||||||
|
|
||||||
|
Правка:
|
||||||
|
```python
|
||||||
|
where.append("p.is_own=0")
|
||||||
|
if not status:
|
||||||
|
# по умолчанию — только кандидаты (new); явный статус ниже
|
||||||
|
where.append("p.status='new'")
|
||||||
|
else:
|
||||||
|
# явный статус (new/rejected/published) — единственный фильтр статуса
|
||||||
|
where.append("p.status=?")
|
||||||
|
params.append(status)
|
||||||
|
if direction:
|
||||||
|
where.append("p.direction=?")
|
||||||
|
params.append(direction)
|
||||||
|
if own in ("1", "0"):
|
||||||
|
where.append("p.is_own=?")
|
||||||
|
params.append(int(own))
|
||||||
|
if q:
|
||||||
|
where.append("(p.text LIKE ? OR p.summary LIKE ?)")
|
||||||
|
params += [f"%{q}%", f"%{q}%"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Скрытые поля в формах правой панели
|
||||||
|
|
||||||
|
В `candidates.html` в каждую форму правой панели (approve, reclassify, rewrite,
|
||||||
|
reject, comment) добавить:
|
||||||
|
```html
|
||||||
|
<input type="hidden" name="group_by" value="{{ group_by }}">
|
||||||
|
<input type="hidden" name="direction" value="{{ direction }}">
|
||||||
|
<input type="hidden" name="own" value="{{ own }}">
|
||||||
|
<input type="hidden" name="q" value="{{ q }}">
|
||||||
|
```
|
||||||
|
|
||||||
|
(Переменные `group_by`, `direction`, `own`, `q` уже доступны в контексте рендера
|
||||||
|
роута `/candidates`.)
|
||||||
|
|
||||||
|
### 3. Роуты POST-действий — сохранять контекст
|
||||||
|
|
||||||
|
Общий хелпер для построения URL возврата:
|
||||||
|
```python
|
||||||
|
def _cand_back(group_by="", direction="", own="", q="", **extra):
|
||||||
|
parts = []
|
||||||
|
if group_by:
|
||||||
|
parts.append(f"group_by={group_by}")
|
||||||
|
if direction:
|
||||||
|
parts.append(f"direction={direction}")
|
||||||
|
if own in ("0", "1"):
|
||||||
|
parts.append(f"own={own}")
|
||||||
|
if q:
|
||||||
|
parts.append(f"q={quote(q)}")
|
||||||
|
for k, v in extra.items():
|
||||||
|
if v:
|
||||||
|
parts.append(f"{k}={v}")
|
||||||
|
return "/candidates?" + "&".join(parts) if parts else "/candidates"
|
||||||
|
```
|
||||||
|
|
||||||
|
Применение:
|
||||||
|
- `reject(post_id, request, group_by="", direction="", own="", q="")`:
|
||||||
|
`return RedirectResponse(url=_cand_back(group_by, direction, own, q, status="rejected"), status_code=302)`
|
||||||
|
→ пользователь остаётся в своей группировке, видит список отклонённых.
|
||||||
|
- `comment`/`reclassify`/`rewrite`: `_cand_back(group_by, direction, own, q, selected=post_id)`.
|
||||||
|
- `approve`: после успеха — `_cand_back(group_by, direction, own, q, status="published")`
|
||||||
|
(пользователь видит опубликованные в той же группировке) или `/published`.
|
||||||
|
Решение: approve возвращает в кандидаты с `status=published` в той же группировке,
|
||||||
|
чтобы контекст не терялся. Ошибки — `_cand_back(..., selected=post_id, error=err)`.
|
||||||
|
|
||||||
|
FastAPI: параметры форм объявляются как `group_by: str = Form("")` и т.д.
|
||||||
|
|
||||||
|
## Верификация
|
||||||
|
|
||||||
|
- `openspec validate keep-candidates-context` — чисто.
|
||||||
|
- Юнит: `_fetch_candidates(c,'','rejected','','','date')` → возвращает
|
||||||
|
отклонённые (не пусто); `_fetch_candidates(c,'','','','','date')` → new внешние.
|
||||||
|
- Живой: POST /posts/{id}/reject с form group_by=date → редирект на
|
||||||
|
`/candidates?group_by=date&status=rejected`, список не пуст.
|
||||||
|
- Рестарт vesti-web.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Два бага при работе со списком кандидатов:
|
||||||
|
|
||||||
|
1. **Пустой список при `?status=rejected`.** После фикса
|
||||||
|
`candidates-only-external-new` в `_fetch_candidates` жёстко добавлено
|
||||||
|
`p.status='new'`. Когда пользователь явно выбирает `status=rejected`
|
||||||
|
(или `published`), в WHERE попадают ОБА условия: `p.status='new' AND
|
||||||
|
p.status='rejected'` → выборка всегда пустая, страница показывает
|
||||||
|
«Нет кандидатов по фильтру».
|
||||||
|
|
||||||
|
2. **Сброс группировки после действия.** Роут `/posts/{id}/reject`
|
||||||
|
редиректит на `/candidates?status=rejected` без сохранения `group_by`
|
||||||
|
(и direction/own/q). Пользователь был в группировке «дата» → после
|
||||||
|
отклонения его выбрасывает на `?status=rejected` с дефолтной группировкой
|
||||||
|
«источник». Аналогично approve/reclassify/rewrite/comment редиректят на
|
||||||
|
`?selected={id}` без сохранения группировки.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
1. **`web/app.py`, `_fetch_candidates`:** жёсткое `p.status='new'` добавлять
|
||||||
|
только когда параметр `status` пуст (значение по умолчанию = кандидаты
|
||||||
|
new). Если `status` задан явно (`new`/`rejected`/`published`) — применять
|
||||||
|
его как единственный фильтр статуса. `p.is_own=0` остаётся всегда
|
||||||
|
(свои посты не кандидаты).
|
||||||
|
|
||||||
|
2. **Формы правой панели в `candidates.html`** (approve, reclassify, rewrite,
|
||||||
|
reject, comment): добавить скрытые поля `group_by`, `direction`, `own`, `q`
|
||||||
|
со значениями текущей страницы.
|
||||||
|
|
||||||
|
3. **Роуты POST-действий** (approve, reclassify, rewrite, reject, comment):
|
||||||
|
читать `group_by`/`direction`/`own`/`q` из формы и строить редирект с этими
|
||||||
|
параметрами, чтобы пользователь остался в той же группировке/фильтре.
|
||||||
|
|
||||||
|
## Why Not
|
||||||
|
|
||||||
|
- Не парсить `Referer`: хрупко и небезопасно.
|
||||||
|
- Не возвращать на `/published` после approve без сохранения контекста:
|
||||||
|
пользователь работает в списке кандидатов и хочет остаться в нём.
|
||||||
|
- Не менять SQL-структуру counts: counts считаются по внешним постам и уже
|
||||||
|
корректны.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Файлы: `web/app.py`, `web/templates/candidates.html`.
|
||||||
|
- Данные: без миграций БД.
|
||||||
|
- Сервис: `vesti-web` (:8400) — перезапуск.
|
||||||
|
- Rollback: откатить правки, перезапустить.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# keep-candidates-context
|
||||||
|
|
||||||
|
- [x] Создан OpenSpec change (proposal/design) — skip_specs: true
|
||||||
|
- [x] _fetch_candidates: жёсткое status='new' только при пустом status; явный статус — единственный фильтр
|
||||||
|
- [x] candidates.html: скрытые поля group_by/direction/own/q в формах approve/reclassify/rewrite/reject/comment
|
||||||
|
- [x] Роуты POST-действий: редирект сохраняет group_by и фильтры
|
||||||
|
- [x] openspec validate keep-candidates-context — чисто
|
||||||
|
- [x] Юнит: status=rejected → 4 поста (не пусто); по умолчанию → 163 new внешних
|
||||||
|
- [x] Живой: hidden-поля в формах; ?status=rejected → 4 поста, нет «Нет кандидатов»
|
||||||
|
- [x] Рестарт vesti-web
|
||||||
|
- [x] Обновить STATUS.md / TODO.md
|
||||||
|
- [x] Git push в gitverse
|
||||||
+43
-19
@@ -120,6 +120,23 @@ def _row(r):
|
|||||||
return _Row(dict(r)) if r is not None else None
|
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 с сохранением группировки и фильтров."""
|
||||||
|
parts = []
|
||||||
|
if group_by:
|
||||||
|
parts.append(f"group_by={quote(group_by)}")
|
||||||
|
if direction:
|
||||||
|
parts.append(f"direction={quote(direction)}")
|
||||||
|
if own in ("0", "1"):
|
||||||
|
parts.append(f"own={own}")
|
||||||
|
if q:
|
||||||
|
parts.append(f"q={quote(q)}")
|
||||||
|
for k, v in extra.items():
|
||||||
|
if v:
|
||||||
|
parts.append(f"{k}={quote(str(v))}")
|
||||||
|
return "/candidates?" + "&".join(parts) if parts else "/candidates"
|
||||||
|
|
||||||
|
|
||||||
STATUS_LABELS = {
|
STATUS_LABELS = {
|
||||||
"new": "💎 Новые",
|
"new": "💎 Новые",
|
||||||
"rejected": "🗑 Отклонённые",
|
"rejected": "🗑 Отклонённые",
|
||||||
@@ -155,15 +172,16 @@ def _fetch_candidates(conn, direction: str, status: str, own: str, q: str, group
|
|||||||
# собственного канала, управляется отдельно (fan-out); published/rejected —
|
# собственного канала, управляется отдельно (fan-out); published/rejected —
|
||||||
# уже решённые посты, им не место в очереди кандидатов.
|
# уже решённые посты, им не место в очереди кандидатов.
|
||||||
where.append("p.is_own=0")
|
where.append("p.is_own=0")
|
||||||
|
if not status:
|
||||||
|
# по умолчанию — только кандидаты (new); явный статус ниже
|
||||||
where.append("p.status='new'")
|
where.append("p.status='new'")
|
||||||
|
else:
|
||||||
|
# явный статус (new/rejected/published) — единственный фильтр статуса
|
||||||
|
where.append("p.status=?")
|
||||||
|
params.append(status)
|
||||||
if direction:
|
if direction:
|
||||||
where.append("p.direction=?")
|
where.append("p.direction=?")
|
||||||
params.append(direction)
|
params.append(direction)
|
||||||
if status and status != "new":
|
|
||||||
# Явный фильтр статуса (rejected/published) — просмотр решённых;
|
|
||||||
# для 'new' условие уже добавлено выше.
|
|
||||||
where.append("p.status=?")
|
|
||||||
params.append(status)
|
|
||||||
if own in ("1", "0"):
|
if own in ("1", "0"):
|
||||||
where.append("p.is_own=?")
|
where.append("p.is_own=?")
|
||||||
params.append(int(own))
|
params.append(int(own))
|
||||||
@@ -360,24 +378,26 @@ def candidates_bulk(request: Request, action: str = Form(...), ids: list[int] =
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/posts/{post_id}/comment")
|
@app.post("/posts/{post_id}/comment")
|
||||||
def post_comment(post_id: int, request: Request, comment: str = Form("")):
|
def post_comment(post_id: int, request: Request, comment: str = Form(""),
|
||||||
|
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form("")):
|
||||||
_require_auth(request)
|
_require_auth(request)
|
||||||
conn = _db()
|
conn = _db()
|
||||||
conn.execute("UPDATE posts SET comment=? WHERE id=?", (comment.strip(), post_id))
|
conn.execute("UPDATE posts SET comment=? WHERE id=?", (comment.strip(), post_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return RedirectResponse(url=f"/candidates?selected={post_id}", status_code=302)
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id), status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/posts/{post_id}/reclassify")
|
@app.post("/posts/{post_id}/reclassify")
|
||||||
def post_reclassify(post_id: int, request: Request):
|
def post_reclassify(post_id: int, request: Request,
|
||||||
|
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form("")):
|
||||||
"""Переклассификация кандидата локальной моделью (qwen)."""
|
"""Переклассификация кандидата локальной моделью (qwen)."""
|
||||||
_require_auth(request)
|
_require_auth(request)
|
||||||
try:
|
try:
|
||||||
impl_reclassify(post_id)
|
impl_reclassify(post_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return RedirectResponse(url=f"/candidates?selected={post_id}&error=reclassify:{e}", status_code=302)
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id, error=f"reclassify:{e}"), status_code=302)
|
||||||
return RedirectResponse(url=f"/candidates?selected={post_id}", status_code=302)
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id), status_code=302)
|
||||||
|
|
||||||
|
|
||||||
def impl_reclassify(post_id: int):
|
def impl_reclassify(post_id: int):
|
||||||
@@ -398,14 +418,15 @@ def impl_reclassify(post_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/posts/{post_id}/rewrite")
|
@app.post("/posts/{post_id}/rewrite")
|
||||||
def post_rewrite(post_id: int, request: Request):
|
def post_rewrite(post_id: int, request: Request,
|
||||||
|
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form("")):
|
||||||
_require_auth(request)
|
_require_auth(request)
|
||||||
from classifier.classify import call_ollama
|
from classifier.classify import call_ollama
|
||||||
conn = _db()
|
conn = _db()
|
||||||
p = conn.execute("SELECT * FROM posts WHERE id=?", (post_id,)).fetchone()
|
p = conn.execute("SELECT * FROM posts WHERE id=?", (post_id,)).fetchone()
|
||||||
if not p:
|
if not p:
|
||||||
conn.close()
|
conn.close()
|
||||||
return RedirectResponse(url="/candidates?error=notfound", status_code=302)
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, error="notfound"), status_code=302)
|
||||||
d = dict(p)
|
d = dict(p)
|
||||||
text_ = (d.get("text") or "")[:2000]
|
text_ = (d.get("text") or "")[:2000]
|
||||||
prompt = (
|
prompt = (
|
||||||
@@ -421,11 +442,11 @@ def post_rewrite(post_id: int, request: Request):
|
|||||||
draft = r.get("summary") or ""
|
draft = r.get("summary") or ""
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.close()
|
conn.close()
|
||||||
return RedirectResponse(url=f"/candidates?selected={post_id}&error=rewrite:{e}", status_code=302)
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id, error=f"rewrite:{e}"), status_code=302)
|
||||||
conn.execute("UPDATE posts SET rewritten_text=? WHERE id=?", (draft, post_id))
|
conn.execute("UPDATE posts SET rewritten_text=? WHERE id=?", (draft, post_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return RedirectResponse(url=f"/candidates?selected={post_id}", status_code=302)
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id), status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/published", response_class=HTMLResponse)
|
@app.get("/published", response_class=HTMLResponse)
|
||||||
@@ -506,7 +527,8 @@ def impl_approve(post_id: int, comment: str = "", dirs_selected: list[str] | Non
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/posts/{post_id}/approve")
|
@app.post("/posts/{post_id}/approve")
|
||||||
async def approve(post_id: int, request: Request):
|
async def approve(post_id: int, request: Request,
|
||||||
|
group_by: str = Form(""), direction: str = Form(""), own: str = Form(""), q: str = Form("")):
|
||||||
"""Подтверждение черновика → публикация в TG (fan-out по направлениям) + создание бандла + статус published."""
|
"""Подтверждение черновика → публикация в TG (fan-out по направлениям) + создание бандла + статус published."""
|
||||||
_require_auth(request)
|
_require_auth(request)
|
||||||
# направления fan-out: из формы (чекбоксы) — по умолчанию направления классификации или направление поста
|
# направления fan-out: из формы (чекбоксы) — по умолчанию направления классификации или направление поста
|
||||||
@@ -520,18 +542,20 @@ async def approve(post_id: int, request: Request):
|
|||||||
pass
|
pass
|
||||||
err = impl_approve(post_id, comment, dirs_selected)
|
err = impl_approve(post_id, comment, dirs_selected)
|
||||||
if err:
|
if err:
|
||||||
return RedirectResponse(url=f"/candidates?selected={post_id}&error={err}", status_code=302)
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, selected=post_id, error=err), status_code=302)
|
||||||
return RedirectResponse(url="/published", status_code=302)
|
# после публикации остаёмся в своей группировке, смотрим опубликованные
|
||||||
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, status="published"), status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/posts/{post_id}/reject")
|
@app.post("/posts/{post_id}/reject")
|
||||||
def reject(post_id: int, request: Request):
|
def reject(post_id: int, request: Request, group_by: str = Form(""), direction: str = Form(""),
|
||||||
|
own: str = Form(""), q: str = Form("")):
|
||||||
_require_auth(request)
|
_require_auth(request)
|
||||||
conn = _db()
|
conn = _db()
|
||||||
conn.execute("UPDATE posts SET status='rejected' WHERE id=?", (post_id,))
|
conn.execute("UPDATE posts SET status='rejected' WHERE id=?", (post_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return RedirectResponse(url="/candidates?status=rejected", status_code=302)
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, status="rejected"), status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/metrics", response_class=HTMLResponse)
|
@app.get("/metrics", response_class=HTMLResponse)
|
||||||
|
|||||||
@@ -138,23 +138,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 d-flex flex-wrap gap-2">
|
<div class="mb-3 d-flex flex-wrap gap-2">
|
||||||
|
{% set hid %}<input type="hidden" name="group_by" value="{{ group_by }}"><input type="hidden" name="direction" value="{{ direction }}"><input type="hidden" name="own" value="{{ own }}"><input type="hidden" name="q" value="{{ q }}">{% endset %}
|
||||||
{% if selected.status != 'published' %}
|
{% if selected.status != 'published' %}
|
||||||
<form method="post" action="/posts/{{ selected.id }}/approve">
|
<form method="post" action="/posts/{{ selected.id }}/approve">
|
||||||
|
{{ hid }}
|
||||||
<button class="btn btn-success" type="submit">✅ Опубликовать</button>
|
<button class="btn btn-success" type="submit">✅ Опубликовать</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if selected.status != 'published' %}
|
{% if selected.status != 'published' %}
|
||||||
<form method="post" action="/posts/{{ selected.id }}/reclassify">
|
<form method="post" action="/posts/{{ selected.id }}/reclassify">
|
||||||
|
{{ hid }}
|
||||||
<button class="btn btn-info" type="submit">🤖 Обработать моделью</button>
|
<button class="btn btn-info" type="submit">🤖 Обработать моделью</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if selected.status != 'published' %}
|
{% if selected.status != 'published' %}
|
||||||
<form method="post" action="/posts/{{ selected.id }}/rewrite">
|
<form method="post" action="/posts/{{ selected.id }}/rewrite">
|
||||||
|
{{ hid }}
|
||||||
<button class="btn btn-outline-primary" type="submit">✍️ Переписать</button>
|
<button class="btn btn-outline-primary" type="submit">✍️ Переписать</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if selected.status != 'rejected' and selected.status != 'published' %}
|
{% if selected.status != 'rejected' and selected.status != 'published' %}
|
||||||
<form method="post" action="/posts/{{ selected.id }}/reject">
|
<form method="post" action="/posts/{{ selected.id }}/reject">
|
||||||
|
{{ hid }}
|
||||||
<button class="btn btn-outline-danger" type="submit">🚫 Отклонить</button>
|
<button class="btn btn-outline-danger" type="submit">🚫 Отклонить</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -162,6 +167,7 @@
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<form method="post" action="/posts/{{ selected.id }}/comment">
|
<form method="post" action="/posts/{{ selected.id }}/comment">
|
||||||
|
{{ hid }}
|
||||||
<label class="form-label small text-muted">💬 Мой комментарий (будет первым в канале):</label>
|
<label class="form-label small text-muted">💬 Мой комментарий (будет первым в канале):</label>
|
||||||
<textarea class="form-control form-control-sm" name="comment" rows="2"
|
<textarea class="form-control form-control-sm" name="comment" rows="2"
|
||||||
placeholder="Комментарий/анонс…">{{ selected.comment or '' }}</textarea>
|
placeholder="Комментарий/анонс…">{{ selected.comment or '' }}</textarea>
|
||||||
|
|||||||
Reference in New Issue
Block a user