DispelEvilBot/admin/routes/schedules.py

191 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Раздел «Расписания»: CRUD рассылок, которые исполняет бот."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from fastapi import APIRouter, HTTPException, Request, Response
from fastapi.responses import HTMLResponse, RedirectResponse
from admin.forms import format_weekdays, parse_schedule_fields
from common import __version__
from common.repositories import schedules_repo, templates_repo
router = APIRouter(prefix="/schedules", tags=["schedules"])
def _raw_from_form(form: Any) -> dict[str, Any]:
return {
"template_key": str(form.get("template_key", "")),
"hour": str(form.get("hour", "")),
"minute": str(form.get("minute", "")),
"weekdays": [str(day) for day in form.getlist("weekdays")],
"timezone": str(form.get("timezone", "")),
"description": str(form.get("description", "")),
"enabled": form.get("enabled") is not None,
}
def _view(values: Mapping[str, Any], *, is_new: bool) -> dict[str, Any]:
"""Привести значения (из БД или формы) к виду, удобному шаблону."""
days: list[int] = []
for day in values.get("weekdays") or ():
try:
days.append(int(day))
except (TypeError, ValueError):
continue
return {
"template_key": values.get("template_key") or "",
"hour": values.get("hour", 16),
"minute": values.get("minute", 0),
"weekdays": sorted(set(days)),
"timezone": values.get("timezone") or "Europe/Moscow",
"description": values.get("description") or "",
"enabled": bool(values.get("enabled", True)),
"id": values.get("id"),
"is_new": is_new,
"action": "/schedules" if is_new else f"/schedules/{values.get('id')}",
}
def _form_response(
request: Request,
schedule: dict[str, Any],
message_templates: list[dict[str, Any]],
*,
errors: list[str] | None = None,
history: list[dict[str, Any]] | None = None,
status_code: int = 200,
) -> Response:
return request.app.state.templates.TemplateResponse(
request=request,
name="schedules/form.html",
context={
"version": __version__,
"schedule": schedule,
"message_templates": message_templates,
"errors": errors or [],
"history": history or [],
"weekday_names": {1: "Пн", 2: "Вт", 3: "Ср", 4: "Чт", 5: "Пт", 6: "Сб", 7: "Вс"},
},
status_code=status_code,
)
@router.get("", response_class=HTMLResponse)
async def list_schedules(request: Request) -> HTMLResponse:
"""Список расписаний: время, дни, шаблон, таймзона и статус."""
return request.app.state.templates.TemplateResponse(
request=request,
name="schedules/list.html",
context={
"version": __version__,
"schedules": await schedules_repo.list_schedules(),
"format_weekdays": format_weekdays,
},
)
@router.get("/new", response_class=HTMLResponse)
async def new_schedule(request: Request) -> HTMLResponse:
"""Форма создания расписания."""
message_templates = await templates_repo.list_templates()
defaults = {
"template_key": message_templates[0]["key"] if message_templates else "",
"hour": 16,
"minute": 0,
"weekdays": [],
"timezone": "Europe/Moscow",
"description": "",
"enabled": True,
}
return _form_response(request, _view(defaults, is_new=True), message_templates)
@router.post("")
async def create_schedule(request: Request) -> Response:
"""Создать расписание (с проверкой полей и существования шаблона)."""
form = await request.form()
raw = _raw_from_form(form)
data, errors = parse_schedule_fields(**raw)
message_templates = await templates_repo.list_templates()
if data is not None and await templates_repo.get_template(data["template_key"]) is None:
errors.append("Шаблон не найден")
if errors:
return _form_response(
request,
_view(raw, is_new=True),
message_templates,
errors=errors,
status_code=400,
)
await schedules_repo.create_schedule(data)
return RedirectResponse(url="/schedules", status_code=303)
@router.get("/{schedule_id}", response_class=HTMLResponse)
async def edit_schedule(request: Request, schedule_id: int) -> HTMLResponse:
"""Форма редактирования и история изменений."""
schedule = await schedules_repo.get_schedule(schedule_id)
if schedule is None:
raise HTTPException(status_code=404, detail="Расписание не найдено")
return _form_response(
request,
_view(schedule, is_new=False),
await templates_repo.list_templates(),
history=await schedules_repo.schedule_history(schedule_id),
)
@router.post("/{schedule_id}")
async def update_schedule(request: Request, schedule_id: int) -> Response:
"""Сохранить изменения расписания."""
if await schedules_repo.get_schedule(schedule_id) is None:
raise HTTPException(status_code=404, detail="Расписание не найдено")
form = await request.form()
raw = _raw_from_form(form)
data, errors = parse_schedule_fields(**raw)
message_templates = await templates_repo.list_templates()
if data is not None and await templates_repo.get_template(data["template_key"]) is None:
errors.append("Шаблон не найден")
if errors:
view = _view({**raw, "id": schedule_id}, is_new=False)
return _form_response(
request,
view,
message_templates,
errors=errors,
history=await schedules_repo.schedule_history(schedule_id),
status_code=400,
)
await schedules_repo.update_schedule(schedule_id, data)
return RedirectResponse(url="/schedules", status_code=303)
@router.post("/{schedule_id}/toggle")
async def toggle_schedule(schedule_id: int) -> Response:
"""Включить или выключить расписание."""
schedule = await schedules_repo.get_schedule(schedule_id)
if schedule is None:
raise HTTPException(status_code=404, detail="Расписание не найдено")
await schedules_repo.set_enabled(schedule_id, not schedule["enabled"])
return RedirectResponse(url="/schedules", status_code=303)
@router.post("/{schedule_id}/delete")
async def delete_schedule(schedule_id: int) -> Response:
"""Удалить расписание (история при этом сохраняется)."""
if not await schedules_repo.delete_schedule(schedule_id):
raise HTTPException(status_code=404, detail="Расписание не найдено")
return RedirectResponse(url="/schedules", status_code=303)