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:
@@ -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
|
||||
Reference in New Issue
Block a user