diff --git a/database.py b/database.py index 16078b4ba941eacaa3954e62d42230cf1da90b7c..a2d1d75f588486954f713d511d11d1079cca0f59 100644 --- a/database.py +++ b/database.py @@ -229,6 +229,81 @@ def get_schedules_for_user(user_id): ) +def get_schedules_by_location_for_user(location_name, user_id): + return get_schedules_query( + dedent( + """\ + WHERE user_id = ? AND location.name = ? + ORDER BY location.ordering, meal.ordering, meal.week_day + """ + ), + (user_id, location_name), + ) + + +def get_schedules_by_location_and_meal_name_for_user( + location_name, meal_name, user_id +): + return get_schedules_query( + dedent( + """\ + WHERE schedule.user_id = ? + AND schedule.location_name = ? + AND schedule.meal_name = ? + ORDER BY location.ordering, meal.ordering, meal.week_day + """ + ), + (user_id, location_name, meal_name), + ) + + +def get_schedules_by_location_and_meal_name_and_week_day_for_user( + location_name, meal_name, week_day, user_id +): + return get_schedules_query( + dedent( + """\ + WHERE schedule.user_id = ? + AND schedule.location_name = ? + AND schedule.meal_name = ? + AND schedule.week_day = ? + ORDER BY location.ordering, meal.ordering, meal.week_day + """ + ), + (user_id, location_name, meal_name, week_day), + ) + + +def get_schedule_by_key(location_name, meal_name, week_day, time, user_id): + rows = get_schedules_query( + dedent( + """\ + WHERE schedule.user_id = ? + AND schedule.location_name = ? + AND schedule.meal_name = ? + AND schedule.week_day = ? + AND schedule.time = ? + ORDER BY location.ordering, meal.ordering, meal.week_day + """ + ), + (user_id, location_name, meal_name, week_day, time), + ) + return rows[0] if len(rows) else None + + +def delete_schedule_by_key(location_name, meal_name, week_day, time, user_id): + connection.execute( + dedent( + """\ + DELETE FROM schedule + WHERE user_id = ? AND location_name = ? AND meal_name = ? + AND week_day = ? AND time = ? + """ + ), + (user_id, location_name, meal_name, week_day, time), + ) + + def get_schedules_matching_datetime(dt: datetime): time = dt.strftime("%H:%M") week_day = dt.weekday() @@ -252,11 +327,12 @@ def upsert_schedule(schedule): """\ SELECT created_at FROM schedule - WHERE week_day = ? AND location_name = ? + WHERE time = ? AND week_day = ? AND location_name = ? AND meal_name = ? AND user_id = ? """ ), ( + schedule.time.strftime("%H:%M"), schedule.week_day.value, schedule.meal.location.name, schedule.meal.name, diff --git a/main.py b/main.py index 6336572b3cdc241d3a10aff0390304e6d2f241f4..c13e40504bad08aa2fe6b01583909c5812a5b114 100644 --- a/main.py +++ b/main.py @@ -25,18 +25,23 @@ from crawler import Day, Location, LocationDays, get_location_days from database import ( delete_all_schedules_from_user, delete_menu_item_indicator_preference, + delete_schedule_by_key, get_location_by_command, get_locations, get_meals, get_meals_by_location_name, get_menu_item_indicators, + get_schedule_by_key, + get_schedules_by_location_and_meal_name_and_week_day_for_user, + get_schedules_by_location_and_meal_name_for_user, + get_schedules_by_location_for_user, get_schedules_for_user, get_schedules_matching_datetime, get_shown_menu_item_indicators_for_user, insert_menu_item_indicator_preference, upsert_schedule, ) -from model import Meal, Schedule +from model import Meal, Schedule, WeekDay def add_time_and_timedelta(time, timedelta): @@ -263,6 +268,25 @@ def answer_customize_indicators_callback( ) +@dataclass +class ScheduleDigestCallbackData: + step: int + + +@dataclass +class ScheduleOneCallbackData: + step: int + + +@dataclass +class DeleteOneNotificationCallbackData: + step: int + location: Optional[Location] = None + meal_name: Optional[str] = None + week_day: Optional[WeekDay] = None + time: Optional[str] = None + + def answer_schedule(user_id): schedules = get_schedules_for_user(user_id) keyboard = [ @@ -273,26 +297,26 @@ def answer_schedule(user_id): ) ], # [ - # InlineKeyboardButton( - # "Agendar notificações diárias...", - # callback_data=ScheduleDigestCallbackData(-1) - # ) + # InlineKeyboardButton( + # "Agendar notificações diárias únicas...", + # callback_data=ScheduleDigestCallbackData(-1), + # ) # ], # [ - # InlineKeyboardButton( - # "Agendar uma notificação individual...", - # callback_data=ScheduleOnceCallbackData(-1) - # ) + # InlineKeyboardButton( + # "Agendar uma notificação individual...", + # callback_data=ScheduleOneCallbackData(-1), + # ) # ], ] if len(schedules): keyboard += [ - # [ - # InlineKeyboardButton( - # "Remover uma notificação individual...", - # callback_data=DeleteOnceCallbackData() - # ) - # ], + [ + InlineKeyboardButton( + "Remover uma notificação individual...", + callback_data=DeleteOneNotificationCallbackData(-1), + ) + ], [ InlineKeyboardButton( "Remover todas as notificações...", @@ -346,6 +370,265 @@ def answer_schedule(user_id): ) +def answer_delete_one_notification(user, data): + user_id = user.id + buttons = [] + if data.step == -1: + schedules = get_schedules_for_user(user_id) + body = ( + "Selecione o RU da notificação individual que você deseja remover." + ) + locations = list( + sorted(set(schedule.meal.location for schedule in schedules)) + ) + if len(locations) == 1: + location = locations[0] + return answer_delete_one_notification( + user, DeleteOneNotificationCallbackData(0, location) + ) + for location in locations: + buttons.append( + [ + InlineKeyboardButton( + f"RU {location.name}", + callback_data=DeleteOneNotificationCallbackData( + 0, location + ), + ) + ] + ) + buttons.append( + [ + InlineKeyboardButton( + "« Voltar", callback_data=ScheduleCallbackData() + ) + ] + ) + elif data.step == 0: + schedules = get_schedules_by_location_for_user( + data.location.name, user_id + ) + body = "Selecione a refeição da notificação individual" + "que você deseja remover." + meal_names = list( + sorted(set(schedule.meal.name for schedule in schedules)) + ) + if len(meal_names) == 1: + meal_name = meal_names[0] + return answer_delete_one_notification( + user, + DeleteOneNotificationCallbackData(1, data.location, meal_name), + ) + for meal_name in meal_names: + buttons.append( + [ + InlineKeyboardButton( + meal_name, + callback_data=DeleteOneNotificationCallbackData( + 1, data.location, meal_name + ), + ) + ] + ) + buttons.append( + [ + InlineKeyboardButton( + "« Voltar", + callback_data=DeleteOneNotificationCallbackData(-1), + ) + ] + ) + elif data.step == 1: + schedules = get_schedules_by_location_and_meal_name_for_user( + data.location.name, data.meal_name, user_id + ) + body = ( + "Selecione o dia da semana da notificação individual" + " que você deseja remover." + ) + week_days = list( + sorted(set(schedule.meal.week_day for schedule in schedules)) + ) + if len(week_days) == 1: + week_day = week_days[0] + return answer_delete_one_notification( + user, + DeleteOneNotificationCallbackData( + 2, data.location, data.meal_name, week_day + ), + ) + for week_day in week_days: + buttons.append( + InlineKeyboardButton( + week_day.long, + callback_data=DeleteOneNotificationCallbackData( + 2, data.location, data.meal_name, week_day + ), + ) + ) + buttons = [buttons[i : i + 2] for i in range(0, len(buttons), 2)] + buttons.append( + [ + InlineKeyboardButton( + "« Voltar", + callback_data=DeleteOneNotificationCallbackData( + 0, data.location + ), + ) + ] + ) + elif data.step == 2: + schedules = ( + get_schedules_by_location_and_meal_name_and_week_day_for_user( + data.location.name, + data.meal_name, + data.week_day.value, + user_id, + ) + ) + body = "Selecione o horário da notificação individual" + "que você deseja remover." + if len(schedules) == 1: + schedule = schedules[0] + return answer_delete_one_notification( + user, + DeleteOneNotificationCallbackData( + 3, + data.location, + data.meal_name, + schedule.week_day, + schedule.time, + ), + ) + for schedule in schedules: + buttons.append( + InlineKeyboardButton( + schedule.time, + callback_data=DeleteOneNotificationCallbackData( + 3, + data.location, + data.meal_name, + schedule.week_day, + schedule.time, + ), + ) + ) + buttons = [buttons[i : i + 4] for i in range(0, len(buttons), 4)] + buttons.append( + [ + InlineKeyboardButton( + "« Voltar", + callback_data=DeleteOneNotificationCallbackData( + 1, data.location, data.meal_name + ), + ) + ] + ) + elif data.step == 3: + schedule = get_schedule_by_key( + data.location.name, + data.meal_name, + data.week_day.value, + data.time, + user_id, + ) + schedules = [schedule] + body = "Tem certeza que quer remover a notificação individual?" + buttons.append( + [ + InlineKeyboardButton( + "Sim, remover notificação", + callback_data=DeleteOneNotificationCallbackData( + 4, + data.location, + data.meal_name, + data.week_day, + schedule.time, + ), + ) + ] + ) + buttons.append( + [ + InlineKeyboardButton( + "« Voltar", + callback_data=DeleteOneNotificationCallbackData( + 2, data.location, data.meal_name, data.week_day + ), + ) + ] + ) + else: + logger.info( + f"User {format_user(user)} removed" + f" location={data.location_name}, meal={data.meal_name}," + f" week_day={data.week_day.value}, time={data.time}" + ) + delete_schedule_by_key( + data.location.name, + data.meal_name, + data.week_day.value, + data.time, + user_id, + ) + return answer_schedule(user_id) + + keyboard = InlineKeyboardMarkup(buttons) + + if len(schedules): + schedules_by_location = defaultdict(list) + for schedule in schedules: + schedules_by_location[schedule.meal.location].append(schedule) + week_day_by_time_meal_by_location = dict() + for key, schedules in schedules_by_location.items(): + by_time_meal = defaultdict(set) + for schedule in schedules: + by_time_meal[(schedule.time, schedule.meal.name)].add( + schedule.week_day + ) + week_day_by_time_meal_by_location[key] = by_time_meal + return ( + "<b>Notificações</b>\n\n" + + "\n\n".join( + f"<b>RU {location.name}</b>\n" + + "\n".join( + f" • {time} · {meal_name} · " + + ", ".join( + week_day.short for week_day in sorted(week_days) + ) + for ( + time, + meal_name, + ), week_days in week_day_by_time_meal.items() + ) + for ( + location, + week_day_by_time_meal, + ) in week_day_by_time_meal_by_location.items() + ) + + "\n\n" + + body, + keyboard, + ) + else: + return ( + "<b>Notificações</b>\n\n" + "<i>Atualmente você não tem nenhuma notificação agendada.</i>", + keyboard, + ) + + +def answer_delete_one_notification_callback( + update: Update, context: CallbackContext +): + body, keyboard = answer_delete_one_notification( + update.effective_user, update.callback_query.data + ) + update.callback_query.message.edit_text( + body, reply_markup=keyboard, parse_mode=PARSEMODE_HTML + ) + + def answer_ready_callback(update: Update, context: CallbackContext) -> None: update.callback_query.message.edit_reply_markup(None) @@ -664,6 +947,12 @@ def main() -> None: pattern=CustomizeIndicatorsCallbackData, ) ) + updater.dispatcher.add_handler( + CallbackQueryHandler( + answer_delete_one_notification_callback, + pattern=DeleteOneNotificationCallbackData, + ) + ) updater.dispatcher.add_handler( CallbackQueryHandler(answer_ready_callback, pattern=ReadyCallbackData) ) diff --git a/model.py b/model.py index 45d0b24ec4c6496503d407120539de724394d4c4..81a3efd909165323e9a4c7ca61fedb2a226872bc 100644 --- a/model.py +++ b/model.py @@ -62,6 +62,11 @@ class Location: ordering: int url: str + def __lt__(self, other): + if self.__class__ is other.__class__: + return self.ordering < other.ordering + return NotImplemented + @dataclass(frozen=True) class LocationWeekDays: