feat: Added travel detail, added proccessing message when changing/setting user location, improvements in messages text

This commit is contained in:
ITQ
2024-03-23 16:19:15 +03:00
parent 7b28635f09
commit 40ea9689ab
12 changed files with 202 additions and 26 deletions
+11 -2
View File
@@ -93,8 +93,17 @@ async def travels_callback(
await callback.message.answer( await callback.message.answer(
messages.TRAVELS, messages.TRAVELS,
reply_markup=travels_keyboard(travels, page, pages), reply_markup=travels_keyboard(
travels,
page,
pages,
user.telegram_id,
),
) )
await callback.message.delete() try:
await callback.message.delete()
except TelegramBadRequest:
pass
await callback.answer() await callback.answer()
+41 -5
View File
@@ -152,11 +152,20 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
elif column == "location": elif column == "location":
location = value.split(", ") location = value.split(", ")
proccessing_message = await message.answer(messages.PROCCESSING)
if len(location) != 2: if len(location) != 2:
await handle_validation_error( await delete_message_from_state(
message,
state, state,
messages.VALIDATION_ERROR, message.chat.id,
message.bot,
)
await proccessing_message.edit_text(messages.VALIDATION_ERROR)
await message.delete()
error_message = proccessing_message
await state.update_data(
error_message_id=error_message.message_id,
) )
return return
@@ -169,7 +178,18 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
value=country, value=country,
) )
except AssertionError as e: except AssertionError as e:
await handle_validation_error(message, state, e) await delete_message_from_state(
state,
message.chat.id,
message.bot,
)
await proccessing_message.edit_text("" + str(e))
await message.delete()
error_message = proccessing_message
await state.update_data(
error_message_id=error_message.message_id,
)
return return
@@ -179,7 +199,18 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
country=validated_country, country=validated_country,
) )
except AssertionError as e: except AssertionError as e:
await handle_validation_error(message, state, e) await delete_message_from_state(
state,
message.chat.id,
message.bot,
)
await proccessing_message.edit_text("" + str(e))
await message.delete()
error_message = proccessing_message
await state.update_data(
error_message_id=error_message.message_id,
)
return return
@@ -204,6 +235,11 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
"city": state_data["value"][1], "city": state_data["value"][1],
}, },
) )
try:
await proccessing_message.delete()
except TelegramBadRequest:
pass
else: else:
data = {state_data["column"]: state_data["value"]} data = {state_data["column"]: state_data["value"]}
user.update(data) user.update(data)
+30 -1
View File
@@ -9,6 +9,8 @@ from app import messages
from app.config import Config from app.config import Config
from app.filters.user import RegisteredCallback from app.filters.user import RegisteredCallback
from app.keyboards.builders import travels_keyboard 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.models.user import User
@@ -40,5 +42,32 @@ async def travels_callback(callback: CallbackQuery) -> None:
await callback.message.edit_text( await callback.message.edit_text(
messages.TRAVELS, messages.TRAVELS,
reply_markup=travels_keyboard(travels, page, pages), reply_markup=travels_keyboard(
travels,
page,
pages,
user.telegram_id,
),
) )
@router.callback_query(
F.data.startswith("travel_detail"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_detail_callback(callback: CallbackQuery) -> None:
if callback.data is None or not isinstance(callback.message, Message):
return
travel_id = int(callback.data.replace("travel_detail_", ""))
travel = Travel().get_travel_by_id(travel_id)
if not travel:
return
await callback.message.edit_text(
travel.get_travel_text(),
reply_markup=get(travel_id),
)
+42 -5
View File
@@ -1,6 +1,7 @@
__all__ = ("router",) __all__ = ("router",)
from aiogram import F, Router from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import CommandStart, StateFilter from aiogram.filters import CommandStart, StateFilter
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import Message, ReplyKeyboardRemove from aiogram.types import Message, ReplyKeyboardRemove
@@ -166,13 +167,22 @@ async def location_handler(message: Message, state: FSMContext) -> None:
if message.text is None or message.from_user is None: if message.text is None or message.from_user is None:
return return
proccessing_message = await message.answer(messages.PROCCESSING)
location = message.text.strip().split(", ") location = message.text.strip().split(", ")
if len(location) != 2: if len(location) != 2:
await handle_validation_error( await delete_message_from_state(
message,
state, state,
messages.VALIDATION_ERROR, message.chat.id,
message.bot,
)
await proccessing_message.edit_text(messages.VALIDATION_ERROR)
await message.delete()
error_message = proccessing_message
await state.update_data(
error_message_id=error_message.message_id,
) )
return return
@@ -185,7 +195,18 @@ async def location_handler(message: Message, state: FSMContext) -> None:
value=country, value=country,
) )
except AssertionError as e: except AssertionError as e:
await handle_validation_error(message, state, e) await delete_message_from_state(
state,
message.chat.id,
message.bot,
)
await proccessing_message.edit_text("" + str(e))
await message.delete()
error_message = proccessing_message
await state.update_data(
error_message_id=error_message.message_id,
)
return return
@@ -195,10 +216,26 @@ async def location_handler(message: Message, state: FSMContext) -> None:
country=validated_country, country=validated_country,
) )
except AssertionError as e: except AssertionError as e:
await handle_validation_error(message, state, e) await delete_message_from_state(
state,
message.chat.id,
message.bot,
)
await proccessing_message.edit_text("" + str(e))
await message.delete()
error_message = proccessing_message
await state.update_data(
error_message_id=error_message.message_id,
)
return return
try:
await proccessing_message.delete()
except TelegramBadRequest:
pass
await delete_message_from_state(state, message.chat.id, message.bot) await delete_message_from_state(state, message.chat.id, message.bot)
await state.update_data(location=[validated_country, validated_city]) await state.update_data(location=[validated_country, validated_city])
+6 -1
View File
@@ -32,5 +32,10 @@ async def command_help_handler(message: Message) -> None:
await message.answer( await message.answer(
messages.TRAVELS, messages.TRAVELS,
reply_markup=travels_keyboard(travels, page, pages), reply_markup=travels_keyboard(
travels,
page,
pages,
user.telegram_id,
),
) )
+7 -2
View File
@@ -16,7 +16,7 @@ def sex_keyboard(choices: str | list):
return builder.as_markup(resize_keyboard=True) return builder.as_markup(resize_keyboard=True)
def travels_keyboard(travels: list, page: int, pages: int): def travels_keyboard(travels: list, page: int, pages: int, user_id: int):
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
rows = [] rows = []
@@ -24,9 +24,14 @@ def travels_keyboard(travels: list, page: int, pages: int):
end_index = min((page + 1) * Config.PAGE_SIZE, len(travels)) end_index = min((page + 1) * Config.PAGE_SIZE, len(travels))
for travel in travels[start_index:end_index]: for travel in travels[start_index:end_index]:
button_text = travel.title
if travel.author_id == user_id:
button_text += " 👑"
rows.append( rows.append(
InlineKeyboardButton( InlineKeyboardButton(
text=travel.title, text=button_text,
callback_data=f"travel_detail_{travel.id}", callback_data=f"travel_detail_{travel.id}",
), ),
) )
+37
View File
@@ -0,0 +1,37 @@
__all__ = ("get",)
from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder
def get(travel_id: int):
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="📝 Change title",
callback_data=f"travel_change_{travel_id}_title",
),
types.InlineKeyboardButton(
text="️ Change description",
callback_data=f"travel_change_{travel_id}_description",
),
)
builder.row(
types.InlineKeyboardButton(
text=" Add location",
callback_data=f"travel_add_{travel_id}_location",
),
types.InlineKeyboardButton(
text=" Add user",
callback_data=f"travel_add_{travel_id}_user",
),
)
builder.row(
types.InlineKeyboardButton(
text="⬅️",
callback_data="menu_travels",
),
)
return builder.as_markup()
+13 -6
View File
@@ -2,7 +2,7 @@
MENU = "<b>Menu:</b>" MENU = "<b>Menu:</b>"
TRAVELS = "📃 <b>Travels:</b>" TRAVELS = "📃 <b>Travels:</b>\n<i>👑 - owner</i>"
NO_TRAVELS = "No travels yet. You can create one with /create_travel command." NO_TRAVELS = "No travels yet. You can create one with /create_travel command."
CREATE_TRAVEL = ( CREATE_TRAVEL = (
"🧳 Let's create new travel!\n<i>Enter /cancel to cancel creating.</i>" "🧳 Let's create new travel!\n<i>Enter /cancel to cancel creating.</i>"
@@ -14,9 +14,14 @@ INPUT_TRAVEL_CALLBACK = (
"All right, travel <b>{key}</b> is set to: <b>{value}</b>" "All right, travel <b>{key}</b> is set to: <b>{value}</b>"
) )
INPUT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>" INPUT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>"
INPUT_TRAVEL_DESCRIPTION_SKIPPED = "Sure. You can always fill it later." INPUT_TRAVEL_DESCRIPTION_SKIPPED = "Sure. You can always fill it later."
TRAVEL_CREATED = "Travel <b>{title}</b> successfully created! You can now view and edit it in the travels list (/travels command)." TRAVEL_CREATED = "Travel <b>{title}</b> successfully created! You can now view and edit it in the travels list (/travels command)."
ACTION_CANCELED = "❌ Action canceled" ACTION_CANCELED = "❌ Action canceled"
TRAVEL_DETAIL = (
"📝 <b>Travel detail</b>\n\n"
"\tTitle: <b>{title}</b>\n"
"\tDescription: <b>{description}</b>\n"
)
WELCOME_MESSAGE = "Hello, <b>{name}</b>! Welcome to the ✈️ Travel Agent bot! Let's start our journey by filling out some information about you." WELCOME_MESSAGE = "Hello, <b>{name}</b>! Welcome to the ✈️ Travel Agent bot! Let's start our journey by filling out some information about you."
WELCOME_AGAIN_MESSAGE = "Hello, <b>{name}</b>! Welcome back to the ✈️ Travel Agent bot! If you get lost, you can always call the /help command for assistance." WELCOME_AGAIN_MESSAGE = "Hello, <b>{name}</b>! Welcome back to the ✈️ Travel Agent bot! If you get lost, you can always call the /help command for assistance."
@@ -39,14 +44,14 @@ INPUT_USERNAME = "Enter your username (this will be used to interact with other
INPUT_AGE = "Enter your age:\n<i>Range: 13-120</i>" INPUT_AGE = "Enter your age:\n<i>Range: 13-120</i>"
INPUT_SEX = "Enter your sex:\n<i>Options: Male or Female</i>" INPUT_SEX = "Enter your sex:\n<i>Options: Male or Female</i>"
INPUT_BIO = "Enter your bio (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>" INPUT_BIO = "Enter your bio (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>"
INPUT_BIO_SKIPPED = "Sure. You can always fill it later." INPUT_BIO_SKIPPED = "Sure. You can always fill it later."
INPUT_LOCATION = "Enter your location in this format:\n<i>Format: country, city</i>\n<i>Example: Russia, Moscow</i>" INPUT_LOCATION = "Enter your location in this format:\n<i>Format: country, city</i>\n<i>Example: Russia, Moscow</i>"
INPUT_CALLBACK = "All right, your <b>{key}</b> is set to: <b>{value}</b>" INPUT_CALLBACK = "All right, your <b>{key}</b> is set to: <b>{value}</b>"
VALIDATION_ERROR = "Invalid input. Please try again." VALIDATION_ERROR = "Invalid input. Please try again."
CANCEL_CHANGE = "<i>Enter /cancel to cancel change.</i>" CANCEL_CHANGE = "<i>Enter /cancel to cancel change.</i>"
PROFILE = ( PROFILE = (
"<b>Your profile:</b>\n\n" "<b>👤 Your profile:</b>\n\n"
"\tUsername: <b>{username}</b>\n" "\tUsername: <b>{username}</b>\n"
"\tAge: <b>{age}</b>\n" "\tAge: <b>{age}</b>\n"
"\tSex: <b>{sex}</b>\n" "\tSex: <b>{sex}</b>\n"
@@ -60,3 +65,5 @@ EDIT_USERNAME = "Enter your username:\n<i>Allowed characters: a-z, A-Z, 0-9, _</
EDIT_BIO = "Enter your bio (enter /skip if you want to set it to None):\n<i>Maximum length: 100 characters</i>" EDIT_BIO = "Enter your bio (enter /skip if you want to set it to None):\n<i>Maximum length: 100 characters</i>"
PROFILE_UPDATED = "✅ Profile updated" PROFILE_UPDATED = "✅ Profile updated"
CHANGE_CANCELED = "❌ Change canceled" CHANGE_CANCELED = "❌ Change canceled"
PROCCESSING = "⌛️ Processing..."
+12 -1
View File
@@ -3,7 +3,7 @@ __all__ = ("Travel", "Location")
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.orm import relationship, validates from sqlalchemy.orm import relationship, validates
from app import session from app import messages, session
from app.models import Base from app.models import Base
from app.models.user import User from app.models.user import User
@@ -55,6 +55,7 @@ class Travel(Base):
@validates("title") @validates("title")
def validate_title(self, key, value): def validate_title(self, key, value):
assert len(value) <= 30, "Title must be 30 characters or fewer." assert len(value) <= 30, "Title must be 30 characters or fewer."
assert "👑" not in value, "👑 is not allowed symbol."
if session.query(Travel).filter(Travel.title == value).first(): if session.query(Travel).filter(Travel.title == value).first():
raise AssertionError("This title is already taken.") raise AssertionError("This title is already taken.")
@@ -70,6 +71,16 @@ class Travel(Base):
return value return value
def get_travel_text(self):
return messages.TRAVEL_DETAIL.format(
title=self.title,
description=self.description,
)
@classmethod
def get_travel_by_id(cls, travel_id):
return session.query(Travel).filter(Travel.id == travel_id).first()
class Location(Base): class Location(Base):
__tablename__ = "locations" __tablename__ = "locations"
+1 -1
View File
@@ -56,7 +56,7 @@ async def handle_validation_error(
message.bot, message.bot,
) )
error_message = await message.answer(str(e)) error_message = await message.answer("" + str(e))
await state.update_data( await state.update_data(
error_message_id=error_message.message_id, error_message_id=error_message.message_id,
) )
+1 -1
View File
@@ -38,7 +38,7 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
environment: environment:
BOT_TOKEN: ${BOT_TOKEN:-6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q} BOT_TOKEN: ${BOT_TOKEN:-6943803094:AAFxMjuiaqLlQbITUOVPlKx6SKIofKrThwk}
REDIS_URL: redis://redis:${REDIS_PORT:-6379}/ REDIS_URL: redis://redis:${REDIS_PORT:-6379}/
SQLALCHEMY_DATABASE_URI: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} SQLALCHEMY_DATABASE_URI: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
entrypoint: ["bash", "-c"] entrypoint: ["bash", "-c"]
+1 -1
View File
@@ -1,6 +1,6 @@
# For app # For app
BOT_TOKEN = <your_bot_token> # default: 6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q BOT_TOKEN = <your_bot_token> # default: 6943803094:AAFxMjuiaqLlQbITUOVPlKx6SKIofKrThwk
SQLALCHEMY_DATABASE_URI = <database_uri> # no need to specify if docker is used SQLALCHEMY_DATABASE_URI = <database_uri> # no need to specify if docker is used
REDIS_URL = <redis_url> # no need to specify if docker is used REDIS_URL = <redis_url> # no need to specify if docker is used