diff --git a/README.md b/README.md index 54da8f6..63f70bc 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/admin/routes/pages.py b/admin/routes/pages.py index 27c6665..8c55c26 100644 --- a/admin/routes/pages.py +++ b/admin/routes/pages.py @@ -12,8 +12,8 @@ router = APIRouter(tags=["pages"]) #: Разделы админки по плану Блока 4 (url — None, пока раздел не готов). SECTIONS: tuple[dict[str, str | None], ...] = ( { - "title": "Последовательности", - "description": "Приветствие и другие наборы сообщений по порядку", + "title": "Шаблоны сообщений", + "description": "Тексты бота: сообщения, картинки и кнопки", "url": "/sequences", }, { diff --git a/admin/routes/sequences.py b/admin/routes/sequences.py index 4834bbd..b8b6f65 100644 --- a/admin/routes/sequences.py +++ b/admin/routes/sequences.py @@ -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) diff --git a/admin/templates/base.html b/admin/templates/base.html index bb533c5..5b04595 100644 --- a/admin/templates/base.html +++ b/admin/templates/base.html @@ -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" > Главная - Последовательности + Шаблоны Расписания Пользователи Звания diff --git a/admin/templates/sequences/edit.html b/admin/templates/sequences/edit.html index 562e79a..d971967 100644 --- a/admin/templates/sequences/edit.html +++ b/admin/templates/sequences/edit.html @@ -3,7 +3,7 @@ {% block title %}{{ sequence.title }}{% endblock %} {% block content %} -

← Последовательности

+

← Шаблоны сообщений

{{ sequence.title }}

@@ -13,7 +13,7 @@ {% if saved %}
- {% if saved == 'meta' %}Параметры последовательности сохранены. + {% if saved == 'meta' %}Параметры шаблона сохранены. {% elif saved == 'order' %}Порядок сообщений обновлён. {% else %}Сообщение сохранено.{% endif %}
diff --git a/admin/templates/sequences/list.html b/admin/templates/sequences/list.html index c2bfdc8..b7e473d 100644 --- a/admin/templates/sequences/list.html +++ b/admin/templates/sequences/list.html @@ -1,12 +1,12 @@ {% extends "base.html" %} -{% block title %}Последовательности{% endblock %} +{% block title %}Шаблоны сообщений{% endblock %} {% block content %}
-

Последовательности

-

Несколько сообщений по порядку: текст, картинка и кнопки.

+

Шаблоны сообщений

+

Тексты бота: сообщения, картинки и кнопки.

@@ -24,7 +24,7 @@
-

Новая последовательность

+

Новый шаблон

@@ -56,7 +56,7 @@ Ключ Название Сообщений - Изменена + Изменён @@ -65,12 +65,17 @@ {{ sequence.key }} - {{ sequence.title }}
{{ sequence.description }}
- {{ sequence.items }} + + {{ sequence.title }} + {% if sequence.description and sequence.description != sequence.title %} +
{{ sequence.description }}
+ {% endif %} + + {{ sequence.item_count }} {{ sequence.updated_at.strftime("%d.%m.%Y %H:%M") }} {% else %} - Последовательностей пока нет. + Шаблонов пока нет. {% endfor %} diff --git a/common/repositories/sequences_repo.py b/common/repositories/sequences_repo.py index 3c095c9..ad7efef 100644 --- a/common/repositories/sequences_repo.py +++ b/common/repositories/sequences_repo.py @@ -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 diff --git a/common/version.py b/common/version.py index 991c6ca..c48b447 100644 --- a/common/version.py +++ b/common/version.py @@ -7,4 +7,4 @@ from __future__ import annotations -__version__ = "0.6.1" +__version__ = "0.6.2" diff --git a/tests/test_admin_sequences.py b/tests/test_admin_sequences.py index 56ea9f8..620d61d 100644 --- a/tests/test_admin_sequences.py +++ b/tests/test_admin_sequences.py @@ -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 " None: diff --git a/tests/test_version.py b/tests/test_version.py index 38b730d..2f8b8df 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -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"