184 lines
6.7 KiB
Python
184 lines
6.7 KiB
Python
"""Репозиторий расписаний рассылок (Блок 4.5).
|
||
|
||
Редактирует админка, исполняет бот: он читает активные строки раз в минуту.
|
||
Каждое изменение пишет снимок строки в ``schedules_history`` (аудит).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from psycopg.types.json import Jsonb
|
||
|
||
from common.db.pool import get_pool
|
||
|
||
#: Поля, которые попадают в историю и форму (без служебных отметок времени).
|
||
SNAPSHOT_FIELDS = ("template_key", "hour", "minute", "weekdays", "timezone", "enabled", "description")
|
||
|
||
|
||
def _snapshot(row: dict[str, Any]) -> dict[str, Any]:
|
||
return {field: row.get(field) for field in SNAPSHOT_FIELDS}
|
||
|
||
|
||
async def list_schedules() -> list[dict[str, Any]]:
|
||
"""Все расписания с описанием шаблона (для таблицы в админке)."""
|
||
async with get_pool().connection() as conn:
|
||
cur = await conn.execute(
|
||
"""
|
||
SELECT s.id, s.template_key, t.description AS template_description,
|
||
s.hour, s.minute, s.weekdays, s.timezone, s.enabled,
|
||
s.description, s.updated_at, s.updated_by
|
||
FROM bot.schedules AS s
|
||
JOIN bot.message_templates AS t ON t.key = s.template_key
|
||
ORDER BY s.hour, s.minute, s.id
|
||
"""
|
||
)
|
||
return await cur.fetchall()
|
||
|
||
|
||
async def list_active() -> list[dict[str, Any]]:
|
||
"""Активные расписания — для планировщика бота."""
|
||
async with get_pool().connection() as conn:
|
||
cur = await conn.execute(
|
||
"""
|
||
SELECT id, template_key, hour, minute, weekdays, timezone
|
||
FROM bot.schedules
|
||
WHERE enabled
|
||
ORDER BY hour, minute, id
|
||
"""
|
||
)
|
||
return await cur.fetchall()
|
||
|
||
|
||
async def get_schedule(schedule_id: int) -> dict[str, Any] | None:
|
||
async with get_pool().connection() as conn:
|
||
cur = await conn.execute(
|
||
"""
|
||
SELECT id, template_key, hour, minute, weekdays, timezone, enabled,
|
||
description, updated_at, updated_by
|
||
FROM bot.schedules
|
||
WHERE id = %s
|
||
""",
|
||
(schedule_id,),
|
||
)
|
||
return await cur.fetchone()
|
||
|
||
|
||
async def create_schedule(data: dict[str, Any], *, editor_id: int | None = None) -> int:
|
||
"""Создать расписание и записать его в историю. Возвращает id."""
|
||
async with get_pool().connection() as conn:
|
||
cur = await conn.execute(
|
||
"""
|
||
INSERT INTO bot.schedules
|
||
(template_key, hour, minute, weekdays, timezone, enabled, description, updated_by)
|
||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||
RETURNING id
|
||
""",
|
||
(
|
||
data["template_key"],
|
||
data["hour"],
|
||
data["minute"],
|
||
data["weekdays"],
|
||
data["timezone"],
|
||
data["enabled"],
|
||
data["description"],
|
||
editor_id,
|
||
),
|
||
)
|
||
row = await cur.fetchone()
|
||
schedule_id = int(row["id"])
|
||
await conn.execute(
|
||
"""
|
||
INSERT INTO bot.schedules_history (schedule_id, old_data, new_data, editor_id)
|
||
VALUES (%s, NULL, %s, %s)
|
||
""",
|
||
(schedule_id, Jsonb(data), editor_id),
|
||
)
|
||
return schedule_id
|
||
|
||
|
||
async def update_schedule(schedule_id: int, data: dict[str, Any], *, editor_id: int | None = None) -> bool:
|
||
"""Обновить расписание и записать старое/новое значение. False — если записи нет."""
|
||
async with get_pool().connection() as conn:
|
||
cur = await conn.execute(
|
||
"SELECT * FROM bot.schedules WHERE id = %s FOR UPDATE",
|
||
(schedule_id,),
|
||
)
|
||
row = await cur.fetchone()
|
||
if row is None:
|
||
return False
|
||
|
||
old = _snapshot(row)
|
||
await conn.execute(
|
||
"""
|
||
UPDATE bot.schedules
|
||
SET template_key = %s, hour = %s, minute = %s, weekdays = %s,
|
||
timezone = %s, enabled = %s, description = %s,
|
||
updated_at = now(), updated_by = %s
|
||
WHERE id = %s
|
||
""",
|
||
(
|
||
data["template_key"],
|
||
data["hour"],
|
||
data["minute"],
|
||
data["weekdays"],
|
||
data["timezone"],
|
||
data["enabled"],
|
||
data["description"],
|
||
editor_id,
|
||
schedule_id,
|
||
),
|
||
)
|
||
await conn.execute(
|
||
"""
|
||
INSERT INTO bot.schedules_history (schedule_id, old_data, new_data, editor_id)
|
||
VALUES (%s, %s, %s, %s)
|
||
""",
|
||
(schedule_id, Jsonb(old), Jsonb(data), editor_id),
|
||
)
|
||
return True
|
||
|
||
|
||
async def set_enabled(schedule_id: int, enabled: bool, *, editor_id: int | None = None) -> bool:
|
||
"""Включить/выключить расписание (тоже с записью в историю)."""
|
||
schedule = await get_schedule(schedule_id)
|
||
if schedule is None:
|
||
return False
|
||
data = _snapshot(schedule)
|
||
data["enabled"] = enabled
|
||
return await update_schedule(schedule_id, data, editor_id=editor_id)
|
||
|
||
|
||
async def delete_schedule(schedule_id: int, *, editor_id: int | None = None) -> bool:
|
||
"""Удалить расписание, сохранив последний снимок в истории."""
|
||
async with get_pool().connection() as conn:
|
||
cur = await conn.execute("SELECT * FROM bot.schedules WHERE id = %s FOR UPDATE", (schedule_id,))
|
||
row = await cur.fetchone()
|
||
if row is None:
|
||
return False
|
||
|
||
await conn.execute(
|
||
"""
|
||
INSERT INTO bot.schedules_history (schedule_id, old_data, new_data, editor_id)
|
||
VALUES (%s, %s, NULL, %s)
|
||
""",
|
||
(schedule_id, Jsonb(_snapshot(row)), editor_id),
|
||
)
|
||
await conn.execute("DELETE FROM bot.schedules WHERE id = %s", (schedule_id,))
|
||
return True
|
||
|
||
|
||
async def schedule_history(schedule_id: int, *, limit: int = 10) -> list[dict[str, Any]]:
|
||
async with get_pool().connection() as conn:
|
||
cur = await conn.execute(
|
||
"""
|
||
SELECT id, old_data, new_data, editor_id, changed_at
|
||
FROM bot.schedules_history
|
||
WHERE schedule_id = %s
|
||
ORDER BY changed_at DESC
|
||
LIMIT %s
|
||
""",
|
||
(schedule_id, limit),
|
||
)
|
||
return await cur.fetchall()
|