mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
selected-layout-stay: отбор остаётся на кандидатах; отобранные — layout на всю высоту (резюме под кнопками, редактор+оригинал в рост, публикация внизу)
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
# VESTI — Статус
|
# VESTI — Статус
|
||||||
|
|
||||||
Обновлено: 2026-09-14 (кандидаты: сохраняем группировку после действий)
|
Обновлено: 2026-09-15 (отбор без ухода со страницы + layout отобранных на всю высоту)
|
||||||
<!--
|
<!--
|
||||||
История обновлений:
|
История обновлений:
|
||||||
|
2026-09-15 — change selected-layout-stay: «⭐ Отобрать» (single+bulk) больше не уводит на /selected — редирект на /candidates с сохранением группировки/фильтров (post_select → src="candidates", bulk select → back). Отобранные: панели на всю высоту окна (calc(100vh - 160px) вместо 82vh), резюме — отдельным блоком сразу под кнопками «Обработать моделью», редактор «Мой пост» + оригинал растянуты на всю оставшуюся высоту, комментарий + «Опубликовать»/«Сохранить комментарий» — в самом низу (border-top). Пустое место внизу убрано.
|
||||||
2026-09-14 — кандидаты: после reject/approve/comment/reclassify/rewrite сохраняется group_by и фильтры (скрытые поля форм + _cand_back); ?status=rejected больше не даёт пустой список (жёсткое status='new' только по умолчанию)
|
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}} · направление)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
skip_specs: true
|
||||||
|
created: 2026-09-15
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# selected-layout-stay
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### 1. `web/app.py` — отбор остаётся на кандидатах
|
||||||
|
|
||||||
|
`post_select()` (`POST /posts/{id}/select`), сейчас:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src="selected", selected=post_id), status_code=302)
|
||||||
|
```
|
||||||
|
|
||||||
|
Стало:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src="candidates"), status_code=302)
|
||||||
|
```
|
||||||
|
|
||||||
|
`candidates_bulk()` `action == "select"`, сейчас:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src="selected"), status_code=302)
|
||||||
|
```
|
||||||
|
|
||||||
|
Стало:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return RedirectResponse(url=back, status_code=302)
|
||||||
|
```
|
||||||
|
|
||||||
|
`back` = `/candidates?group_by=<...>&direction=&own=&q=` (src=candidates из
|
||||||
|
скрытого поля формы bulk).
|
||||||
|
|
||||||
|
### 2. `web/templates/selected.html` — правая панель на всю высоту
|
||||||
|
|
||||||
|
Высота панели: `style="height: calc(100vh - 160px);"` (было `82vh`).
|
||||||
|
Левая панель: `max-height: calc(100vh - 160px)` (было `82vh`).
|
||||||
|
|
||||||
|
Порядок блоков в правой панели (flex-колонка):
|
||||||
|
|
||||||
|
1. Заголовок `#{{ selected.id }} …` — `flex-shrink-0` (без изменений).
|
||||||
|
2. Бейджи — `mb-2`, `flex-shrink-0` (без изменений).
|
||||||
|
3. Блок кнопок (reclassify/rewrite/reject) — `flex-shrink-0`
|
||||||
|
(текущий блок kнопок, формы без изменений).
|
||||||
|
4. **Резюме** — новый блок сразу под кнопками:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{% if selected.summary %}
|
||||||
|
<div class="flex-shrink-0 mb-2">
|
||||||
|
<div class="border rounded bg-light p-2 small">
|
||||||
|
<span class="badge bg-secondary">резюме</span> {{ selected.summary }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
(из блока «Оригинальный пост» строка с summary удаляется).
|
||||||
|
|
||||||
|
5. `<hr class="flex-shrink-0 my-2">`.
|
||||||
|
|
||||||
|
6. Редактор — `row g-2 flex-grow-1` со `style="min-height:0;"`:
|
||||||
|
- колонка «Мой пост»: `d-flex flex-column` + `min-height:0`; форма —
|
||||||
|
`d-flex flex-column flex-grow-1` + `min-height:0`; textarea —
|
||||||
|
`flex-grow-1` + `style="min-height:140px; resize:vertical;"` (было
|
||||||
|
`rows="8"`), кнопка «💾 Сохранить» под ним;
|
||||||
|
- колонка «Оригинальный пост»: `d-flex flex-column` + `min-height:0`;
|
||||||
|
контейнер текста/медиа — `flex-grow-1 overflow-auto` +
|
||||||
|
`min-height:0` (было `max-height:300px`).
|
||||||
|
|
||||||
|
7. **Низ (flex-shrink-0)** — блок с `border-top pt-2 mt-2` и approve-формой:
|
||||||
|
поле комментария (`rows="2"`), «✅ Опубликовать», «💾 Сохранить
|
||||||
|
комментарий». Перенесено из середины (was: внутри блока кнопок, `mb-3`).
|
||||||
|
|
||||||
|
`{{ hid }}` (скрытые поля контекста) — используется во всех формах (уже есть).
|
||||||
|
|
||||||
|
### 3. `web/templates/candidates.html` — высоты панелей
|
||||||
|
|
||||||
|
- Левая панель: `max-height: 82vh` → `max-height: calc(100vh - 160px)`.
|
||||||
|
- Правая панель: `height: 82vh` → `height: calc(100vh - 160px)`.
|
||||||
|
|
||||||
|
Остальная вёрстка кандидатов без изменений.
|
||||||
|
|
||||||
|
## Верификация
|
||||||
|
|
||||||
|
- `openspec validate selected-layout-stay` — чисто.
|
||||||
|
- Smoke (curl/TestClient): login → POST `/posts/{id}/select` с
|
||||||
|
`group_by=date` → `Location: /candidates?group_by=date` (БЕЗ `/selected`).
|
||||||
|
- Smoke: POST `/candidates/bulk` action=select → `Location: /candidates?...`.
|
||||||
|
- GET `/selected` — в HTML: `calc(100vh - 160px)`, блок «резюме» сразу после
|
||||||
|
кнопок, approve-форма в конце панели.
|
||||||
|
- Перезапуск: `sudo systemctl restart vesti-web`.
|
||||||
|
- Ручная проверка пользователем.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# selected-layout-stay: отбор без ухода со страницы + удобный layout отобранных
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Два замечания пользователя по веб-интерфейсу:
|
||||||
|
|
||||||
|
1. **«⭐ Отобрать» уводит со страницы кандидатов.** При отметке поста
|
||||||
|
(single-кнопка или bulk) редирект идёт на `/selected` — поток отбора
|
||||||
|
разрывается: после каждого отбора нужно возвращаться назад в очередь
|
||||||
|
кандидатов. Пользователь: «При отметке поста на странице кандидатов не
|
||||||
|
нужно открывать страницу отобранных — остаёмся на странице кандидатов».
|
||||||
|
|
||||||
|
2. **Layout страницы отобранных неудобный:**
|
||||||
|
- поля «Мой пост» и «Оригинальный пост» высотой всего несколько строчек
|
||||||
|
(textarea rows=8, оригинал max-height:300px) — не хватает места для
|
||||||
|
работы с текстом;
|
||||||
|
- резюме оригинального поста спрятано внизу блока оригинала — должно быть
|
||||||
|
под блоком кнопок «Обработать моделью»;
|
||||||
|
- поле комментария и кнопки публикации стоят в середине страницы — должны
|
||||||
|
быть в самом низу;
|
||||||
|
- внизу страницы остаётся пустое место (панели `height: 82vh`, контент не
|
||||||
|
растягивается) — нужно использовать всё окно браузера по высоте.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
### 1. Отбор остаётся на кандидатах (`web/app.py`)
|
||||||
|
|
||||||
|
- `post_select()` (`POST /posts/{id}/select`): редирект после отбора —
|
||||||
|
`_cand_back(group_by, direction, own, q, src="candidates")` вместо
|
||||||
|
`src="selected"` (пост ушёл из очереди, возвращаемся в кандидаты в той же
|
||||||
|
группировке/фильтрах).
|
||||||
|
- `candidates_bulk()` `action == "select"`: редирект на `back` (текущая
|
||||||
|
группировка + фильтры, src=candidates из скрытого поля формы) вместо
|
||||||
|
перехода в `/selected`.
|
||||||
|
|
||||||
|
### 2. Layout отобранных (`web/templates/selected.html`)
|
||||||
|
|
||||||
|
Правая панель перестраивается по вертикали (flex-колонка на всю высоту окна):
|
||||||
|
|
||||||
|
1. Заголовок поста + бейджи (как сейчас).
|
||||||
|
2. Блок кнопок «🤖 Обработать моделью / ✍️ Переписать / 🚫 Отклонить».
|
||||||
|
3. **Резюме** (`selected.summary`) — сразу под кнопками, отдельный блок.
|
||||||
|
4. Разделитель.
|
||||||
|
5. «Мой пост» (textarea) + «Оригинальный пост» (текст+медиа) — **растянуты на
|
||||||
|
всю оставшуюся высоту** (flex-grow-1, min-height:0, оригинал — overflow-auto).
|
||||||
|
6. **В самом низу**: поле «💬 Мой комментарий» + «✅ Опубликовать» /
|
||||||
|
«💾 Сохранить комментарий» (border-top поверх).
|
||||||
|
|
||||||
|
Высота панелей: `82vh` → `calc(100vh - 160px)` (navbar 4.5rem + заголовок +
|
||||||
|
форма фильтров) — страница использует всё окно, пустого места внизу нет.
|
||||||
|
То же для левой панели (`max-height`) и для страницы кандидатов
|
||||||
|
(`web/templates/candidates.html`), чтобы поведение было консистентным.
|
||||||
|
|
||||||
|
## Why Not
|
||||||
|
|
||||||
|
- Не менять переход после «✅ Опубликовать» (на опубликованные) — осознанное
|
||||||
|
поведение, не просили.
|
||||||
|
- Не добавлять «вернуться к следующему кандидату» автоматом — редирект в
|
||||||
|
очередь с сохранением фильтров достаточен и предсказуем.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Файлы: `web/app.py` (2 точки редиректа), `web/templates/selected.html`
|
||||||
|
(правая панель), `web/templates/candidates.html` (высоты панелей).
|
||||||
|
- Данные: без изменений.
|
||||||
|
- Сервис: `vesti-web` (:8400) — перезапуск.
|
||||||
|
- Rollback: откатить коммит/вернуть редиректы на `src="selected"` и высоты
|
||||||
|
`82vh`, перезапустить.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# selected-layout-stay
|
||||||
|
|
||||||
|
- [x] Создан OpenSpec change (proposal/design) — `skip_specs: true`
|
||||||
|
- [x] `openspec validate selected-layout-stay` — чисто
|
||||||
|
- [x] web/app.py `post_select()`: редирект на кандидаты (src="candidates") после отбора
|
||||||
|
- [x] web/app.py `candidates_bulk()` action=select: редирект на `back` (кандидаты)
|
||||||
|
- [x] web/templates/selected.html: правое поле — резюме под кнопками, редактор/оригинал flex-grow-1, комментарий+публикация внизу, высоты calc(100vh - 160px)
|
||||||
|
- [x] web/templates/candidates.html: высоты панелей calc(100vh - 160px)
|
||||||
|
- [x] Smoke-тесты (read-only, TestClient): GET /candidates и /selected рендерятся, новая высота, резюме-блок, approve-форма внизу; редиректы select — проверены по коду + редирект single select подтверждён (Location=/candidates)
|
||||||
|
- [x] Перезапуск `sudo systemctl restart vesti-web` (HTTP 200)
|
||||||
|
- [ ] Обновить STATUS.md / TODO.md
|
||||||
|
- [ ] Бэкап (крон 2:45 делает сам, вручную НЕ запускать)
|
||||||
|
- [ ] Финальный e2e (временный select + откат статуса) — требует подтверждения пользователя (правило: не мутировать БД без явного согласия)
|
||||||
+4
-4
@@ -377,13 +377,13 @@ def selected(request: Request, direction: str = "", own: str = "", q: str = "",
|
|||||||
@app.post("/posts/{post_id}/select")
|
@app.post("/posts/{post_id}/select")
|
||||||
def post_select(post_id: int, request: Request,
|
def post_select(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("")):
|
||||||
"""Отобрать кандидата (new → selected). Затем переходим в отобранные."""
|
"""Отобрать кандидата (new → selected). Остаёмся на странице кандидатов."""
|
||||||
_require_auth(request)
|
_require_auth(request)
|
||||||
conn = _db()
|
conn = _db()
|
||||||
conn.execute("UPDATE posts SET status='selected' WHERE id=? AND status='new' AND is_own=0", (post_id,))
|
conn.execute("UPDATE posts SET status='selected' WHERE id=? AND status='new' AND is_own=0", (post_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src="selected", selected=post_id), status_code=302)
|
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src="candidates"), status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/candidates/select")
|
@app.post("/candidates/select")
|
||||||
@@ -415,11 +415,11 @@ def candidates_bulk(request: Request, action: str = Form(...), ids: list[int] =
|
|||||||
ph = ", ".join("?" * len(ids))
|
ph = ", ".join("?" * len(ids))
|
||||||
rows = conn.execute(f"SELECT * FROM posts WHERE id IN ({ph})", ids).fetchall()
|
rows = conn.execute(f"SELECT * FROM posts WHERE id IN ({ph})", ids).fetchall()
|
||||||
if action == "select":
|
if action == "select":
|
||||||
# отбор кандидатов (new → selected)
|
# отбор кандидатов (new → selected); остаёмся на странице кандидатов
|
||||||
conn.execute("UPDATE posts SET status='selected' WHERE id IN ({}) AND status='new' AND is_own=0".format(ph), ids)
|
conn.execute("UPDATE posts SET status='selected' WHERE id IN ({}) AND status='new' AND is_own=0".format(ph), ids)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return RedirectResponse(url=_cand_back(group_by, direction, own, q, src="selected"), status_code=302)
|
return RedirectResponse(url=back, status_code=302)
|
||||||
if action == "reject":
|
if action == "reject":
|
||||||
conn.execute(f"UPDATE posts SET status='rejected' WHERE id IN ({ph})", ids)
|
conn.execute(f"UPDATE posts SET status='rejected' WHERE id IN ({ph})", ids)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<!-- ЛЕВАЯ ПАНЕЛЬ: список + группировка + массовые операции -->
|
<!-- ЛЕВАЯ ПАНЕЛЬ: список + группировка + массовые операции -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="border rounded p-2 d-flex flex-column" style="max-height: 82vh; background:#fff;">
|
<div class="border rounded p-2 d-flex flex-column" style="max-height: calc(100vh - 160px); background:#fff;">
|
||||||
<div class="d-flex flex-wrap align-items-center mb-1 gap-1">
|
<div class="d-flex flex-wrap align-items-center mb-1 gap-1">
|
||||||
<span class="me-1 text-muted small">Группировка:</span>
|
<span class="me-1 text-muted small">Группировка:</span>
|
||||||
{% for val, label in [('source', 'источник'), ('date', 'дата'), ('status', 'статус')] %}
|
{% for val, label in [('source', 'источник'), ('date', 'дата'), ('status', 'статус')] %}
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
|
|
||||||
<!-- ПРАВАЯ ПАНЕЛЬ: детали выбранного -->
|
<!-- ПРАВАЯ ПАНЕЛЬ: детали выбранного -->
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="border rounded p-3 bg-white d-flex flex-column" style="height: 82vh;">
|
<div class="border rounded p-3 bg-white d-flex flex-column" style="height: calc(100vh - 160px);">
|
||||||
{% if selected %}
|
{% if selected %}
|
||||||
<div class="d-flex justify-content-between align-items-start flex-shrink-0">
|
<div class="d-flex justify-content-between align-items-start flex-shrink-0">
|
||||||
<h5 class="mb-0">#{{ selected.id }} {{ selected.source_name or '?' }}</h5>
|
<h5 class="mb-0">#{{ selected.id }} {{ selected.source_name or '?' }}</h5>
|
||||||
|
|||||||
+54
-45
@@ -43,7 +43,7 @@
|
|||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<!-- ЛЕВАЯ ПАНЕЛЬ: список отобранных -->
|
<!-- ЛЕВАЯ ПАНЕЛЬ: список отобранных -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="border rounded p-2 d-flex flex-column" style="max-height: 82vh; background:#fff;">
|
<div class="border rounded p-2 d-flex flex-column" style="max-height: calc(100vh - 160px); background:#fff;">
|
||||||
<div class="d-flex flex-wrap align-items-center mb-1 gap-1">
|
<div class="d-flex flex-wrap align-items-center mb-1 gap-1">
|
||||||
<span class="me-1 text-muted small">Группировка:</span>
|
<span class="me-1 text-muted small">Группировка:</span>
|
||||||
{% for val, label in [('source', 'источник'), ('date', 'дата'), ('status', 'статус')] %}
|
{% for val, label in [('source', 'источник'), ('date', 'дата'), ('status', 'статус')] %}
|
||||||
@@ -112,24 +112,24 @@
|
|||||||
|
|
||||||
<!-- ПРАВАЯ ПАНЕЛЬ: детали отобранного + подготовка своего поста -->
|
<!-- ПРАВАЯ ПАНЕЛЬ: детали отобранного + подготовка своего поста -->
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="border rounded p-3 bg-white d-flex flex-column" style="height: 82vh;">
|
<div class="border rounded p-3 bg-white d-flex flex-column" style="height: calc(100vh - 160px);">
|
||||||
{% if selected %}
|
{% if selected %}
|
||||||
<div class="d-flex justify-content-between align-items-start flex-shrink-0">
|
<div class="d-flex justify-content-between align-items-start flex-shrink-0">
|
||||||
<h5 class="mb-0">#{{ selected.id }} {{ selected.source_name or '?' }}</h5>
|
<h5 class="mb-0">#{{ selected.id }} {{ selected.source_name or '?' }}</h5>
|
||||||
<small class="text-muted">{{ (selected.published_at or selected.fetched_at) | dt }}</small>
|
<small class="text-muted">{{ (selected.published_at or selected.fetched_at) | dt }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2 flex-shrink-0">
|
||||||
{% if selected.direction %}<span class="badge bg-secondary">{{ selected.direction }}</span>{% endif %}
|
{% if selected.direction %}<span class="badge bg-secondary">{{ selected.direction }}</span>{% endif %}
|
||||||
{% if selected.relevance %}<span class="badge bg-{{ 'success' if selected.relevance=='critical' else 'info' }}">{{ selected.relevance }}</span>{% endif %}
|
{% if selected.relevance %}<span class="badge bg-{{ 'success' if selected.relevance=='critical' else 'info' }}{{ 'border' if selected.relevance=='critical' else '' }}">{{ selected.relevance }}</span>{% endif %}
|
||||||
{% if selected.interest %}<span class="badge bg-warning text-dark">interest {{ selected.interest }}</span>{% endif %}
|
{% if selected.interest %}<span class="badge bg-warning text-dark">interest {{ selected.interest }}</span>{% endif %}
|
||||||
{% if selected.is_own %}<span class="badge bg-warning text-dark">⭐ СВОЙ</span>{% endif %}
|
{% if selected.is_own %}<span class="badge bg-warning text-dark">⭐ СВОЙ</span>{% endif %}
|
||||||
{% if selected.fwd_from_channel_id %}<span class="badge bg-light text-dark">↩︎ форвард</span>{% endif %}
|
{% if selected.fwd_from_channel_id %}<span class="badge bg-light text-dark">↩︎ форвард</span>{% endif %}
|
||||||
<small class="text-muted ms-2">👁 {{ selected.views }} 💬 {{ selected.reactions_total }}</small>
|
<small class="text-muted ms-2">👁 {{ selected.views }} 💬 {{ selected.reactions_total }}</small>
|
||||||
{% if selected.url %}<a class="btn btn-sm btn-outline-secondary ms-2" href="{{ selected.url }}" target="_blank">Исходник</a>{% endif %}
|
{% if selected.url %}<a class="btn btn-sm btn-outline-secondary ms-2" href="{{ selected.url }}" target="_blank">Исходник</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3 flex-shrink-0">
|
|
||||||
{% 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 }}"><input type="hidden" name="src" value="selected">{% endset %}
|
{% 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 }}"><input type="hidden" name="src" value="selected">{% endset %}
|
||||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
<!-- Кнопки действий -->
|
||||||
|
<div class="d-flex flex-wrap gap-2 mb-2 flex-shrink-0">
|
||||||
{% 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 }}
|
{{ hid }}
|
||||||
@@ -149,7 +149,53 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Резюме — сразу под кнопками -->
|
||||||
|
{% if selected.summary %}
|
||||||
|
<div class="flex-shrink-0 mb-2">
|
||||||
|
<div class="border rounded bg-light p-2 small">
|
||||||
|
<span class="badge bg-secondary">резюме</span> {{ selected.summary }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<hr class="flex-shrink-0 my-2">
|
||||||
|
<!-- подготовка своего поста: редактор + оригинал рядом, на всю оставшуюся высоту -->
|
||||||
|
<div class="row g-2 flex-grow-1" style="min-height:0;">
|
||||||
|
<div class="col-md-6 d-flex flex-column" style="min-height:0;">
|
||||||
|
<h6 class="text-muted">✍️ Мой пост (редактируйте/сохраните — пойдёт в канал от вашего имени)</h6>
|
||||||
|
<form method="post" action="/posts/{{ selected.id }}/rewrite-save" class="row g-1 d-flex flex-column flex-grow-1" style="min-height:0;">
|
||||||
|
{{ hid }}
|
||||||
|
<textarea class="form-control form-control-sm flex-grow-1" name="rewritten_text" rows="8" style="flex-grow:1; min-height:140px; resize:vertical;">{{ selected.rewritten_text or '' }}</textarea>
|
||||||
|
<div class="col-auto mt-1">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" type="submit">💾 Сохранить</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 d-flex flex-column" style="min-height:0;">
|
||||||
|
<h6 class="text-muted">Оригинальный пост</h6>
|
||||||
|
<div class="border rounded p-2 bg-light small overflow-auto flex-grow-1" style="min-height:0;">
|
||||||
|
{% if selected.text %}
|
||||||
|
<div class="post-text">{{ selected.text | markdown }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if selected.media_path %}
|
||||||
|
{% set media_src = '/media/' ~ selected.media_path.split('/')[-1] %}
|
||||||
|
{% set media_ext = selected.media_path.lower().rsplit('.', 1)[-1] if '.' in selected.media_path else '' %}
|
||||||
|
{% if media_ext in ['jpg','jpeg','png','gif','webp'] %}
|
||||||
|
<a href="{{ media_src }}" target="_blank" title="Открыть полноразмер">
|
||||||
|
<img src="{{ media_src }}" class="img-fluid rounded mt-2 d-block" style="max-height:50vh" alt="медиа поста">
|
||||||
|
</a>
|
||||||
|
{% elif media_ext in ['mp4','webm','mov'] %}
|
||||||
|
<video src="{{ media_src }}" controls class="mt-2 d-block" style="max-height:50vh"></video>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-sm btn-outline-secondary mt-2" href="{{ media_src }}" target="_blank">📎 Медиафайл</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="flex-shrink-0 my-2">
|
||||||
|
<!-- Комментарий + публикация — в самом низу -->
|
||||||
{% if selected.status != 'published' %}
|
{% if selected.status != 'published' %}
|
||||||
|
<div class="flex-shrink-0 pt-2 border-top">
|
||||||
<form method="post" action="/posts/{{ selected.id }}/approve" id="approve-form">
|
<form method="post" action="/posts/{{ selected.id }}/approve" id="approve-form">
|
||||||
{{ hid }}
|
{{ hid }}
|
||||||
<label class="form-label small text-muted mb-0 d-block">💬 Мой комментарий (будет первым в канале):</label>
|
<label class="form-label small text-muted mb-0 d-block">💬 Мой комментарий (будет первым в канале):</label>
|
||||||
@@ -162,52 +208,15 @@
|
|||||||
title="Сохранить комментарий в черновик (не публикует). Он подтянется при следующей публикации">💾 Сохранить комментарий</button>
|
title="Сохранить комментарий в черновик (не публикует). Он подтянется при следующей публикации">💾 Сохранить комментарий</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- подготовка своего поста: редактор + оригинал рядом -->
|
|
||||||
<hr class="flex-shrink-0">
|
|
||||||
<div class="row g-2 flex-shrink-0">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h6 class="text-muted">✍️ Мой пост (редактируйте/сохраните — пойдёт в канал от вашего имени)</h6>
|
|
||||||
<form method="post" action="/posts/{{ selected.id }}/rewrite-save" class="row g-1">
|
|
||||||
{{ hid }}
|
|
||||||
<textarea class="form-control form-control-sm" name="rewritten_text" rows="8">{{ selected.rewritten_text or '' }}</textarea>
|
|
||||||
<div class="col-auto mt-1">
|
|
||||||
<button class="btn btn-sm btn-outline-primary" type="submit">💾 Сохранить</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h6 class="text-muted">Оригинальный пост</h6>
|
|
||||||
<div class="border rounded p-2 bg-light small overflow-auto" style="max-height:300px;">
|
|
||||||
{% if selected.text %}
|
|
||||||
<div class="post-text">{{ selected.text | markdown }}</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if selected.media_path %}
|
|
||||||
{% set media_src = '/media/' ~ selected.media_path.split('/')[-1] %}
|
|
||||||
{% set media_ext = selected.media_path.lower().rsplit('.', 1)[-1] if '.' in selected.media_path else '' %}
|
|
||||||
{% if media_ext in ['jpg','jpeg','png','gif','webp'] %}
|
|
||||||
<a href="{{ media_src }}" target="_blank" title="Открыть полноразмер">
|
|
||||||
<img src="{{ media_src }}" class="img-fluid rounded mt-2 d-block" style="max-height:320px" alt="медиа поста">
|
|
||||||
</a>
|
|
||||||
{% elif media_ext in ['mp4','webm','mov'] %}
|
|
||||||
<video src="{{ media_src }}" controls class="mt-2 d-block" style="max-height:320px"></video>
|
|
||||||
{% else %}
|
|
||||||
<a class="btn btn-sm btn-outline-secondary mt-2" href="{{ media_src }}" target="_blank">📎 Медиафайл</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if selected.summary %}<div class="mt-2"><span class="badge bg-light text-dark">резюме</span> {{ selected.summary }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-info py-3">Выберите отобранный пост слева</div>
|
<div class="alert alert-info py-3">Выберите отобранный пост слева</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user