Add delete functionality for tasks and events in Telegram bot. Implemented commands for deleting items, including weekly tasks with scope options. Enhanced main menu and button handlers for better user interaction. Updated state management for deletion process.

This commit is contained in:
vrubel 2025-12-30 13:54:59 +03:00
parent 8bde59e953
commit 457dc74485

View file

@ -4,7 +4,7 @@ from telegram.ext import (
ContextTypes, ConversationHandler, filters
)
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy import select, and_
from datetime import datetime, timedelta
import pytz
import os
@ -24,6 +24,7 @@ BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
# Состояния для ConversationHandler
SELECTING_DATE, SELECTING_HOUR, SELECTING_MINUTE, SELECTING_DURATION_HOUR, SELECTING_DURATION_MIN, ENTERING_TITLE = range(6)
SELECTING_TASK_DATE, ENTERING_TASK_TITLE, SELECTING_REPEAT, SELECTING_COPY_DAYS = range(10, 14)
SELECTING_DELETE_DATE, SELECTING_DELETE_ITEM = range(20, 22)
user_states = {} # Временное хранилище состояний пользователей
@ -100,23 +101,70 @@ def calculate_end_time(start_time: str, duration_min: int) -> str:
return f"{end_hour:02d}:{end_minute:02d}"
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /start"""
await register_user(str(update.effective_chat.id))
async def show_main_menu(message):
"""Показать главное меню с кнопками"""
keyboard = [
[InlineKeyboardButton("📅 Сегодня", callback_data="schedule_today")],
[InlineKeyboardButton("📅 Завтра", callback_data="schedule_tomorrow")],
[InlineKeyboardButton("📅 Оставшиеся дни недели", callback_data="schedule_week")],
[InlineKeyboardButton(" Добавить занятие", callback_data="add_event")],
[InlineKeyboardButton(" Добавить задачу", callback_data="add_task")],
[InlineKeyboardButton("🗑 Удалить", callback_data="delete_start")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.reply_text(
"Выберите действие:",
reply_markup=reply_markup
)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /start"""
await register_user(str(update.effective_chat.id))
await show_main_menu(update.message)
async def today_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /today"""
await register_user(str(update.effective_chat.id))
today = datetime.now(TZ).date()
date_str = today.strftime("%Y-%m-%d")
text = await get_schedule_for_date(date_str)
await update.message.reply_text(text)
async def nextday_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /nextday"""
await register_user(str(update.effective_chat.id))
today = datetime.now(TZ).date()
tomorrow = today + timedelta(days=1)
date_str = tomorrow.strftime("%Y-%m-%d")
text = await get_schedule_for_date(date_str)
await update.message.reply_text(text)
async def addtask_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /addtask"""
await register_user(str(update.effective_chat.id))
keyboard = [
[InlineKeyboardButton(" Добавить занятие", callback_data="add_event")],
[InlineKeyboardButton(" Добавить задачу", callback_data="add_task")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"Выберите действие:",
"Что вы хотите добавить?",
reply_markup=reply_markup
)
async def deltask_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /deltask"""
await register_user(str(update.effective_chat.id))
chat_id = str(update.effective_chat.id)
user_states[chat_id] = {"action": "delete"}
await show_date_selection_for_delete(update.message)
return SELECTING_DELETE_DATE
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик кнопок"""
query = update.callback_query
@ -126,26 +174,23 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
today = datetime.now(TZ).date()
if query.data == "schedule_today":
# Расписание на сегодня (аналог /today) - отправляем новое сообщение
date_str = today.strftime("%Y-%m-%d")
text = await get_schedule_for_date(date_str)
await query.edit_message_text(text)
await query.message.reply_text(text)
elif query.data == "schedule_tomorrow":
# Расписание на завтра (аналог /nextday) - отправляем новое сообщение
tomorrow = today + timedelta(days=1)
date_str = tomorrow.strftime("%Y-%m-%d")
text = await get_schedule_for_date(date_str)
await query.edit_message_text(text)
await query.message.reply_text(text)
elif query.data == "schedule_week":
# Оставшиеся дни недели
weekday = today.weekday()
text = ""
for i in range(weekday, 7):
date = today + timedelta(days=i - weekday)
date_str = date.strftime("%Y-%m-%d")
day_text = await get_schedule_for_date(date_str)
text += day_text + "\n"
await query.edit_message_text(text[:4000]) # Telegram limit
elif query.data == "delete_start":
# Начало процесса удаления (аналог /deltask)
user_states[chat_id] = {"action": "delete"}
await show_date_selection_for_delete(query)
return SELECTING_DELETE_DATE
elif query.data == "add_event":
# Начинаем процесс добавления event
@ -159,6 +204,15 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
await show_date_selection(query, "task")
return SELECTING_TASK_DATE
elif query.data.startswith("del_date_"):
# Выбрана дата для удаления
date_str = query.data.replace("del_date_", "")
if chat_id not in user_states:
user_states[chat_id] = {}
user_states[chat_id]["delete_date"] = date_str
await show_items_for_deletion(query, date_str)
return SELECTING_DELETE_ITEM
elif query.data.startswith("date_"):
# Выбрана дата
date_str = query.data.replace("date_", "")
@ -197,16 +251,31 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
await query.edit_message_text("Введите название занятия:")
return ENTERING_TITLE
# Возвращаемся в главное меню
keyboard = [
[InlineKeyboardButton("📅 Сегодня", callback_data="schedule_today")],
[InlineKeyboardButton("📅 Завтра", callback_data="schedule_tomorrow")],
[InlineKeyboardButton("📅 Оставшиеся дни недели", callback_data="schedule_week")],
[InlineKeyboardButton(" Добавить занятие", callback_data="add_event")],
[InlineKeyboardButton(" Добавить задачу", callback_data="add_task")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите действие:", reply_markup=reply_markup)
elif query.data.startswith("delete_"):
# Удаление элемента (формат: delete_ID_kind)
parts = query.data.replace("delete_", "").split("_")
if len(parts) >= 2:
item_id = int(parts[0])
kind = parts[1]
await delete_item_by_id(query, item_id, kind)
if chat_id in user_states:
del user_states[chat_id]
return ConversationHandler.END
elif query.data.startswith("delete_scope_"):
# Удаление weekly task с указанием области
parts = query.data.replace("delete_scope_", "").split("_")
if len(parts) >= 3:
item_id = int(parts[0])
kind = parts[1]
scope = parts[2] # "one" или "series"
await delete_item_by_scope(query, item_id, kind, scope)
if chat_id in user_states:
del user_states[chat_id]
return ConversationHandler.END
# Возвращаемся в главное меню после завершения операции
await show_main_menu(query.message)
return ConversationHandler.END
@ -237,6 +306,37 @@ async def show_date_selection(query, kind="event"):
await query.edit_message_text("Выберите дату:", reply_markup=reply_markup)
async def show_date_selection_for_delete(query_or_message):
"""Показать выбор даты для удаления"""
today = datetime.now(TZ).date()
monday = today - timedelta(days=today.weekday())
next_monday = monday + timedelta(days=7)
keyboard = []
weekdays = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
# Текущая неделя
for i in range(7):
date = monday + timedelta(days=i)
date_str = date.strftime("%Y-%m-%d")
label = f"{weekdays[i]} {date.day}"
keyboard.append([InlineKeyboardButton(label, callback_data=f"del_date_{date_str}")])
# Следующая неделя
for i in range(7):
date = next_monday + timedelta(days=i)
date_str = date.strftime("%Y-%m-%d")
label = f"{weekdays[i]} {date.day}"
keyboard.append([InlineKeyboardButton(label, callback_data=f"del_date_{date_str}")])
reply_markup = InlineKeyboardMarkup(keyboard)
# Проверяем, это query или message
if hasattr(query_or_message, 'edit_message_text'):
await query_or_message.edit_message_text("Выберите дату для удаления:", reply_markup=reply_markup)
else:
await query_or_message.reply_text("Выберите дату для удаления:", reply_markup=reply_markup)
async def show_hour_selection(query):
"""Показать выбор часа"""
keyboard = []
@ -395,6 +495,151 @@ async def handle_repeat_choice(update: Update, context: ContextTypes.DEFAULT_TYP
return ConversationHandler.END
async def show_items_for_deletion(query, date_str: str):
"""Показать список задач и занятий для удаления"""
async with AsyncSessionLocal() as db:
from backend.utils import materialize_weekly_tasks
# Получаем tasks (разовые)
result = await db.execute(
select(Task).where(
and_(Task.date == date_str, Task.repeat_weekly == False)
)
)
tasks = result.scalars().all()
# Получаем weekly tasks для этой даты
weekly_items = await materialize_weekly_tasks(db, date_str, date_str)
weekly_tasks = [item for item in weekly_items if item.kind == "task"]
# Получаем events
result = await db.execute(
select(Event).where(Event.date == date_str)
)
events = result.scalars().all()
events = sorted(events, key=lambda e: e.start_time)
keyboard = []
items_list = []
# Добавляем tasks
for task in tasks:
items_list.append(("task", task.id, task.title, None))
keyboard.append([InlineKeyboardButton(f"📋 {task.title}", callback_data=f"delete_{task.id}_task")])
# Добавляем weekly tasks
for task_item in weekly_tasks:
# Для weekly tasks нужно найти оригинальную задачу
result = await db.execute(
select(Task).where(Task.id == task_item.id)
)
task = result.scalar_one_or_none()
if task:
items_list.append(("task", task.id, task.title, None))
keyboard.append([InlineKeyboardButton(f"📋 {task.title} (повторяется)", callback_data=f"delete_{task.id}_task")])
# Добавляем events
for event in events:
end_time = calculate_end_time(event.start_time, event.duration_min)
items_list.append(("event", event.id, event.title, event.start_time))
keyboard.append([InlineKeyboardButton(f"{event.start_time}-{end_time} {event.title}", callback_data=f"delete_{event.id}_event")])
if not keyboard:
await query.edit_message_text(f"На {format_date_russian(date_str)} нет записей для удаления.")
return ConversationHandler.END
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"Выберите запись для удаления на {format_date_russian(date_str)}:",
reply_markup=reply_markup
)
async def delete_item_by_id(query, item_id: int, kind: str):
"""Удалить элемент по ID"""
async with AsyncSessionLocal() as db:
if kind == "task":
result = await db.execute(select(Task).where(Task.id == item_id))
task = result.scalar_one_or_none()
if task:
if task.repeat_weekly:
# Для weekly task нужно спросить область применения
chat_id = str(query.from_user.id)
if chat_id not in user_states:
user_states[chat_id] = {}
user_states[chat_id]["delete_item_id"] = item_id
user_states[chat_id]["delete_kind"] = "task"
user_states[chat_id]["delete_date"] = user_states[chat_id].get("delete_date", "")
keyboard = [
[InlineKeyboardButton("Только на эту дату", callback_data=f"delete_scope_{item_id}_task_one")],
[InlineKeyboardButton("Для всего ряда", callback_data=f"delete_scope_{item_id}_task_series")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"Задача '{task.title}' повторяется каждую неделю. Что удалить?",
reply_markup=reply_markup
)
return SELECTING_DELETE_ITEM
else:
await db.delete(task)
await db.commit()
await query.edit_message_text(f"✅ Задача '{task.title}' удалена.")
return ConversationHandler.END
elif kind == "event":
result = await db.execute(select(Event).where(Event.id == item_id))
event = result.scalar_one_or_none()
if event:
title = event.title
await db.delete(event)
await db.commit()
await query.edit_message_text(f"✅ Занятие '{title}' удалено.")
return ConversationHandler.END
await query.edit_message_text("❌ Запись не найдена.")
return ConversationHandler.END
async def delete_item_by_scope(query, item_id: int, kind: str, scope: str):
"""Удалить элемент с указанием области (для weekly tasks)"""
async with AsyncSessionLocal() as db:
if kind == "task":
result = await db.execute(select(Task).where(Task.id == item_id))
task = result.scalar_one_or_none()
if task and task.repeat_weekly:
chat_id = str(query.from_user.id)
state = user_states.get(chat_id, {})
date_str = state.get("delete_date", "")
if scope == "one":
# Создаем исключение на удаление для конкретной даты
from backend.database import WeeklyTaskException
weekday = get_weekday_from_date(date_str)
exception = WeeklyTaskException(
weekday=weekday,
date=date_str,
action="delete"
)
db.add(exception)
await db.commit()
await query.edit_message_text(f"✅ Задача '{task.title}' удалена только на {format_date_russian(date_str)}.")
elif scope == "series":
# Удаляем весь ряд weekly tasks
await db.execute(
Task.__table__.delete().where(Task.weekday == task.weekday)
)
await db.commit()
await query.edit_message_text(f"✅ Задача '{task.title}' удалена для всего ряда.")
return ConversationHandler.END
await query.edit_message_text("❌ Ошибка при удалении.")
return ConversationHandler.END
async def send_reminders():
"""Отправка напоминаний о событиях"""
while True:
@ -477,7 +722,24 @@ async def start_bot():
name="task_conv",
)
# Команды
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("today", today_command))
application.add_handler(CommandHandler("nextday", nextday_command))
application.add_handler(CommandHandler("addtask", addtask_command))
# Conversation handler для удаления
delete_conv = ConversationHandler(
entry_points=[CommandHandler("deltask", deltask_command)],
states={
SELECTING_DELETE_DATE: [CallbackQueryHandler(button_handler, pattern="^del_date_")],
SELECTING_DELETE_ITEM: [CallbackQueryHandler(button_handler, pattern="^(delete_|delete_scope_)")],
},
fallbacks=[],
name="delete_conv",
)
application.add_handler(delete_conv)
application.add_handler(event_conv)
application.add_handler(task_conv)
application.add_handler(CallbackQueryHandler(button_handler))