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 ContextTypes, ConversationHandler, filters
) )
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select, and_
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pytz import pytz
import os import os
@ -24,6 +24,7 @@ BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
# Состояния для ConversationHandler # Состояния для ConversationHandler
SELECTING_DATE, SELECTING_HOUR, SELECTING_MINUTE, SELECTING_DURATION_HOUR, SELECTING_DURATION_MIN, ENTERING_TITLE = range(6) 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_TASK_DATE, ENTERING_TASK_TITLE, SELECTING_REPEAT, SELECTING_COPY_DAYS = range(10, 14)
SELECTING_DELETE_DATE, SELECTING_DELETE_ITEM = range(20, 22)
user_states = {} # Временное хранилище состояний пользователей 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}" return f"{end_hour:02d}:{end_minute:02d}"
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): async def show_main_menu(message):
"""Обработчик команды /start""" """Показать главное меню с кнопками"""
await register_user(str(update.effective_chat.id))
keyboard = [ keyboard = [
[InlineKeyboardButton("📅 Сегодня", callback_data="schedule_today")], [InlineKeyboardButton("📅 Сегодня", callback_data="schedule_today")],
[InlineKeyboardButton("📅 Завтра", callback_data="schedule_tomorrow")], [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_event")],
[InlineKeyboardButton(" Добавить задачу", callback_data="add_task")], [InlineKeyboardButton(" Добавить задачу", callback_data="add_task")],
] ]
reply_markup = InlineKeyboardMarkup(keyboard) reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text( await update.message.reply_text(
"Выберите действие:", "Что вы хотите добавить?",
reply_markup=reply_markup 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): async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик кнопок""" """Обработчик кнопок"""
query = update.callback_query query = update.callback_query
@ -126,26 +174,23 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
today = datetime.now(TZ).date() today = datetime.now(TZ).date()
if query.data == "schedule_today": if query.data == "schedule_today":
# Расписание на сегодня (аналог /today) - отправляем новое сообщение
date_str = today.strftime("%Y-%m-%d") date_str = today.strftime("%Y-%m-%d")
text = await get_schedule_for_date(date_str) 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": elif query.data == "schedule_tomorrow":
# Расписание на завтра (аналог /nextday) - отправляем новое сообщение
tomorrow = today + timedelta(days=1) tomorrow = today + timedelta(days=1)
date_str = tomorrow.strftime("%Y-%m-%d") date_str = tomorrow.strftime("%Y-%m-%d")
text = await get_schedule_for_date(date_str) 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": elif query.data == "delete_start":
# Оставшиеся дни недели # Начало процесса удаления (аналог /deltask)
weekday = today.weekday() user_states[chat_id] = {"action": "delete"}
text = "" await show_date_selection_for_delete(query)
for i in range(weekday, 7): return SELECTING_DELETE_DATE
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 == "add_event": elif query.data == "add_event":
# Начинаем процесс добавления event # Начинаем процесс добавления event
@ -159,6 +204,15 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
await show_date_selection(query, "task") await show_date_selection(query, "task")
return SELECTING_TASK_DATE 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_"): elif query.data.startswith("date_"):
# Выбрана дата # Выбрана дата
date_str = query.data.replace("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("Введите название занятия:") await query.edit_message_text("Введите название занятия:")
return ENTERING_TITLE return ENTERING_TITLE
# Возвращаемся в главное меню elif query.data.startswith("delete_"):
keyboard = [ # Удаление элемента (формат: delete_ID_kind)
[InlineKeyboardButton("📅 Сегодня", callback_data="schedule_today")], parts = query.data.replace("delete_", "").split("_")
[InlineKeyboardButton("📅 Завтра", callback_data="schedule_tomorrow")], if len(parts) >= 2:
[InlineKeyboardButton("📅 Оставшиеся дни недели", callback_data="schedule_week")], item_id = int(parts[0])
[InlineKeyboardButton(" Добавить занятие", callback_data="add_event")], kind = parts[1]
[InlineKeyboardButton(" Добавить задачу", callback_data="add_task")], await delete_item_by_id(query, item_id, kind)
] if chat_id in user_states:
reply_markup = InlineKeyboardMarkup(keyboard) del user_states[chat_id]
await query.edit_message_text("Выберите действие:", reply_markup=reply_markup) 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 return ConversationHandler.END
@ -237,6 +306,37 @@ async def show_date_selection(query, kind="event"):
await query.edit_message_text("Выберите дату:", reply_markup=reply_markup) 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): async def show_hour_selection(query):
"""Показать выбор часа""" """Показать выбор часа"""
keyboard = [] keyboard = []
@ -395,6 +495,151 @@ async def handle_repeat_choice(update: Update, context: ContextTypes.DEFAULT_TYP
return ConversationHandler.END 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(): async def send_reminders():
"""Отправка напоминаний о событиях""" """Отправка напоминаний о событиях"""
while True: while True:
@ -477,7 +722,24 @@ async def start_bot():
name="task_conv", name="task_conv",
) )
# Команды
application.add_handler(CommandHandler("start", start)) 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(event_conv)
application.add_handler(task_conv) application.add_handler(task_conv)
application.add_handler(CallbackQueryHandler(button_handler)) application.add_handler(CallbackQueryHandler(button_handler))