From bf958e1c12da37cd3e29cbc8c6d053dc4a2fa47f Mon Sep 17 00:00:00 2001 From: Kenik Date: Wed, 2 Sep 2026 21:37:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=BE=D0=BC=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D0=BA=D0=B8:=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=86=D0=B0=20reminders=20=D0=B8=20CRUD=20=D0=B2=20database.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- database.py | 76 ++++++++++++++++++++++++++++++++++++++++++ tests/test_database.py | 27 +++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/database.py b/database.py index 767a1c7..badcd27 100644 --- a/database.py +++ b/database.py @@ -192,6 +192,21 @@ class DatabaseManager: ) """) + # Table for admin reminders (настраиваемые напоминания: дата, текст, периодичность) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text TEXT NOT NULL, + remind_at TEXT NOT NULL, -- якорная дата первого срабатывания, не меняется + next_at TEXT NOT NULL, -- следующее срабатывание, двигается вперёд + recurrence TEXT NOT NULL, -- 'once' | 'weekly' | 'monthly' | 'yearly' + recipients TEXT NOT NULL, -- JSON: список vk_id, либо строка "all" + created_by INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + active INTEGER NOT NULL DEFAULT 1 + ) + """) + conn.commit() # ---------- Редактируемые ссылки (кнопки записи/оплаты) ---------- @@ -606,6 +621,67 @@ class DatabaseManager: cursor.execute("SELECT user_id FROM admins ORDER BY user_id") return [r[0] for r in cursor.fetchall()] + # --- Напоминалки для админов (дата/время, периодичность) --- + def add_reminder(self, text: str, remind_at: str, recurrence: str, + recipients, created_by: int) -> int: + """recipients — список vk_id (list[int]) либо строка 'all'.""" + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT INTO reminders (text, remind_at, next_at, recurrence, recipients, created_by) " + "VALUES (?, ?, ?, ?, ?, ?)", + (text, remind_at, remind_at, recurrence, json.dumps(recipients), created_by) + ) + conn.commit() + return cursor.lastrowid + + def _rows_with_parsed_recipients(self, cursor) -> list: + rows = [dict(r) for r in cursor.fetchall()] + for r in rows: + r["recipients"] = json.loads(r["recipients"]) + return rows + + def get_active_reminders(self) -> list: + """Активные напоминания для отображения в админке, по next_at.""" + with self._get_connection() as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute( + "SELECT id, text, remind_at, next_at, recurrence, recipients, created_by " + "FROM reminders WHERE active = 1 ORDER BY next_at" + ) + return self._rows_with_parsed_recipients(cursor) + + def get_due_reminders(self, now_str: str) -> list: + """Активные напоминания, чьё время (next_at) уже наступило.""" + with self._get_connection() as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute( + "SELECT id, text, remind_at, next_at, recurrence, recipients, created_by " + "FROM reminders WHERE active = 1 AND next_at <= ?", (now_str,) + ) + return self._rows_with_parsed_recipients(cursor) + + def update_reminder_next_at(self, reminder_id: int, next_at: str): + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("UPDATE reminders SET next_at = ? WHERE id = ?", (next_at, reminder_id)) + conn.commit() + + def deactivate_reminder(self, reminder_id: int): + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("UPDATE reminders SET active = 0 WHERE id = ?", (reminder_id,)) + conn.commit() + + def delete_reminder(self, reminder_id: int) -> bool: + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM reminders WHERE id = ?", (reminder_id,)) + conn.commit() + return cursor.rowcount > 0 + # --- Быстрые ответы (custom FAQ) --- def add_custom_faq(self, keyword: str, answer: str) -> int: with self._get_connection() as conn: diff --git a/tests/test_database.py b/tests/test_database.py index 1f7cfda..4a7ce4c 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -56,3 +56,30 @@ def test_marketing_subscribers(tmp_db): tmp_db.update_user_field(11, "consent_2", "Не согласен") subs = tmp_db.get_marketing_subscribers() assert 10 in subs and 11 not in subs + + +def test_reminders_crud(tmp_db): + rid = tmp_db.add_reminder("Продлить домен", "2027-03-15 09:00:00", "yearly", [1, 2], created_by=1) + active = tmp_db.get_active_reminders() + assert any(r["id"] == rid for r in active) + r = next(r for r in active if r["id"] == rid) + assert r["recipients"] == [1, 2] and r["recurrence"] == "yearly" + + due = tmp_db.get_due_reminders("2027-03-15 09:00:00") + assert any(r["id"] == rid for r in due) + assert tmp_db.get_due_reminders("2027-03-14 09:00:00") == [] + + tmp_db.update_reminder_next_at(rid, "2028-03-15 09:00:00") + r2 = next(r for r in tmp_db.get_active_reminders() if r["id"] == rid) + assert r2["next_at"] == "2028-03-15 09:00:00" + + tmp_db.deactivate_reminder(rid) + assert not any(r["id"] == rid for r in tmp_db.get_active_reminders()) + + +def test_reminders_all_recipients_and_delete(tmp_db): + rid = tmp_db.add_reminder("Оплатить сервер", "2027-01-01 09:00:00", "once", "all", created_by=1) + r = next(r for r in tmp_db.get_active_reminders() if r["id"] == rid) + assert r["recipients"] == "all" + assert tmp_db.delete_reminder(rid) + assert not tmp_db.delete_reminder(rid) # уже удалено, повторное удаление — False