Раздел «Последовательности» стал «Шаблонами сообщений», а счётчик сообщений починен
В списке колонка «Сообщений» показывала метод словаря вместо числа: SQL-алиас items переименован в item_count, и шаблон больше не натыкается на dict.items. Заодно в интерфейсе и README раздел называется «Шаблоны сообщений», а одинаковое название и описание больше не дублируются в списке.
This commit is contained in:
parent
3347dacbb5
commit
5e14847449
|
|
@ -55,7 +55,7 @@ APP_ENV=prod python -m bot # prod
|
|||
## Админка (Блок 4)
|
||||
|
||||
Отдельный сервис `admin/` (FastAPI + Jinja2/HTMX), запуск `python -m admin`.
|
||||
Все тексты бота — **последовательности** (`bot.message_sequences`): миграция `V24`
|
||||
Все тексты бота — **шаблоны сообщений** (`bot.message_sequences`): миграция `V24`
|
||||
переносит каждый старый шаблон в последовательность из одного сообщения, а бот
|
||||
отправляет их через общий sender. Старые `message_templates` остаются фолбэком в коде.
|
||||
Правки применяются без перезапуска; предпросмотр использует тот же `render()`, что и бот.
|
||||
|
|
@ -96,7 +96,7 @@ ID пользователя ведёт в его карточку. ClickHouse ч
|
|||
уведомлением «кто кого выгнал»; историю изменений показывает, кто менял ник — сам
|
||||
пользователь через `/ник` или админ.
|
||||
|
||||
Раздел «Последовательности» — набор сообщений по порядку (приветствие и любые другие):
|
||||
Раздел «Шаблоны сообщений» — набор сообщений по порядку (приветствие и любые другие):
|
||||
у каждого свой текст, картинка из общего volume `media`, вкл/выкл и несколько кнопок.
|
||||
Редактор текста — textarea с тулбаром: жирный, курсив, подчёркивание, зачёркивание,
|
||||
код, блок кода, цитата, спойлер и ссылка; есть предпросмотр и счётчик символов
|
||||
|
|
@ -137,7 +137,7 @@ docker compose up --build
|
|||
CI собирает два образа: `dispelevilbot-bot` и `dispelevilbot-admin`. Админка в compose
|
||||
по умолчанию публикуется только на loopback (`127.0.0.1:${ADMIN_HTTP_PORT}`, test 8100 /
|
||||
prod 8101); для nginx-proxy-manager задаётся `ADMIN_BIND_IP=172.17.0.1`, наружу выйдет
|
||||
вместе с доменом и TLS (Блок 4.2). Картинки последовательностей лежат в общем
|
||||
вместе с доменом и TLS (Блок 4.2). Картинки шаблонов лежат в общем
|
||||
Docker-volume `media` (`/app/media`), который монтируется и боту, и админке.
|
||||
|
||||
Прод/test-контуры — один `docker-compose.prod.yml`, разделяются Compose-проектом и env-файлом:
|
||||
|
|
@ -171,7 +171,7 @@ make backup E=test # на домашнем сервере: ./scripts/clickho
|
|||
|
||||
## Версионирование
|
||||
|
||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.6.1`.
|
||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.6.2`.
|
||||
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
||||
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
|
||||
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ router = APIRouter(tags=["pages"])
|
|||
#: Разделы админки по плану Блока 4 (url — None, пока раздел не готов).
|
||||
SECTIONS: tuple[dict[str, str | None], ...] = (
|
||||
{
|
||||
"title": "Последовательности",
|
||||
"description": "Приветствие и другие наборы сообщений по порядку",
|
||||
"title": "Шаблоны сообщений",
|
||||
"description": "Тексты бота: сообщения, картинки и кнопки",
|
||||
"url": "/sequences",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ async def _editor_response(
|
|||
) -> HTMLResponse:
|
||||
sequence = await sequences_repo.get_sequence(key)
|
||||
if sequence is None:
|
||||
raise HTTPException(status_code=404, detail="Последовательность не найдена")
|
||||
raise HTTPException(status_code=404, detail="Шаблон не найден")
|
||||
|
||||
items = await sequences_repo.get_items(key)
|
||||
_prepare_items(items)
|
||||
|
|
@ -151,7 +151,7 @@ async def create_sequence(request: Request) -> Response:
|
|||
):
|
||||
return await _list_response(
|
||||
request,
|
||||
errors=["Последовательность с таким ключом уже существует"],
|
||||
errors=["Шаблон с таким ключом уже существует"],
|
||||
form=raw_form,
|
||||
status_code=400,
|
||||
)
|
||||
|
|
@ -188,7 +188,7 @@ async def update_sequence(request: Request, key: str) -> Response:
|
|||
description,
|
||||
editor_id=_editor_id(request),
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="Последовательность не найдена")
|
||||
raise HTTPException(status_code=404, detail="Шаблон не найден")
|
||||
return RedirectResponse(url=f"/sequences/{key}?saved=meta", status_code=303)
|
||||
|
||||
|
||||
|
|
@ -198,7 +198,7 @@ async def add_item(request: Request, key: str) -> Response:
|
|||
check_csrf(request, str(form.get("csrf_token", "")))
|
||||
|
||||
if await sequences_repo.add_item(key, editor_id=_editor_id(request)) is None:
|
||||
raise HTTPException(status_code=404, detail="Последовательность не найдена")
|
||||
raise HTTPException(status_code=404, detail="Шаблон не найден")
|
||||
return RedirectResponse(url=f"/sequences/{key}?saved=item", status_code=303)
|
||||
|
||||
|
||||
|
|
@ -210,7 +210,7 @@ async def update_item(request: Request, key: str, item_id: int) -> Response:
|
|||
items = await sequences_repo.get_items(key)
|
||||
item = next((row for row in items if int(row["id"]) == item_id), None)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="Сообщение не найдено")
|
||||
raise HTTPException(status_code=404, detail="Сообщение шаблона не найдено")
|
||||
|
||||
text = str(form.get("text", ""))
|
||||
enabled = form.get("enabled") is not None
|
||||
|
|
@ -274,7 +274,7 @@ async def update_item(request: Request, key: str, item_id: int) -> Response:
|
|||
if updated is None:
|
||||
if saved_file:
|
||||
_delete_image(settings, saved_file)
|
||||
raise HTTPException(status_code=404, detail="Сообщение не найдено")
|
||||
raise HTTPException(status_code=404, detail="Сообщение шаблона не найдено")
|
||||
|
||||
_, old_image = updated
|
||||
if update_image and old_image and old_image != new_image:
|
||||
|
|
@ -292,7 +292,7 @@ async def move_item(request: Request, key: str, item_id: int) -> Response:
|
|||
if direction not in {"up", "down"}:
|
||||
raise HTTPException(status_code=400, detail="Неверное направление")
|
||||
if await sequences_repo.move_item(item_id, direction, editor_id=_editor_id(request)) is None:
|
||||
raise HTTPException(status_code=404, detail="Сообщение не найдено")
|
||||
raise HTTPException(status_code=404, detail="Сообщение шаблона не найдено")
|
||||
return RedirectResponse(url=f"/sequences/{key}?saved=order", status_code=303)
|
||||
|
||||
|
||||
|
|
@ -304,10 +304,10 @@ async def delete_item(request: Request, key: str, item_id: int) -> Response:
|
|||
items = await sequences_repo.get_items(key)
|
||||
item = next((row for row in items if int(row["id"]) == item_id), None)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="Сообщение не найдено")
|
||||
raise HTTPException(status_code=404, detail="Сообщение шаблона не найдено")
|
||||
|
||||
if await sequences_repo.delete_item(item_id, editor_id=_editor_id(request)) is None:
|
||||
raise HTTPException(status_code=404, detail="Сообщение не найдено")
|
||||
raise HTTPException(status_code=404, detail="Сообщение шаблона не найдено")
|
||||
|
||||
_delete_image(request.app.state.settings, item["image_name"])
|
||||
return RedirectResponse(url=f"/sequences/{key}?saved=item", status_code=303)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
class="hidden w-full flex-col gap-2 border-t border-slate-100 pt-2 text-sm sm:flex sm:w-auto sm:flex-row sm:items-center sm:gap-4 sm:border-0 sm:pt-0"
|
||||
>
|
||||
<a href="/" class="py-1 text-slate-600 hover:text-slate-900">Главная</a>
|
||||
<a href="/sequences" class="py-1 text-slate-600 hover:text-slate-900">Последовательности</a>
|
||||
<a href="/sequences" class="py-1 text-slate-600 hover:text-slate-900">Шаблоны</a>
|
||||
<a href="/schedules" class="py-1 text-slate-600 hover:text-slate-900">Расписания</a>
|
||||
<a href="/users" class="py-1 text-slate-600 hover:text-slate-900">Пользователи</a>
|
||||
<a href="/titles" class="py-1 text-slate-600 hover:text-slate-900">Звания</a>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{% block title %}{{ sequence.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="text-sm text-slate-500"><a href="/sequences" class="hover:underline">← Последовательности</a></p>
|
||||
<p class="text-sm text-slate-500"><a href="/sequences" class="hover:underline">← Шаблоны сообщений</a></p>
|
||||
<div class="mt-1 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">{{ sequence.title }}</h1>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
{% if saved %}
|
||||
<div class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-800">
|
||||
{% if saved == 'meta' %}Параметры последовательности сохранены.
|
||||
{% if saved == 'meta' %}Параметры шаблона сохранены.
|
||||
{% elif saved == 'order' %}Порядок сообщений обновлён.
|
||||
{% else %}Сообщение сохранено.{% endif %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Последовательности{% endblock %}
|
||||
{% block title %}Шаблоны сообщений{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Последовательности</h1>
|
||||
<p class="mt-1 text-sm text-slate-600">Несколько сообщений по порядку: текст, картинка и кнопки.</p>
|
||||
<h1 class="text-2xl font-semibold">Шаблоны сообщений</h1>
|
||||
<p class="mt-1 text-sm text-slate-600">Тексты бота: сообщения, картинки и кнопки.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
<form method="post" action="/sequences" class="mt-6 rounded-lg border border-slate-200 bg-white p-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ request.session.get('csrf', '') }}" />
|
||||
<h2 class="text-lg font-medium">Новая последовательность</h2>
|
||||
<h2 class="text-lg font-medium">Новый шаблон</h2>
|
||||
<div class="mt-3 grid gap-3 sm:grid-cols-4">
|
||||
<div>
|
||||
<label for="key" class="text-xs text-slate-500">Ключ</label>
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
<th class="px-4 py-3 font-medium">Ключ</th>
|
||||
<th class="px-4 py-3 font-medium">Название</th>
|
||||
<th class="px-4 py-3 font-medium">Сообщений</th>
|
||||
<th class="px-4 py-3 font-medium">Изменена</th>
|
||||
<th class="px-4 py-3 font-medium">Изменён</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
|
|
@ -65,12 +65,17 @@
|
|||
<td class="px-4 py-3">
|
||||
<a href="/sequences/{{ sequence.key }}" class="font-mono text-sky-700 hover:underline">{{ sequence.key }}</a>
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ sequence.title }}<div class="text-xs text-slate-500">{{ sequence.description }}</div></td>
|
||||
<td class="px-4 py-3">{{ sequence.items }}</td>
|
||||
<td class="px-4 py-3">
|
||||
{{ sequence.title }}
|
||||
{% if sequence.description and sequence.description != sequence.title %}
|
||||
<div class="text-xs text-slate-500">{{ sequence.description }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ sequence.item_count }}</td>
|
||||
<td class="px-4 py-3 text-slate-500">{{ sequence.updated_at.strftime("%d.%m.%Y %H:%M") }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="4" class="px-4 py-6 text-center text-slate-500">Последовательностей пока нет.</td></tr>
|
||||
<tr><td colspan="4" class="px-4 py-6 text-center text-slate-500">Шаблонов пока нет.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ async def list_sequences() -> list[dict[str, Any]]:
|
|||
cur = await conn.execute(
|
||||
"""
|
||||
SELECT s.key, s.title, s.description, s.updated_at, s.updated_by,
|
||||
count(i.id) AS items
|
||||
count(i.id) AS item_count
|
||||
FROM bot.message_sequences AS s
|
||||
LEFT JOIN bot.message_sequence_items AS i ON i.sequence_key = s.key
|
||||
GROUP BY s.key, s.title, s.description, s.updated_at, s.updated_by
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "0.6.1"
|
||||
__version__ = "0.6.2"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ SEQUENCE: dict[str, Any] = {
|
|||
"key": "welcome",
|
||||
"title": "Приветствие",
|
||||
"description": "Сообщения при входе",
|
||||
"item_count": 2,
|
||||
"updated_at": datetime(2026, 10, 25, tzinfo=UTC),
|
||||
"updated_by": None,
|
||||
}
|
||||
|
|
@ -132,6 +133,8 @@ def test_sequences_list(fake_sequences: dict[str, Any], client_factory) -> None:
|
|||
assert response.status_code == 200
|
||||
assert "welcome" in response.text
|
||||
assert "Приветствие" in response.text
|
||||
assert "<built-in method" not in response.text
|
||||
assert "Шаблоны сообщений" in response.text
|
||||
|
||||
|
||||
def test_sequence_editor_has_toolbar(fake_sequences: dict[str, Any], client_factory) -> None:
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@ from common import __version__
|
|||
|
||||
def test_version_is_set():
|
||||
assert __version__
|
||||
assert __version__ == "0.6.1"
|
||||
assert __version__ == "0.6.2"
|
||||
|
|
|
|||
Loading…
Reference in New Issue