/
tari.gpt
/
ServitorBot
Обзор
Документация
Войти
/
tari.gpt
/
ServitorBot
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
database.py
1 462 строки
56 KB
tarrri
fix: prevent recurring task parent templates from appearing in task lists
04 дек 2025, 16:19
04 дек 2025, 16:19
38964ad
Код
Авторство
О чём код?
import sqlite3 from datetime import datetime, timedelta import json import logging import notes_database logger = logging.getLogger(__name__) def create_users_table(): """Создание таблицы пользователей""" try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY, username TEXT, first_name TEXT, registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() conn.close() print("Таблица users успешно создана") return True except sqlite3.Error as e: print(f"Ошибка при создании таблицы users: {e}") return False def create_notification_settings_table(): """Создание таблицы настроек уведомлений пользователей""" try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS user_notification_settings ( user_id INTEGER PRIMARY KEY, timezone TEXT DEFAULT 'Europe/Moscow', morning_reminder_enabled BOOLEAN DEFAULT 1, morning_reminder_time TEXT DEFAULT '09:00', evening_reminder_enabled BOOLEAN DEFAULT 1, evening_reminder_time TEXT DEFAULT '17:00', reflection_reminder_enabled BOOLEAN DEFAULT 1, reflection_reminder_time TEXT DEFAULT '21:00', deadline_alert_hours TEXT DEFAULT '1,3', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(user_id) ) ''') # Создаём индекс для быстрого поиска пользователей по времени напоминаний cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_notification_times ON user_notification_settings( morning_reminder_time, evening_reminder_time, reflection_reminder_time, timezone ) ''') conn.commit() conn.close() print("Таблица user_notification_settings успешно создана") return True except sqlite3.Error as e: print(f"Ошибка при создании таблицы user_notification_settings: {e}") return False def init_db(): """Инициализация базы данных и создание таблицы tasks""" try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, client_name TEXT NOT NULL, task TEXT NOT NULL, deadline TEXT, contact_info TEXT, completed INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() conn.close() # Создаём таблицу пользователей create_users_table() # Создаём таблицу настроек уведомлений create_notification_settings_table() # Инициализируем таблицы заметок notes_database.init_notes_db() # Мигрируем поля для повторяющихся задач migrate_recurring_tasks_fields() # Мигрируем настройки ежемесячных инсайтов migrate_monthly_insights_settings() print("База данных успешно инициализирована") return True except sqlite3.Error as e: print(f"Ошибка при инициализации БД: {e}") return False def add_task(user_id, client_name, task, deadline, contact_info=None, deadline_time=None): """ Добавляет новую задачу в базу данных Args: user_id: ID пользователя Telegram client_name: Имя клиента task: Описание задачи deadline: Дедлайн задачи contact_info: Контактная информация клиента (опционально) deadline_time: Время выполнения задачи в формате HH:MM (опционально) Returns: ID новой задачи если успешно добавлена, None в случае ошибки """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() cursor.execute(''' INSERT INTO tasks (user_id, client_name, task, deadline, contact_info, deadline_time) VALUES (?, ?, ?, ?, ?, ?) ''', (user_id, client_name, task, deadline, contact_info, deadline_time)) task_id = cursor.lastrowid conn.commit() conn.close() return task_id except sqlite3.Error as e: print(f"Ошибка при добавлении задачи: {e}") return None def get_active_tasks(user_id): """ Получает список незавершенных задач пользователя Args: user_id: ID пользователя Telegram Returns: Список словарей с данными задач, отсортированный по дедлайну В случае ошибки возвращает пустой список """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row # Позволяет обращаться к колонкам по имени cursor = conn.cursor() # Исключаем родительские паттерны (parent_recurring_id IS NULL AND is_recurring = 1) # Оставляем только обычные задачи и экземпляры повторяющихся задач cursor.execute(''' SELECT id, user_id, client_name, task, deadline, contact_info, deadline_time, created_at, is_recurring, parent_recurring_id, recurrence_instance_date FROM tasks WHERE user_id = ? AND completed = 0 AND NOT (parent_recurring_id IS NULL AND is_recurring = 1) ORDER BY deadline ASC, deadline_time ASC ''', (user_id,)) rows = cursor.fetchall() conn.close() # Преобразуем результат в список словарей tasks = [] for row in rows: tasks.append({ 'id': row['id'], 'user_id': row['user_id'], 'client_name': row['client_name'], 'task': row['task'], 'deadline': row['deadline'], 'contact_info': row['contact_info'], 'deadline_time': row['deadline_time'], 'created_at': row['created_at'], 'is_recurring': row['is_recurring'], 'parent_recurring_id': row['parent_recurring_id'], 'recurrence_instance_date': row['recurrence_instance_date'] }) return tasks except sqlite3.Error as e: print(f"Ошибка при получении задач: {e}") return [] def complete_task(task_id, user_id): """ Отмечает задачу как выполненную. Для повторяющихся задач автоматически генерирует следующий экземпляр, если активных меньше 3. Args: task_id: ID задачи user_id: ID пользователя (для проверки прав доступа) Returns: True если задача успешно отмечена как выполненная False если задача не найдена или произошла ошибка """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() # Проверяем, что задача существует и принадлежит пользователю cursor.execute(''' SELECT id, is_recurring, parent_recurring_id, task, deadline FROM tasks WHERE id = ? AND user_id = ? ''', (task_id, user_id)) task = cursor.fetchone() if task is None: print(f"[COMPLETE_TASK] Задача {task_id} не найдена или не принадлежит пользователю {user_id}") conn.close() return False print(f"[COMPLETE_TASK] User {user_id} завершает задачу ID={task_id}: '{task['task'][:50]}...' (deadline: {task['deadline']}, recurring: {task['is_recurring']}, parent: {task['parent_recurring_id']})") # Отмечаем задачу как выполненную cursor.execute(''' UPDATE tasks SET completed = 1 WHERE id = ? AND user_id = ? ''', (task_id, user_id)) conn.commit() conn.close() print(f"[COMPLETE_TASK] Задача {task_id} успешно отмечена как выполненная") # Если это повторяющаяся задача (экземпляр), генерируем следующий экземпляр при необходимости if task['is_recurring'] and task['parent_recurring_id']: parent_id = task['parent_recurring_id'] print(f"[COMPLETE_TASK] Задача {task_id} - это экземпляр повторяющейся задачи. Генерируем новые экземпляры для parent_id={parent_id}") # Проверяем количество активных экземпляров и генерируем при необходимости created = generate_recurring_instances(parent_id, user_id, target_count=3) print(f"[COMPLETE_TASK] Создано новых экземпляров: {created}") return True except sqlite3.Error as e: print(f"[COMPLETE_TASK] Ошибка при завершении задачи {task_id}: {e}") return False def get_task_by_id(task_id, user_id): """ Получает одну задачу по ID для конкретного пользователя Args: task_id: ID задачи user_id: ID пользователя (для проверки прав доступа) Returns: Словарь с данными задачи или None если задача не найдена """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(''' SELECT id, user_id, client_name, task, deadline, contact_info, deadline_time, created_at, is_recurring, parent_recurring_id, recurrence_instance_date FROM tasks WHERE id = ? AND user_id = ? AND completed = 0 ''', (task_id, user_id)) row = cursor.fetchone() conn.close() if row: return { 'id': row['id'], 'user_id': row['user_id'], 'client_name': row['client_name'], 'task': row['task'], 'deadline': row['deadline'], 'contact_info': row['contact_info'], 'deadline_time': row['deadline_time'], 'created_at': row['created_at'], 'is_recurring': row['is_recurring'], 'parent_recurring_id': row['parent_recurring_id'], 'recurrence_instance_date': row['recurrence_instance_date'] } return None except sqlite3.Error as e: print(f"Ошибка при получении задачи: {e}") return None def update_task_field(task_id, user_id, field_name, new_value): """ Обновляет конкретное поле задачи Args: task_id: ID задачи user_id: ID пользователя (для проверки прав доступа) field_name: Имя поля для обновления (client_name, task, deadline, contact_info) new_value: Новое значение поля Returns: True если поле успешно обновлено, False в случае ошибки """ # Проверяем, что обновляется разрешённое поле allowed_fields = ['client_name', 'task', 'deadline', 'contact_info', 'deadline_time'] if field_name not in allowed_fields: logger.warning(f"⚠️ SECURITY: Попытка обновить недопустимое поле '{field_name}' пользователем {user_id} для задачи {task_id}") print(f"Попытка обновить недопустимое поле: {field_name}") return False try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() # Проверяем, что задача существует и принадлежит пользователю cursor.execute(''' SELECT id FROM tasks WHERE id = ? AND user_id = ? ''', (task_id, user_id)) if cursor.fetchone() is None: logger.warning(f"⚠️ SECURITY: Попытка изменить чужую задачу. User {user_id} пытался изменить task {task_id}") print(f"Задача {task_id} не найдена или не принадлежит пользователю {user_id}") conn.close() return False # Обновляем поле (используем параметризованный запрос для безопасности) query = f"UPDATE tasks SET {field_name} = ? WHERE id = ? AND user_id = ?" cursor.execute(query, (new_value, task_id, user_id)) conn.commit() conn.close() return True except sqlite3.Error as e: print(f"Ошибка при обновлении поля {field_name}: {e}") return False def delete_task(task_id, user_id): """ Удаляет задачу из базы данных Args: task_id: ID задачи user_id: ID пользователя (для проверки прав доступа) Returns: True если задача успешно удалена, False в случае ошибки """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() # Проверяем, что задача существует и принадлежит пользователю cursor.execute(''' SELECT id FROM tasks WHERE id = ? AND user_id = ? ''', (task_id, user_id)) if cursor.fetchone() is None: logger.warning(f"⚠️ SECURITY: Попытка удалить чужую задачу. User {user_id} пытался удалить task {task_id}") print(f"Задача {task_id} не найдена или не принадлежит пользователю {user_id}") conn.close() return False # Удаляем задачу cursor.execute(''' DELETE FROM tasks WHERE id = ? AND user_id = ? ''', (task_id, user_id)) conn.commit() conn.close() return True except sqlite3.Error as e: print(f"Ошибка при удалении задачи: {e}") return False def get_tasks_for_today(user_id, target_date): """ Получает список незавершенных задач на конкретную дату (сегодня) Args: user_id: ID пользователя Telegram target_date: Дата в формате YYYY-MM-DD (строка) Returns: Список словарей с данными задач (id, client_name, contact_info, task, deadline, deadline_time) В случае ошибки возвращает пустой список """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(''' SELECT id, client_name, contact_info, task, deadline, deadline_time FROM tasks WHERE user_id = ? AND completed = 0 AND deadline = ? AND NOT (parent_recurring_id IS NULL AND is_recurring = 1) ORDER BY deadline_time ASC, id ASC ''', (user_id, target_date)) rows = cursor.fetchall() conn.close() # Преобразуем результат в список словарей tasks = [] for row in rows: tasks.append({ 'id': row['id'], 'client_name': row['client_name'], 'contact_info': row['contact_info'], 'task': row['task'], 'deadline': row['deadline'], 'deadline_time': row['deadline_time'] }) return tasks except sqlite3.Error as e: print(f"Ошибка при получении задач на сегодня: {e}") return [] def get_tasks_for_tomorrow(user_id, target_date): """ Получает список незавершенных задач на конкретную дату (завтра) Args: user_id: ID пользователя Telegram target_date: Дата в формате YYYY-MM-DD (строка) Returns: Список словарей с данными задач (id, client_name, contact_info, task, deadline) В случае ошибки возвращает пустой список """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(''' SELECT id, client_name, contact_info, task, deadline, deadline_time FROM tasks WHERE user_id = ? AND completed = 0 AND deadline = ? AND NOT (parent_recurring_id IS NULL AND is_recurring = 1) ORDER BY deadline_time ASC, id ASC ''', (user_id, target_date)) rows = cursor.fetchall() conn.close() # Преобразуем результат в список словарей tasks = [] for row in rows: tasks.append({ 'id': row['id'], 'client_name': row['client_name'], 'contact_info': row['contact_info'], 'task': row['task'], 'deadline': row['deadline'], 'deadline_time': row['deadline_time'] }) return tasks except sqlite3.Error as e: print(f"Ошибка при получении задач на завтра: {e}") return [] def get_overdue_tasks(user_id, current_date): """ Получает список просроченных незавершенных задач Args: user_id: ID пользователя Telegram current_date: Текущая дата в формате YYYY-MM-DD (строка) Returns: Список словарей с данными задач (id, client_name, contact_info, task, deadline) В случае ошибки возвращает пустой список """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(''' SELECT id, client_name, contact_info, task, deadline, deadline_time FROM tasks WHERE user_id = ? AND completed = 0 AND deadline < ? AND deadline != 'нет' AND NOT (parent_recurring_id IS NULL AND is_recurring = 1) ORDER BY deadline ASC, deadline_time ASC ''', (user_id, current_date)) rows = cursor.fetchall() conn.close() # Преобразуем результат в список словарей tasks = [] for row in rows: tasks.append({ 'id': row['id'], 'client_name': row['client_name'], 'contact_info': row['contact_info'], 'task': row['task'], 'deadline': row['deadline'], 'deadline_time': row['deadline_time'] }) return tasks except sqlite3.Error as e: print(f"Ошибка при получении просроченных задач: {e}") return [] def add_user(user_id, username=None, first_name=None): """ Добавляет пользователя в таблицу users Args: user_id: ID пользователя Telegram username: Имя пользователя в Telegram (опционально) first_name: Имя пользователя (опционально) Returns: True если пользователь был добавлен False если пользователь уже существовал """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() # Пытаемся добавить пользователя, если уже существует - игнорируем cursor.execute(''' INSERT OR IGNORE INTO users (user_id, username, first_name) VALUES (?, ?, ?) ''', (user_id, username, first_name)) # Проверяем, была ли произведена вставка rows_affected = cursor.rowcount conn.commit() conn.close() return rows_affected > 0 except sqlite3.Error as e: print(f"Ошибка при добавлении пользователя: {e}") return False def user_exists(user_id): """ Проверяет существует ли пользователь в таблице Args: user_id: ID пользователя Telegram Returns: True если пользователь существует, False если нет """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() cursor.execute(''' SELECT user_id FROM users WHERE user_id = ? ''', (user_id,)) result = cursor.fetchone() conn.close() return result is not None except sqlite3.Error as e: print(f"Ошибка при проверке пользователя: {e}") return False def get_all_users(): """ Возвращает список всех пользователей Returns: Список словарей с данными пользователей [{user_id, username, first_name}, ...] В случае ошибки возвращает пустой список """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(''' SELECT user_id, username, first_name FROM users ORDER BY registered_at ASC ''') rows = cursor.fetchall() conn.close() # Преобразуем результат в список словарей users = [] for row in rows: users.append({ 'user_id': row['user_id'], 'username': row['username'], 'first_name': row['first_name'] }) return users except sqlite3.Error as e: print(f"Ошибка при получении списка пользователей: {e}") return [] def get_user_by_id(user_id): """ Возвращает данные одного пользователя Args: user_id: ID пользователя Telegram Returns: Словарь с данными пользователя {user_id, username, first_name} или None """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(''' SELECT user_id, username, first_name FROM users WHERE user_id = ? ''', (user_id,)) row = cursor.fetchone() conn.close() if row: return { 'user_id': row['user_id'], 'username': row['username'], 'first_name': row['first_name'] } return None except sqlite3.Error as e: print(f"Ошибка при получении пользователя: {e}") return None def migrate_user_notification_settings(): """ Создает дефолтные настройки уведомлений для существующих пользователей, у которых еще нет записи в таблице user_notification_settings. Returns: Количество пользователей, для которых были созданы настройки """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() # Получить пользователей без настроек cursor.execute(''' SELECT u.user_id FROM users u LEFT JOIN user_notification_settings s ON u.user_id = s.user_id WHERE s.user_id IS NULL ''') users_to_migrate = cursor.fetchall() # Создать дефолтные настройки для каждого пользователя for (user_id,) in users_to_migrate: cursor.execute(''' INSERT INTO user_notification_settings (user_id, timezone, morning_reminder_enabled, morning_reminder_time, evening_reminder_enabled, evening_reminder_time, reflection_reminder_enabled, reflection_reminder_time, deadline_alert_hours) VALUES (?, 'Europe/Moscow', 1, '09:00', 1, '17:00', 1, '21:00', '1,3') ''', (user_id,)) conn.commit() conn.close() count = len(users_to_migrate) if count > 0: print(f"Мигрировано настроек для {count} пользователей") return count except sqlite3.Error as e: print(f"Ошибка при миграции настроек уведомлений: {e}") return 0 def migrate_recurring_tasks_fields(): """ Добавляет поля для повторяющихся задач в таблицу tasks, если их еще нет. Новые поля: - is_recurring: флаг повторяющейся задачи - recurrence_pattern: JSON с паттерном повторения - recurrence_end_date: дата окончания повторений - parent_recurring_id: ID родительского паттерна - recurrence_instance_date: дата конкретного экземпляра Returns: True если миграция прошла успешно, False в случае ошибки """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() # Получаем список существующих колонок cursor.execute("PRAGMA table_info(tasks)") existing_columns = {row[1] for row in cursor.fetchall()} # Словарь новых полей и их определений new_fields = { 'is_recurring': 'INTEGER DEFAULT 0', 'recurrence_pattern': 'TEXT', 'recurrence_end_date': 'TEXT', 'parent_recurring_id': 'INTEGER', 'recurrence_instance_date': 'TEXT' } # Добавляем только те поля, которых еще нет fields_added = [] for field_name, field_definition in new_fields.items(): if field_name not in existing_columns: cursor.execute(f'ALTER TABLE tasks ADD COLUMN {field_name} {field_definition}') fields_added.append(field_name) print(f"Добавлено поле {field_name} в таблицу tasks") conn.commit() conn.close() if fields_added: print(f"Миграция завершена: добавлено полей - {len(fields_added)}") else: print("Все поля для повторяющихся задач уже существуют") return True except sqlite3.Error as e: print(f"Ошибка при миграции полей для повторяющихся задач: {e}") return False # ============================================================================ # Функции для работы с повторяющимися задачами # ============================================================================ def calculate_next_occurrence(current_date_str, pattern_dict): """ Вычисляет следующую дату повторения на основе паттерна. Args: current_date_str: Текущая дата в формате YYYY-MM-DD или объект datetime.date pattern_dict: Словарь с паттерном повторения {type, interval, days_of_week, day_of_month} Returns: Следующая дата в формате YYYY-MM-DD или None если повторение невозможно """ try: # Преобразуем в datetime, если передан объект date if isinstance(current_date_str, datetime): current_date = current_date_str elif hasattr(current_date_str, 'year'): # datetime.date object current_date = datetime.combine(current_date_str, datetime.min.time()) else: current_date = datetime.strptime(current_date_str, '%Y-%m-%d') pattern_type = pattern_dict.get('type') interval = pattern_dict.get('interval', 1) if pattern_type == 'daily': next_date = current_date + timedelta(days=interval) elif pattern_type == 'weekly': days_of_week = pattern_dict.get('days_of_week', []) if days_of_week: # Найти следующий подходящий день недели for i in range(1, 8): candidate = current_date + timedelta(days=i) # weekday(): 0=понедельник, 6=воскресенье if candidate.weekday() in days_of_week: next_date = candidate break else: return None else: next_date = current_date + timedelta(weeks=interval) elif pattern_type == 'monthly': day_of_month = pattern_dict.get('day_of_month', current_date.day) # Переходим к следующему месяцу next_month = current_date.month + interval next_year = current_date.year while next_month > 12: next_month -= 12 next_year += 1 # Обрабатываем случай, когда день месяца не существует try: next_date = current_date.replace(year=next_year, month=next_month, day=day_of_month) except ValueError: # Если такого дня нет, берём последний день месяца import calendar last_day = calendar.monthrange(next_year, next_month)[1] next_date = current_date.replace(year=next_year, month=next_month, day=last_day) else: return None return next_date.strftime('%Y-%m-%d') except Exception as e: print(f"Ошибка при вычислении следующей даты повторения: {e}") return None def add_recurring_task(user_id, client_name, task, first_deadline, contact_info=None, deadline_time=None, recurrence_pattern=None, recurrence_end_date=None, instances_to_generate=3): """ Создаёт повторяющуюся задачу и генерирует N первых экземпляров. Args: user_id: ID пользователя client_name: Имя клиента task: Описание задачи first_deadline: Первый дедлайн в формате YYYY-MM-DD contact_info: Контактная информация (опционально) deadline_time: Время дедлайна HH:MM (опционально) recurrence_pattern: Словарь с паттерном повторения recurrence_end_date: Дата окончания повторений (опционально) instances_to_generate: Количество экземпляров для генерации (по умолчанию 3) Returns: ID родительского паттерна или None в случае ошибки """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() # Создаём родительский паттерн (без конкретной даты экземпляра) pattern_json = json.dumps(recurrence_pattern, ensure_ascii=False) if recurrence_pattern else None cursor.execute(''' INSERT INTO tasks (user_id, client_name, task, deadline, contact_info, deadline_time, is_recurring, recurrence_pattern, recurrence_end_date, parent_recurring_id) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, NULL) ''', (user_id, client_name, task, first_deadline, contact_info, deadline_time, pattern_json, recurrence_end_date)) parent_id = cursor.lastrowid # Генерируем первые N экземпляров current_date = first_deadline for _ in range(instances_to_generate): if recurrence_end_date and current_date > recurrence_end_date: break cursor.execute(''' INSERT INTO tasks (user_id, client_name, task, deadline, contact_info, deadline_time, is_recurring, parent_recurring_id, recurrence_instance_date) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?) ''', (user_id, client_name, task, current_date, contact_info, deadline_time, parent_id, current_date)) # Вычисляем следующую дату if recurrence_pattern: next_date = calculate_next_occurrence(current_date, recurrence_pattern) if next_date: current_date = next_date else: break else: break conn.commit() conn.close() return parent_id except Exception as e: print(f"Ошибка при создании повторяющейся задачи: {e}") return None def count_active_instances(parent_id, user_id): """ Подсчитывает количество активных (незавершенных) экземпляров повторяющейся задачи. Args: parent_id: ID родительского паттерна user_id: ID пользователя Returns: Количество активных экземпляров """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() cursor.execute(''' SELECT COUNT(*) FROM tasks WHERE parent_recurring_id = ? AND user_id = ? AND completed = 0 ''', (parent_id, user_id)) count = cursor.fetchone()[0] conn.close() return count except Exception as e: print(f"Ошибка при подсчёте активных экземпляров: {e}") return 0 def generate_recurring_instances(parent_id, user_id, target_count=3): """ Генерирует экземпляры повторяющейся задачи до достижения целевого количества. Args: parent_id: ID родительского паттерна user_id: ID пользователя target_count: Целевое количество активных экземпляров Returns: Количество созданных экземпляров """ try: print(f"[GENERATE_INSTANCES] Начало генерации экземпляров для parent_id={parent_id}, user_id={user_id}, target_count={target_count}") conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() # Получаем родительский паттерн cursor.execute(''' SELECT recurrence_pattern, recurrence_end_date, client_name, task, contact_info, deadline_time FROM tasks WHERE id = ? AND user_id = ? AND parent_recurring_id IS NULL ''', (parent_id, user_id)) parent = cursor.fetchone() if not parent or not parent['recurrence_pattern']: print(f"[GENERATE_INSTANCES] Родительская задача {parent_id} не найдена или не имеет паттерна повторения") conn.close() return 0 pattern = json.loads(parent['recurrence_pattern']) end_date = parent['recurrence_end_date'] print(f"[GENERATE_INSTANCES] Родитель найден: '{parent['task'][:50]}...', паттерн: {pattern}") # Подсчитываем текущее количество активных экземпляров current_count = count_active_instances(parent_id, user_id) instances_to_create = target_count - current_count print(f"[GENERATE_INSTANCES] Активных экземпляров: {current_count}, нужно создать: {instances_to_create}") if instances_to_create <= 0: print(f"[GENERATE_INSTANCES] Экземпляры не нужны (уже есть {current_count})") conn.close() return 0 # Находим последний экземпляр по дате cursor.execute(''' SELECT recurrence_instance_date FROM tasks WHERE parent_recurring_id = ? AND user_id = ? ORDER BY recurrence_instance_date DESC LIMIT 1 ''', (parent_id, user_id)) last_instance = cursor.fetchone() if last_instance and last_instance['recurrence_instance_date']: current_date = last_instance['recurrence_instance_date'] print(f"[GENERATE_INSTANCES] Последний экземпляр имеет дату: {current_date}") else: print(f"[GENERATE_INSTANCES] Не найден последний экземпляр - прерываем генерацию") conn.close() return 0 # Создаём новые экземпляры created_count = 0 for i in range(instances_to_create): next_date = calculate_next_occurrence(current_date, pattern) if not next_date: print(f"[GENERATE_INSTANCES] Не удалось рассчитать следующую дату после {current_date}") break if end_date and next_date > end_date: print(f"[GENERATE_INSTANCES] Следующая дата {next_date} превышает дату окончания {end_date}") break cursor.execute(''' INSERT INTO tasks (user_id, client_name, task, deadline, contact_info, deadline_time, is_recurring, parent_recurring_id, recurrence_instance_date) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?) ''', (user_id, parent['client_name'], parent['task'], next_date, parent['contact_info'], parent['deadline_time'], parent_id, next_date)) new_id = cursor.lastrowid created_count += 1 print(f"[GENERATE_INSTANCES] Создан экземпляр #{i+1}: ID={new_id}, deadline={next_date}, completed=0 (по умолчанию)") current_date = next_date conn.commit() conn.close() print(f"[GENERATE_INSTANCES] Генерация завершена. Создано экземпляров: {created_count}") return created_count except Exception as e: print(f"[GENERATE_INSTANCES] Ошибка при генерации экземпляров для parent_id={parent_id}: {e}") import traceback traceback.print_exc() return 0 def get_recurring_pattern(task_id, user_id): """ Получает паттерн повторения для задачи (либо сам паттерн, либо родительский). Args: task_id: ID задачи user_id: ID пользователя Returns: Словарь с данными паттерна или None """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() # Сначала получаем задачу cursor.execute(''' SELECT id, parent_recurring_id, recurrence_pattern, is_recurring FROM tasks WHERE id = ? AND user_id = ? ''', (task_id, user_id)) task = cursor.fetchone() if not task: conn.close() return None # Если это экземпляр, получаем родительский паттерн if task['parent_recurring_id']: cursor.execute(''' SELECT id, recurrence_pattern, recurrence_end_date, client_name, task, contact_info, deadline, deadline_time FROM tasks WHERE id = ? AND user_id = ? ''', (task['parent_recurring_id'], user_id)) pattern_task = cursor.fetchone() else: pattern_task = task conn.close() if pattern_task and pattern_task['recurrence_pattern']: return { 'id': pattern_task['id'], 'pattern': json.loads(pattern_task['recurrence_pattern']), 'end_date': pattern_task.get('recurrence_end_date'), 'client_name': pattern_task.get('client_name'), 'task': pattern_task.get('task'), 'contact_info': pattern_task.get('contact_info'), 'deadline': pattern_task.get('deadline'), 'deadline_time': pattern_task.get('deadline_time') } return None except Exception as e: print(f"Ошибка при получении паттерна повторения: {e}") return None def update_recurring_pattern(parent_id, user_id, new_pattern=None, new_end_date=None, update_future_instances=True): """ Обновляет паттерн повторения и опционально все будущие экземпляры. Args: parent_id: ID родительского паттерна user_id: ID пользователя new_pattern: Новый паттерн повторения (словарь) new_end_date: Новая дата окончания update_future_instances: Обновлять ли будущие экземпляры Returns: True если обновление прошло успешно, False иначе """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() # Обновляем родительский паттерн if new_pattern: pattern_json = json.dumps(new_pattern, ensure_ascii=False) cursor.execute(''' UPDATE tasks SET recurrence_pattern = ?, recurrence_end_date = ? WHERE id = ? AND user_id = ? AND parent_recurring_id IS NULL ''', (pattern_json, new_end_date, parent_id, user_id)) if update_future_instances: # Удаляем все будущие незавершенные экземпляры today = datetime.now().strftime('%Y-%m-%d') cursor.execute(''' DELETE FROM tasks WHERE parent_recurring_id = ? AND user_id = ? AND completed = 0 AND deadline >= ? ''', (parent_id, user_id, today)) conn.commit() conn.close() # Если нужно, генерируем новые экземпляры if update_future_instances and new_pattern: generate_recurring_instances(parent_id, user_id, target_count=3) return True except Exception as e: print(f"Ошибка при обновлении паттерна повторения: {e}") return False def delete_recurring_task(task_id, user_id, delete_all_future=False): """ Удаляет повторяющуюся задачу с опцией удаления всех будущих экземпляров. Args: task_id: ID задачи (экземпляра или паттерна) user_id: ID пользователя delete_all_future: Удалить все будущие экземпляры Returns: True если удаление прошло успешно, False иначе """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() # Получаем информацию о задаче cursor.execute(''' SELECT id, parent_recurring_id, recurrence_instance_date FROM tasks WHERE id = ? AND user_id = ? ''', (task_id, user_id)) task = cursor.fetchone() if not task: conn.close() return False if delete_all_future: # Определяем parent_id parent_id = task['parent_recurring_id'] if task['parent_recurring_id'] else task_id # Удаляем все будущие экземпляры (включая текущий) if task['recurrence_instance_date']: current_date = task['recurrence_instance_date'] cursor.execute(''' DELETE FROM tasks WHERE parent_recurring_id = ? AND user_id = ? AND recurrence_instance_date >= ? ''', (parent_id, user_id, current_date)) # Если удаляем сам паттерн, удаляем и все его экземпляры if not task['parent_recurring_id']: cursor.execute(''' DELETE FROM tasks WHERE parent_recurring_id = ? AND user_id = ? ''', (task_id, user_id)) cursor.execute(''' DELETE FROM tasks WHERE id = ? AND user_id = ? ''', (task_id, user_id)) else: # Удаляем только эту задачу cursor.execute(''' DELETE FROM tasks WHERE id = ? AND user_id = ? ''', (task_id, user_id)) conn.commit() conn.close() return True except Exception as e: print(f"Ошибка при удалении повторяющейся задачи: {e}") return False def stop_recurring_pattern(task_id, user_id): """ Полностью останавливает повторение задачи, удаляя паттерн и все его экземпляры. Args: task_id: ID задачи (может быть экземпляр или паттерн) user_id: ID пользователя Returns: True если паттерн успешно остановлен, False иначе """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() # Получаем информацию о задаче cursor.execute(''' SELECT id, parent_recurring_id, is_recurring FROM tasks WHERE id = ? AND user_id = ? ''', (task_id, user_id)) task = cursor.fetchone() if not task: conn.close() return False # Если это не повторяющаяся задача, используем обычное удаление if not task['is_recurring']: cursor.execute('DELETE FROM tasks WHERE id = ? AND user_id = ?', (task_id, user_id)) conn.commit() conn.close() return True # Определяем ID паттерна if task['parent_recurring_id']: # Это экземпляр - берем parent_id pattern_id = task['parent_recurring_id'] else: # Это сам паттерн pattern_id = task_id # Удаляем ВСЕ экземпляры (включая завершенные) cursor.execute(''' DELETE FROM tasks WHERE parent_recurring_id = ? AND user_id = ? ''', (pattern_id, user_id)) deleted_instances = cursor.rowcount # Удаляем сам паттерн cursor.execute(''' DELETE FROM tasks WHERE id = ? AND user_id = ? ''', (pattern_id, user_id)) deleted_pattern = cursor.rowcount conn.commit() conn.close() print(f"Паттерн {pattern_id} полностью остановлен: удалено {deleted_instances} экземпляров + {deleted_pattern} паттерн") return True except Exception as e: print(f"Ошибка при остановке паттерна повторения: {e}") return False def get_active_recurring_patterns(user_id=None): """ Получает список всех активных паттернов повторения. Args: user_id: ID пользователя (опционально, если None - для всех пользователей) Returns: Список словарей с данными паттернов """ try: conn = sqlite3.connect('tasks.db') conn.row_factory = sqlite3.Row cursor = conn.cursor() if user_id: cursor.execute(''' SELECT id, user_id, client_name, task, contact_info, deadline, deadline_time, recurrence_pattern, recurrence_end_date FROM tasks WHERE user_id = ? AND is_recurring = 1 AND parent_recurring_id IS NULL ''', (user_id,)) else: cursor.execute(''' SELECT id, user_id, client_name, task, contact_info, deadline, deadline_time, recurrence_pattern, recurrence_end_date FROM tasks WHERE is_recurring = 1 AND parent_recurring_id IS NULL ''') rows = cursor.fetchall() conn.close() patterns = [] for row in rows: pattern_dict = json.loads(row['recurrence_pattern']) if row['recurrence_pattern'] else None patterns.append({ 'id': row['id'], 'user_id': row['user_id'], 'client_name': row['client_name'], 'task': row['task'], 'contact_info': row['contact_info'], 'deadline': row['deadline'], 'deadline_time': row['deadline_time'], 'pattern': pattern_dict, 'end_date': row['recurrence_end_date'] }) return patterns except Exception as e: print(f"Ошибка при получении активных паттернов: {e}") return [] def migrate_monthly_insights_settings(): """ Добавляет поля для ежемесячных инсайтов в user_notification_settings и создает таблицу sent_monthly_digests. Безопасная миграция с проверкой существования полей и таблиц. """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() # Проверяем существование поля monthly_insights_enabled cursor.execute("PRAGMA table_info(user_notification_settings)") columns = [column[1] for column in cursor.fetchall()] if 'monthly_insights_enabled' not in columns: cursor.execute(''' ALTER TABLE user_notification_settings ADD COLUMN monthly_insights_enabled BOOLEAN DEFAULT 1 ''') logger.info("Добавлено поле monthly_insights_enabled") if 'monthly_insights_time' not in columns: cursor.execute(''' ALTER TABLE user_notification_settings ADD COLUMN monthly_insights_time TEXT DEFAULT '20:00' ''') logger.info("Добавлено поле monthly_insights_time") # Создаем таблицу sent_monthly_digests если не существует cursor.execute(''' CREATE TABLE IF NOT EXISTS sent_monthly_digests ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, year INTEGER NOT NULL, month INTEGER NOT NULL, sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, insights_count INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(user_id), UNIQUE(user_id, year, month) ) ''') logger.info("Таблица sent_monthly_digests проверена/создана") # Создаем индекс для быстрого поиска cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_sent_digests ON sent_monthly_digests(user_id, year, month) ''') conn.commit() conn.close() return True except sqlite3.Error as e: logger.error(f"Ошибка миграции monthly insights: {e}") return False def is_monthly_digest_sent(user_id, year, month): """ Проверяет, был ли отправлен дайджест за указанный месяц. Args: user_id: ID пользователя year: Год month: Месяц (1-12) Returns: bool: True если дайджест уже отправлен, False иначе """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() cursor.execute(''' SELECT COUNT(*) FROM sent_monthly_digests WHERE user_id = ? AND year = ? AND month = ? ''', (user_id, year, month)) count = cursor.fetchone()[0] conn.close() return count > 0 except sqlite3.Error as e: logger.error(f"Ошибка проверки отправленного дайджеста: {e}") return False def mark_monthly_digest_sent(user_id, year, month, insights_count): """ Отмечает, что дайджест за месяц был отправлен. Args: user_id: ID пользователя year: Год month: Месяц (1-12) insights_count: Количество инсайтов в дайджесте Returns: bool: True при успехе, False при ошибке """ try: conn = sqlite3.connect('tasks.db') cursor = conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO sent_monthly_digests (user_id, year, month, insights_count) VALUES (?, ?, ?, ?) ''', (user_id, year, month, insights_count)) conn.commit() conn.close() return True except sqlite3.Error as e: logger.error(f"Ошибка сохранения отправленного дайджеста: {e}") return False