133 lines
5.3 KiB
Python
133 lines
5.3 KiB
Python
"""Раздел «Сообщения»: список шаблонов, редактор и предпросмотр (HTMX)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import html
|
||
|
||
from fastapi import APIRouter, HTTPException, Request, Response
|
||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||
|
||
from admin.preview import SAMPLE_CONTEXT
|
||
from admin.security import check_csrf
|
||
from common import __version__
|
||
from common.repositories import templates_repo
|
||
from common.texts.render import MAX_MESSAGE_LENGTH, TAG_REGISTRY, TemplateError, render, validate
|
||
|
||
router = APIRouter(prefix="/messages", tags=["messages"])
|
||
|
||
|
||
def _editor_context(
|
||
request: Request,
|
||
template: dict[str, object],
|
||
*,
|
||
text: str | None = None,
|
||
errors: list[str] | None = None,
|
||
saved: bool = False,
|
||
history: list[dict[str, object]] | None = None,
|
||
) -> dict[str, object]:
|
||
"""Контекст страницы редактора (общий для GET и повторного показа после ошибки)."""
|
||
return {
|
||
"version": __version__,
|
||
"template": template,
|
||
"text": text if text is not None else template["text"],
|
||
"errors": errors or [],
|
||
"saved": saved,
|
||
"history": history or [],
|
||
"tags": TAG_REGISTRY,
|
||
"max_length": MAX_MESSAGE_LENGTH,
|
||
}
|
||
|
||
|
||
@router.get("", response_class=HTMLResponse)
|
||
async def list_messages(request: Request) -> HTMLResponse:
|
||
"""Список шаблонов: ключ, описание, длина и время последней правки."""
|
||
templates = request.app.state.templates
|
||
return templates.TemplateResponse(
|
||
request=request,
|
||
name="messages/list.html",
|
||
context={"version": __version__, "templates": await templates_repo.list_templates()},
|
||
)
|
||
|
||
|
||
@router.post("/preview", response_class=HTMLResponse)
|
||
async def preview_message(request: Request) -> HTMLResponse:
|
||
"""Отрендерить текст с тестовыми значениями (HTMX-фрагмент).
|
||
|
||
Результат кладётся в ``iframe sandbox``: разметка Telegram видна «как в чате»,
|
||
но скрипты из введённого текста не выполняются.
|
||
"""
|
||
form = await request.form()
|
||
check_csrf(request, str(form.get("csrf_token", "")))
|
||
text = str(form.get("text", ""))
|
||
|
||
unknown = validate(text)
|
||
if unknown:
|
||
return HTMLResponse(
|
||
'<p class="text-sm text-red-600">Неизвестные теги: ' + html.escape(", ".join(unknown)) + "</p>"
|
||
)
|
||
|
||
try:
|
||
rendered = render(text, SAMPLE_CONTEXT)
|
||
except TemplateError as exc: # pragma: no cover - validate уже отсекает неизвестные теги
|
||
return HTMLResponse(f'<p class="text-sm text-red-600">{html.escape(str(exc))}</p>')
|
||
|
||
if len(rendered) > MAX_MESSAGE_LENGTH:
|
||
return HTMLResponse(
|
||
f'<p class="text-sm text-red-600">Текст длиннее лимита Telegram: '
|
||
f"{len(rendered)} из {MAX_MESSAGE_LENGTH} символов.</p>"
|
||
)
|
||
|
||
return HTMLResponse(
|
||
'<iframe sandbox class="h-64 w-full rounded border border-slate-200 bg-white" '
|
||
f'srcdoc="{html.escape(rendered, quote=True)}"></iframe>'
|
||
)
|
||
|
||
|
||
@router.post("/{key}")
|
||
async def save_message(request: Request, key: str) -> Response:
|
||
"""Сохранить текст шаблона; при ошибках вернуть редактор с подсветкой."""
|
||
form = await request.form()
|
||
check_csrf(request, str(form.get("csrf_token", "")))
|
||
text = str(form.get("text", ""))
|
||
|
||
template = await templates_repo.get_template(key)
|
||
if template is None:
|
||
raise HTTPException(status_code=404, detail="Шаблон не найден")
|
||
|
||
errors: list[str] = []
|
||
unknown = validate(text)
|
||
if unknown:
|
||
errors.append("Неизвестные теги: " + ", ".join(unknown))
|
||
if not text.strip():
|
||
errors.append("Текст не может быть пустым")
|
||
if len(text) > MAX_MESSAGE_LENGTH:
|
||
errors.append(f"Длина {len(text)} превышает лимит Telegram ({MAX_MESSAGE_LENGTH})")
|
||
|
||
if errors:
|
||
history = await templates_repo.template_history(key)
|
||
return request.app.state.templates.TemplateResponse(
|
||
request=request,
|
||
name="messages/edit.html",
|
||
context=_editor_context(request, template, text=text, errors=errors, history=history),
|
||
status_code=400,
|
||
)
|
||
|
||
editor_id = int(request.state.admin["telegram_id"])
|
||
await templates_repo.update_template(key, text, editor_id=editor_id)
|
||
return RedirectResponse(url=f"/messages/{key}?saved=1", status_code=303)
|
||
|
||
|
||
@router.get("/{key}", response_class=HTMLResponse)
|
||
async def edit_message(request: Request, key: str, saved: bool = False) -> HTMLResponse:
|
||
"""Редактор шаблона: текст, подсказка по тегам, предпросмотр и история."""
|
||
template = await templates_repo.get_template(key)
|
||
if template is None:
|
||
raise HTTPException(status_code=404, detail="Шаблон не найден")
|
||
|
||
history = await templates_repo.template_history(key)
|
||
return request.app.state.templates.TemplateResponse(
|
||
request=request,
|
||
name="messages/edit.html",
|
||
context=_editor_context(request, template, saved=saved, history=history),
|
||
)
|