diff --git a/app/callbacks/menu.py b/app/callbacks/menu.py index 92ac2ef..43eadea 100644 --- a/app/callbacks/menu.py +++ b/app/callbacks/menu.py @@ -1,7 +1,6 @@ __all__ = ("router",) from aiogram import F, Router -from aiogram.exceptions import TelegramBadRequest from aiogram.filters import StateFilter from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message @@ -35,11 +34,6 @@ async def profile_callback(callback: CallbackQuery) -> None: ) await callback.answer() - try: - await callback.message.delete() - except TelegramBadRequest: - pass - @router.callback_query( F.data == "menu_create_travel", @@ -63,11 +57,6 @@ async def create_travel_callback( await callback.answer() - try: - await callback.message.delete() - except TelegramBadRequest: - pass - @router.callback_query( F.data == "menu_travels", @@ -101,9 +90,4 @@ async def travels_callback( ), ) - try: - await callback.message.delete() - except TelegramBadRequest: - pass - await callback.answer() diff --git a/app/callbacks/profile.py b/app/callbacks/profile.py index 5866616..7087ff1 100644 --- a/app/callbacks/profile.py +++ b/app/callbacks/profile.py @@ -102,7 +102,7 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None: if column == "username": try: - validated_value = User().validate_username( + validated_title = User().validate_username( key="username", value=value, ) @@ -111,7 +111,7 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None: return - await state.update_data(value=validated_value, successfully=True) + await state.update_data(value=validated_title, successfully=True) elif column == "age": try: validated_age = User().validate_age(key="age", value=value) diff --git a/app/callbacks/travels.py b/app/callbacks/travels.py index 4a6dd31..fdb5eac 100644 --- a/app/callbacks/travels.py +++ b/app/callbacks/travels.py @@ -3,20 +3,54 @@ __all__ = ("router",) from aiogram import F, Router from aiogram.exceptions import TelegramBadRequest from aiogram.filters import StateFilter +from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message -from app import messages +from app import messages, session from app.config import Config -from app.filters.user import RegisteredCallback +from app.filters.user import Registered, RegisteredCallback from app.keyboards.builders import travels_keyboard from app.keyboards.travel import get from app.models.travel import Travel from app.models.user import User +from app.states.travel import TravelAlteringState +from app.utils.states import delete_message_from_state, handle_validation_error router = Router(name="menu_callback") +@router.callback_query( + F.data == "travels", + RegisteredCallback(), + StateFilter(None), +) +async def travels_index_callback(callback: CallbackQuery) -> None: + page = 0 + + if callback.from_user is None or not isinstance(callback.message, Message): + return + + user = User().get_user_by_telegram_id(callback.from_user.id) + + travels = user.get_user_travels() + + if not travels or travels == []: + await callback.message.edit_text(messages.NO_TRAVELS) + else: + pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE + + await callback.message.edit_text( + messages.TRAVELS, + reply_markup=travels_keyboard( + travels, + page, + pages, + user.telegram_id, + ), + ) + + @router.callback_query( F.data.startswith("travels_page"), RegisteredCallback(), @@ -71,3 +105,138 @@ async def travel_detail_callback(callback: CallbackQuery) -> None: travel.get_travel_text(), reply_markup=get(travel_id), ) + + +@router.callback_query( + F.data.startswith("travel_change"), + RegisteredCallback(), + StateFilter(None), +) +async def travel_change_callback( + callback: CallbackQuery, + state: FSMContext, +) -> None: + if ( + callback.data is None + or callback.message is None + or not isinstance(callback.message, Message) + ): + return + + travel_id, column = callback.data.replace("travel_change_", "").split("_") + + if column == "title": + message = await callback.message.answer( + f"{messages.INPUT_TRAVEL_TITLE}\n{messages.CANCEL_CHANGE}", + ) + elif column == "description": + message = await callback.message.answer( + f"{messages.EDIT_TRAVEL_DESCRIPTION}\n{messages.CANCEL_CHANGE}", + ) + + await state.update_data( + column=column, + travel_message_id=callback.message.message_id, + input_message_id=message.message_id, + travel_id=travel_id, + ) + await state.set_state(TravelAlteringState.value) + + await callback.answer() + + +@router.message(TravelAlteringState.value, F.text, Registered()) +async def travel_change_entered(message: Message, state: FSMContext) -> None: + if ( + message.text is None + or message.from_user is None + or message.bot is None + ): + return + + data = await state.get_data() + + column = data["column"] + travel_id = data["travel_id"] + value = message.text.strip() + + if value == "/cancel": + await message.answer( + messages.CHANGE_CANCELED, + ) + + await state.update_data(successfully=True) + await message.delete() + await delete_message_from_state( + state, + message.chat.id, + message.bot, + ) + await state.clear() + + return + + if column == "title": + try: + validated_title = Travel().validate_title( + key="title", + value=value, + ) + except AssertionError as e: + await handle_validation_error(message, state, e) + + return + + await state.update_data(value=validated_title, successfully=True) + elif column == "description": + if value == "/skip": + await state.update_data(value=None, successfully=True) + await delete_message_from_state( + state, + message.chat.id, + message.bot, + ) + else: + try: + validated_description = Travel().validate_description( + key="description", value=value, + ) + except AssertionError as e: + await handle_validation_error(message, state, e) + + return + + await state.update_data( + value=validated_description, successfully=True, + ) + + await message.delete() + await delete_message_from_state(state, message.chat.id, message.bot) + + state_data = await state.get_data() + + travel = Travel().get_travel_queryset_by_id(travel_id) + + data = {state_data["column"]: state_data["value"]} + travel.update(data) + + session.commit() + + travel = travel.first() + session.refresh(travel) + + try: + await message.bot.edit_message_text( + travel.get_travel_text(), + message.chat.id, + state_data["travel_message_id"], + reply_markup=get(travel_id), + ) + except TelegramBadRequest: + pass + + await message.answer( + messages.TRAVEL_UPDATED, + ) + + await state.clear() diff --git a/app/handlers/travels_command.py b/app/handlers/travels_command.py index daf826d..a27af10 100644 --- a/app/handlers/travels_command.py +++ b/app/handlers/travels_command.py @@ -15,7 +15,7 @@ router = Router(name="travels_command") @router.message(Command("travels"), Registered(), StateFilter(None)) -async def command_help_handler(message: Message) -> None: +async def command_travels_handler(message: Message) -> None: page = 0 if message.from_user is None: diff --git a/app/keyboards/travel.py b/app/keyboards/travel.py index 7a005a7..7cb57bb 100644 --- a/app/keyboards/travel.py +++ b/app/keyboards/travel.py @@ -17,6 +17,16 @@ def get(travel_id: int): callback_data=f"travel_change_{travel_id}_description", ), ) + builder.row( + types.InlineKeyboardButton( + text="πŸ—ΊοΈ Locations", + callback_data=f"travel_locations_{travel_id}", + ), + types.InlineKeyboardButton( + text="πŸ‘€ Users", + callback_data=f"travel_users_{travel_id}", + ), + ) builder.row( types.InlineKeyboardButton( text="βž• Add location", @@ -30,7 +40,7 @@ def get(travel_id: int): builder.row( types.InlineKeyboardButton( text="⬅️", - callback_data="menu_travels", + callback_data="travels", ), ) diff --git a/app/messages.py b/app/messages.py index 15db843..e4bce33 100644 --- a/app/messages.py +++ b/app/messages.py @@ -7,6 +7,8 @@ NO_TRAVELS = "No travels yet. You can create one with /create_travel command." CREATE_TRAVEL = ( "🧳 Let's create new travel!\nEnter /cancel to cancel creating." ) +TRAVEL_UPDATED = "βœ… Travel updated" +EDIT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to set it to None):\nMaximum length: 100 characters" INPUT_TRAVEL_TITLE = ( "Enter travel title:\nMaximum length: 30 characters" ) diff --git a/app/models/travel.py b/app/models/travel.py index 04bd229..509cd48 100644 --- a/app/models/travel.py +++ b/app/models/travel.py @@ -74,13 +74,19 @@ class Travel(Base): def get_travel_text(self): return messages.TRAVEL_DETAIL.format( title=self.title, - description=self.description, + description=( + self.description if self.description else messages.NOT_SET + ), ) @classmethod def get_travel_by_id(cls, travel_id): return session.query(Travel).filter(Travel.id == travel_id).first() + @classmethod + def get_travel_queryset_by_id(cls, travel_id): + return session.query(Travel).filter(Travel.id == travel_id) + class Location(Base): __tablename__ = "locations" diff --git a/app/models/user.py b/app/models/user.py index 3f34719..e97b12c 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -98,7 +98,8 @@ class User(Base): return normalized_value def get_user_travels(self) -> list: - return self.owned_travels + self.travels + all_travels = self.owned_travels + self.travels + return sorted(all_travels, key=lambda travel: travel.id) def get_human_readable_datejoined(self) -> str: return self.date_joined.strftime("%Y-%m-%d %H:%M:%S") diff --git a/app/states/travel.py b/app/states/travel.py index 6ed1f8b..3543954 100644 --- a/app/states/travel.py +++ b/app/states/travel.py @@ -7,3 +7,13 @@ class TravelCreationState(StatesGroup): error_message_id = State() title = State() description = State() + + +class TravelAlteringState(StatesGroup): + travel_message_id = State() + input_message_id = State() + error_message_id = State() + successfully = State() + travel_id = State() + column = State() + value = State()