feat: Added notes creation, view and deletion, added route planning, added location list with current weather and nearby locations, code improvements and fixes

This commit is contained in:
ITQ
2024-03-26 07:49:50 +03:00
parent 65719a61ef
commit 88dfe1704d
24 changed files with 1571 additions and 301 deletions
+4 -2
View File
@@ -6,7 +6,7 @@ from aiogram import Bot, Dispatcher
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from aiogram.fsm.storage.redis import RedisStorage from aiogram.fsm.storage.redis import RedisStorage
from app.callbacks import menu, profile, travels from app.callbacks import location, menu, notes, profile, travel
from app.config import Config from app.config import Config
from app.handlers import ( from app.handlers import (
create_travel_command, create_travel_command,
@@ -40,7 +40,9 @@ async def main() -> None:
travels_command.router, travels_command.router,
menu.router, menu.router,
profile.router, profile.router,
travels.router, travel.router,
location.router,
notes.router,
) )
await bot.delete_webhook(drop_pending_updates=True) await bot.delete_webhook(drop_pending_updates=True)
@@ -7,258 +7,25 @@ from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import StateFilter from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message from aiogram.types import CallbackQuery, Message
import sqlalchemy as sa
from app import messages, session from app import messages, session
from app.config import Config from app.config import Config
from app.filters.user import Registered, RegisteredCallback from app.filters.user import Registered, RegisteredCallback
from app.keyboards.builders import travels_keyboard from app.keyboards.builders import locations_keyboard, sights_keyboard
from app.keyboards.confirm_location import get as confirm_location_get from app.keyboards.confirm_location import get as confirm_location_get
from app.keyboards.travel import get as travel_get from app.keyboards.location import get as location_get
from app.models.travel import Location, Travel from app.models.travel import Location, Travel
from app.models.user import User from app.states.travel import (
from app.states.travel import CreateLocationState, TravelAlteringState CreateLocationState,
)
from app.utils.geo import get_location_by_name
from app.utils.sights import find_trips, get_info_by_xid
from app.utils.states import delete_message_from_state, handle_validation_error from app.utils.states import delete_message_from_state, handle_validation_error
from app.utils.weather import get_current_weather
router = Router(name="menu_callback") router = Router(name="location_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 == []:
try:
await callback.message.edit_text(messages.NO_TRAVELS)
except TelegramBadRequest:
pass
else:
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
try:
await callback.message.edit_text(
messages.TRAVELS,
reply_markup=travels_keyboard(
travels,
page,
pages,
user.telegram_id,
),
)
except TelegramBadRequest:
pass
@router.callback_query(
F.data.startswith("travels_page"),
RegisteredCallback(),
StateFilter(None),
)
async def travels_callback(callback: CallbackQuery) -> None:
if callback.data is None or not isinstance(callback.message, Message):
return
page = int(callback.data.replace("travels_page_", ""))
user = User().get_user_by_telegram_id(callback.from_user.id)
travels = user.get_user_travels()
if not travels or travels == []:
try:
await callback.message.edit_text(messages.NO_TRAVELS)
except TelegramBadRequest:
pass
else:
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
try:
await callback.message.edit_text(
messages.TRAVELS,
reply_markup=travels_keyboard(
travels,
page,
pages,
user.telegram_id,
),
)
except TelegramBadRequest:
pass
@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=travel_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("_")
travel = Travel().get_travel_by_id(travel_id)
if not travel:
return
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=travel_get(travel_id),
)
except TelegramBadRequest:
pass
await message.answer(
messages.TRAVEL_UPDATED,
)
await state.clear()
@router.callback_query( @router.callback_query(
@@ -525,8 +292,6 @@ async def location_date_end_entered(
return return
await delete_message_from_state(state, message.chat.id, message.bot)
await state.update_data( await state.update_data(
date_end=datetime.datetime.strftime( date_end=datetime.datetime.strftime(
validated_date_end, validated_date_end,
@@ -536,6 +301,35 @@ async def location_date_end_entered(
data = await state.get_data() data = await state.get_data()
overlapping_location = (
session.query(Location)
.filter(
sa.and_(
Location.travel_id == data["travel_id"],
Location.date_start < data["date_end"],
Location.date_end > data["date_start"],
),
)
.first()
)
if overlapping_location:
await handle_validation_error(
message,
state,
messages.OVERLAPPING_LOCATION,
)
return
await message.answer(
messages.INPUT_TRAVEL_CALLBACK.format(
key="end date",
value=date_end,
),
)
await delete_message_from_state(state, message.chat.id, message.bot)
if "temp_location" in data: if "temp_location" in data:
del data["temp_location"] del data["temp_location"]
@@ -566,26 +360,241 @@ async def location_date_end_entered(
@router.callback_query( @router.callback_query(
F.data.startswith("travel_delete"), F.data.startswith("travel_locations_page"),
RegisteredCallback(), RegisteredCallback(),
StateFilter(None), StateFilter(None),
) )
async def delete_travel_callback( async def travel_locations_page_callback(
callback: CallbackQuery, callback: CallbackQuery,
): ):
if callback.data is None or not isinstance(callback.message, Message): if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return return
travel_id = int(callback.data.replace("travel_delete_", "")) travel_id, page = map(
int,
callback.data.replace("travel_locations_page_", "").split("_"),
)
travel = Travel.get_travel_queryset_by_id(travel_id) travel = Travel.get_travel_by_id(travel_id)
travel.delete() if not travel or travel == []:
return
locations = Travel.get_sorted_locations(travel)
pages = (len(locations) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
try:
await callback.message.edit_text(
messages.LOCATIONS,
reply_markup=locations_keyboard(
locations,
page,
pages,
travel_id,
),
)
except TelegramBadRequest:
pass
@router.callback_query(
F.data.startswith("travel_location_detail"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_detail_location_callback(
callback: CallbackQuery,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
location_id = int(callback.data.replace("travel_location_detail_", ""))
location = Location.get_location_by_id(location_id)
if not location or location == []:
return
try:
await callback.message.edit_text(
location.get_location_text(),
reply_markup=location_get(location.travel.id, location.id),
)
except TelegramBadRequest:
pass
@router.callback_query(
F.data.startswith("travel_locationdelete"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_locations_delete_callback(
callback: CallbackQuery,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
location_id = int(callback.data.replace("travel_locationdelete_", ""))
location_queryset = Location.get_location_queryset_by_id(location_id)
if not location_queryset or location_queryset == []:
return
travel = location_queryset.first().travel
location_queryset.delete()
session.commit() session.commit()
await callback.message.answer(messages.DELETED_TRAVEL) locations = Travel.get_sorted_locations(travel)
await callback.message.delete() pages = (len(locations) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
try:
await callback.message.edit_text(
messages.LOCATIONS,
reply_markup=locations_keyboard(
locations,
0,
pages,
travel.id,
),
)
except TelegramBadRequest:
pass
await callback.message.answer(
messages.LOCATION_DELETED,
)
await callback.answer()
@router.callback_query(
F.data.startswith("travel_locationsights"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_locationsights_callback(
callback: CallbackQuery,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
location_id = int(callback.data.replace("travel_locationsights_", ""))
location = Location.get_location_by_id(location_id)
if not location or location == []:
return
geocode = get_location_by_name(location.location)
sights = find_trips(geocode[1].raw.get("lat"), geocode[1].raw.get("lon"))
if sights is None or len(sights) == 0:
await callback.message.answer(
messages.NO_SIGHTS_FOUND.format(
location=location.location,
distance=Config.NEARBY_SIGHTS_RADIUS,
),
)
else:
await callback.message.answer(
messages.SIGHTS_HEADER
+ messages.SIGHTS_FOOTER.format(
location=location.location,
sights_count=len(sights),
distance=Config.NEARBY_SIGHTS_RADIUS,
),
reply_markup=sights_keyboard(sights[:20]),
)
await callback.answer()
@router.callback_query(
F.data.startswith("travel_sight_detail"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_sight_detail_callback(
callback: CallbackQuery,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
sight_xid = callback.data.replace("travel_sight_detail_", "")
await get_info_by_xid(callback, sight_xid)
await callback.answer()
@router.callback_query(
F.data.startswith("travel_locationweather"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_locationweather_callback(
callback: CallbackQuery,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
location_id = int(callback.data.replace("travel_locationweather_", ""))
location = Location.get_location_by_id(location_id)
if not location or location == []:
return
geocode = get_location_by_name(location.location)
weather = get_current_weather(
geocode[1].raw.get("lat"),
geocode[1].raw.get("lon"),
)
await callback.message.answer(
messages.LOCATION_WEATHER.format(
location=location.location,
weather_main=weather.get("weather")[0].get("main"),
temp=weather.get("main").get("temp"),
feels_like=weather.get("main").get("feels_like"),
temp_min=weather.get("main").get("temp_min"),
temp_max=weather.get("main").get("temp_max"),
pressure=weather.get("main").get("pressure"),
humidity=weather.get("main").get("humidity"),
),
reply_to_message_id=callback.message.message_id,
)
await callback.answer() await callback.answer()
+13
View File
@@ -91,3 +91,16 @@ async def travels_callback(
) )
await callback.answer() await callback.answer()
@router.callback_query(
F.data == "menu_help",
RegisteredCallback(),
StateFilter(None),
)
async def help_callback(callback: CallbackQuery) -> None:
if not isinstance(callback.message, Message):
return
await callback.message.answer(messages.HELP_MESSAGE)
await callback.answer()
+300
View File
@@ -0,0 +1,300 @@
__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, session
from app.config import Config
from app.filters.user import RegisteredCallback
from app.keyboards.builders import notes_keyboard
from app.keyboards.note import get as notes_get
from app.models.travel import Note, Travel
from app.states.travel import (
CreateNoteState,
)
router = Router(name="menu_callback")
@router.callback_query(
F.data.startswith("travel_add_note"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_add_note_callback(
callback: CallbackQuery,
state: FSMContext,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
travel_id = int(callback.data.replace("travel_add_note_", ""))
travel = Travel.get_travel_by_id(travel_id)
if not travel or travel == []:
return
await state.set_state(CreateNoteState.file_id)
await state.update_data(travel_id=travel_id)
await callback.message.answer(
messages.ADD_NOTE,
)
await callback.answer()
@router.message(
CreateNoteState.file_id,
)
async def create_note_file_id(message: Message, state: FSMContext):
if message.from_user is None:
return
if message.text == "/cancel":
await message.answer(
messages.ACTION_CANCELED,
)
await state.update_data()
await message.delete()
await state.clear()
return
if message.photo is None and message.document is None:
return
if message.photo is not None:
await state.update_data(
file_type="photo",
file_id=message.photo[-1].file_id,
file_name="photo",
)
# await message.answer_photo(message.photo[-1].file_id)
elif message.document is not None:
await state.update_data(
file_type="document",
file_id=message.document.file_id,
file_name=message.document.file_name,
)
# await message.answer_document(message.document.file_id)
data = await state.get_data()
data["author_id"] = message.from_user.id
session.add(Note(**data))
session.commit()
await message.answer(
messages.NOTE_ADDED.format(file_name=data["file_name"]),
)
await state.clear()
@router.callback_query(
F.data.startswith("travel_notes_page"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_notes_page_callback(
callback: CallbackQuery,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
travel_id, page = map(
int,
callback.data.replace("travel_notes_page_", "").split("_"),
)
travel = Travel.get_travel_queryset_by_id(travel_id)
if not travel or travel == []:
return
travel = travel.first()
notes = Travel().get_notes(callback.from_user.id, travel, public=False)
pages = (len(notes) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
try:
await callback.message.edit_text(
messages.NOTES,
reply_markup=notes_keyboard(notes, page, pages, travel.id),
)
except TelegramBadRequest:
pass
@router.callback_query(
F.data.startswith("travel_note_detail"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_note_detail_callback(
callback: CallbackQuery,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
note_id = int(callback.data.replace("travel_note_detail_", ""))
note = Note.get_note_by_id(note_id)
if not note or note == []:
return
try:
await callback.message.edit_text(
note.get_note_text(),
reply_markup=notes_get(travel_id=note.travel.id, note=note),
)
except TelegramBadRequest:
pass
@router.callback_query(
F.data.startswith("travel_notesend"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_notesend_callback(
callback: CallbackQuery,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
note_id = int(callback.data.replace("travel_notesend_", ""))
note = Note.get_note_by_id(note_id)
if not note or note == []:
return
if note.file_type == "photo":
await callback.message.answer_photo(
note.file_id,
reply_to_message_id=callback.message.message_id,
)
elif note.file_type == "document":
await callback.message.answer_document(
note.file_id,
reply_to_message_id=callback.message.message_id,
)
await callback.answer()
@router.callback_query(
F.data.startswith("travel_note_change_privacy"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_note_change_privacy_callback(
callback: CallbackQuery,
state: FSMContext,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
note_id = int(callback.data.replace("travel_note_change_privacy_", ""))
note = Note().get_note_by_id(note_id)
if not note or note == []:
return
if note.public:
note.public = False
else:
note.public = True
session.commit()
try:
await callback.message.edit_text(
note.get_note_text(),
reply_markup=notes_get(travel_id=note.travel.id, note=note),
)
except TelegramBadRequest:
pass
@router.callback_query(
F.data.startswith("travel_notedelete"),
RegisteredCallback(),
StateFilter(None),
)
async def travel_notedelete_callback(
callback: CallbackQuery,
):
if (
callback.message is None
or callback.data is None
or not isinstance(callback.message, Message)
):
return
note_id = int(callback.data.replace("travel_notedelete_", ""))
note = Note().get_note_queryset_by_id(note_id)
note_first = note.first()
file_name = note_first.file_name
travel = note_first.travel
if not note or note == []:
return
note.delete()
session.commit()
notes = Travel().get_notes(callback.from_user.id, travel, public=False)
pages = (len(notes) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
try:
await callback.message.edit_text(
messages.NOTES,
reply_markup=notes_keyboard(notes, 0, pages, travel.id),
)
except TelegramBadRequest:
pass
await callback.message.answer(
messages.NOTE_DELETED.format(file_name=file_name),
)
+13 -2
View File
@@ -22,7 +22,7 @@ router = Router(name="profile_callback")
@router.callback_query( @router.callback_query(
F.data.startswith("profile_change_"), F.data.startswith("profile_change"),
StateFilter(None), StateFilter(None),
RegisteredCallback(), RegisteredCallback(),
) )
@@ -227,8 +227,12 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
state_data = await state.get_data() state_data = await state.get_data()
user = User.get_user_queryset_by_telegram_id(message.from_user.id) user = User.get_user_queryset_by_telegram_id(message.from_user.id)
user_first = user.first()
if isinstance(state_data["value"], list): if isinstance(state_data["value"], list):
old_value = user_first.country + ", " + user_first.city
new_value = state_data["value"][0] + ", " + state_data["value"][1]
user.update( user.update(
{ {
"country": state_data["value"][0], "country": state_data["value"][0],
@@ -241,6 +245,9 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
except TelegramBadRequest: except TelegramBadRequest:
pass pass
else: else:
old_value = getattr(user.first(), str(column))
new_value = state_data["value"]
data = {state_data["column"]: state_data["value"]} data = {state_data["column"]: state_data["value"]}
user.update(data) user.update(data)
@@ -260,7 +267,11 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
pass pass
await message.answer( await message.answer(
messages.PROFILE_UPDATED, messages.PROFILE_UPDATED.format(
key=state_data["column"],
old_value=old_value if old_value else messages.NOT_SET,
new_value=(new_value if new_value else messages.NOT_SET),
),
reply_markup=ReplyKeyboardRemove(), reply_markup=ReplyKeyboardRemove(),
) )
+303
View File
@@ -0,0 +1,303 @@
__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, session
from app.config import Config
from app.filters.user import Registered, RegisteredCallback
from app.keyboards.builders import travels_keyboard
from app.keyboards.travel import get as travel_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 == []:
try:
await callback.message.edit_text(messages.NO_TRAVELS)
except TelegramBadRequest:
pass
else:
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
try:
await callback.message.edit_text(
messages.TRAVELS,
reply_markup=travels_keyboard(
travels,
page,
pages,
user.telegram_id,
),
)
except TelegramBadRequest:
pass
@router.callback_query(
F.data.startswith("travels_page"),
RegisteredCallback(),
StateFilter(None),
)
async def travels_callback(callback: CallbackQuery) -> None:
if callback.data is None or not isinstance(callback.message, Message):
return
page = int(callback.data.replace("travels_page_", ""))
user = User().get_user_by_telegram_id(callback.from_user.id)
travels = user.get_user_travels()
if not travels or travels == []:
try:
await callback.message.edit_text(messages.NO_TRAVELS)
except TelegramBadRequest:
pass
else:
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
try:
await callback.message.edit_text(
messages.TRAVELS,
reply_markup=travels_keyboard(
travels,
page,
pages,
user.telegram_id,
),
)
except TelegramBadRequest:
pass
@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
try:
await callback.message.edit_text(
travel.get_travel_text(),
reply_markup=travel_get(travel),
)
except TelegramBadRequest:
pass
@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("_")
travel = Travel().get_travel_by_id(travel_id)
if not travel:
return
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=travel_get(travel),
)
except TelegramBadRequest:
pass
await message.answer(
messages.TRAVEL_UPDATED,
)
await state.clear()
@router.callback_query(
F.data.startswith("travel_delete"),
RegisteredCallback(),
StateFilter(None),
)
async def delete_travel_callback(
callback: CallbackQuery,
):
if callback.data is None or not isinstance(callback.message, Message):
return
travel_id = int(callback.data.replace("travel_delete_", ""))
user = User().get_user_by_telegram_id(callback.from_user.id)
travel = Travel.get_travel_queryset_by_id(travel_id)
travel.delete()
session.commit()
travels = user.get_user_travels()
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
await callback.message.answer(messages.DELETED_TRAVEL)
await callback.message.edit_text(
messages.TRAVELS,
reply_markup=travels_keyboard(
travels,
0,
pages,
callback.from_user.id,
),
)
await callback.answer()
+7
View File
@@ -17,4 +17,11 @@ class Config:
"REDIS_URL", "REDIS_URL",
"redis://localhost:6379", "redis://localhost:6379",
) )
OPENTRIPMAP_API_KEY = os.getenv(
"OPENTRIPMAP_API_KEY",
)
OPENWEATHERMAP_API_KEY = os.getenv(
"OPENWEATHERMAP_API_KEY",
)
NEARBY_SIGHTS_RADIUS = 2000
PAGE_SIZE = 6 PAGE_SIZE = 6
+176 -1
View File
@@ -58,9 +58,10 @@ def travels_keyboard(travels: list, page: int, pages: int, user_id: int):
), ),
) )
total_pages = 1 if pages == 0 else pages
navigation_row.append( navigation_row.append(
InlineKeyboardButton( InlineKeyboardButton(
text=f"{page + 1}/{pages}", text=f"{page + 1}/{total_pages}",
callback_data="pass", callback_data="pass",
), ),
) )
@@ -83,3 +84,177 @@ def travels_keyboard(travels: list, page: int, pages: int, user_id: int):
builder.row(*navigation_row) builder.row(*navigation_row)
return builder.as_markup() return builder.as_markup()
def locations_keyboard(locations: list, page: int, pages: int, travel_id: int):
builder = InlineKeyboardBuilder()
rows = []
start_index = page * Config.PAGE_SIZE
end_index = min((page + 1) * Config.PAGE_SIZE, len(locations))
for location in locations[start_index:end_index]:
button_text = location.location
rows.append(
InlineKeyboardButton(
text=button_text,
callback_data=f"travel_location_detail_{location.id}",
),
)
for _ in range(0, Config.PAGE_SIZE - len(rows)):
rows.append(InlineKeyboardButton(text=" ", callback_data="pass"))
builder.row(*rows, width=2)
navigation_row = []
if page > 0:
navigation_row.append(
InlineKeyboardButton(
text="⬅️",
callback_data=f"travel_locations_{travel_id}_{page - 1}",
),
)
else:
navigation_row.append(
InlineKeyboardButton(
text=" ",
callback_data="pass",
),
)
total_pages = 1 if pages == 0 else pages
navigation_row.append(
InlineKeyboardButton(
text=f"{page + 1}/{total_pages}",
callback_data="pass",
),
)
if page < pages - 1:
navigation_row.append(
InlineKeyboardButton(
text="➡️",
callback_data=f"travel_locations_{travel_id}_{page + 1}",
),
)
else:
navigation_row.append(
InlineKeyboardButton(
text=" ",
callback_data="pass",
),
)
builder.row(*navigation_row)
builder.row(
InlineKeyboardButton(
text="⬅️",
callback_data=f"travel_detail_{travel_id}",
),
)
return builder.as_markup()
def notes_keyboard(notes, page: int, pages: int, travel_id: int):
builder = InlineKeyboardBuilder()
rows = []
start_index = page * Config.PAGE_SIZE
end_index = min((page + 1) * Config.PAGE_SIZE, len(notes))
for note in notes[start_index:end_index]:
if note.file_type == "photo":
button_text = f"Photo ID: {note.id}"
else:
button_text = note.file_name
rows.append(
InlineKeyboardButton(
text=button_text,
callback_data=f"travel_note_detail_{note.id}",
),
)
for _ in range(0, Config.PAGE_SIZE - len(rows)):
rows.append(InlineKeyboardButton(text=" ", callback_data="pass"))
builder.row(*rows, width=2)
navigation_row = []
if page > 0:
navigation_row.append(
InlineKeyboardButton(
text="⬅️",
callback_data=f"travel_notes_page_{travel_id}_{page - 1}",
),
)
else:
navigation_row.append(
InlineKeyboardButton(
text=" ",
callback_data="pass",
),
)
total_pages = 1 if pages == 0 else pages
navigation_row.append(
InlineKeyboardButton(
text=f"{page + 1}/{total_pages}",
callback_data="pass",
),
)
if page < pages - 1:
navigation_row.append(
InlineKeyboardButton(
text="➡️",
callback_data=f"travel_notes_page_{travel_id}_{page + 1}",
),
)
else:
navigation_row.append(
InlineKeyboardButton(
text=" ",
callback_data="pass",
),
)
builder.row(*navigation_row)
builder.row(
InlineKeyboardButton(
text="⬅️",
callback_data=f"travel_detail_{travel_id}",
),
)
return builder.as_markup()
def sights_keyboard(sights: list):
builder = InlineKeyboardBuilder()
rows = []
for sight in sights:
button_text = sight[0]
rows.append(
InlineKeyboardButton(
text=button_text,
callback_data=f"travel_sight_detail_{sight[1]}",
),
)
for _ in range(0, 20 - len(rows)):
rows.append(InlineKeyboardButton(text=" ", callback_data="pass"))
builder.row(*rows, width=2)
return builder.as_markup()
+48
View File
@@ -0,0 +1,48 @@
__all__ = ("get",)
from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder
def get(travel_id: int, location_id: int):
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="⏩ Get nearby sights",
callback_data=f"travel_locationsights_{location_id}",
),
)
builder.row(
types.InlineKeyboardButton(
text="☁️ Current weather",
callback_data=f"travel_locationweather_{location_id}",
),
)
builder.row(
types.InlineKeyboardButton(
text="❌ Delete location",
callback_data=f"travel_locationdelete_{location_id}",
),
)
builder.row(
types.InlineKeyboardButton(
text="⬅️",
callback_data=f"travel_locations_page_{travel_id}_0",
),
)
return builder.as_markup()
def get_public(travel_id: int, location_id: int):
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="⬅️",
callback_data=f"travel_locations_page_{travel_id}_0",
),
)
return builder.as_markup()
+2 -2
View File
@@ -23,8 +23,8 @@ def get():
callback_data="menu_travels", callback_data="menu_travels",
), ),
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="🔵 Temp", text="❓ Help",
callback_data="menu_temp", callback_data="menu_help",
), ),
) )
+46
View File
@@ -0,0 +1,46 @@
__all__ = ("get",)
from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder
def get(travel_id: int, note):
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="⏩ View note",
callback_data=f"travel_notesend_{note.id}",
),
)
if note.public:
builder.row(
types.InlineKeyboardButton(
text="🔒 Make private",
callback_data=f"travel_note_change_privacy_{note.id}",
),
)
else:
builder.row(
types.InlineKeyboardButton(
text="🔓 Make public",
callback_data=f"travel_note_change_privacy_{note.id}",
),
)
builder.row(
types.InlineKeyboardButton(
text="❌ Delete note",
callback_data=f"travel_notedelete_{note.id}",
),
)
builder.row(
types.InlineKeyboardButton(
text="⬅️",
callback_data=f"travel_detail_{travel_id}",
),
)
return builder.as_markup()
+133 -10
View File
@@ -3,54 +3,177 @@ __all__ = ("get",)
from aiogram import types from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from app.models.travel import Travel
from app.utils.geo import get_location_by_name
from app.utils.map import get_url_map
def get(travel: Travel):
locations = Travel().get_sorted_locations(travel, asc=False)
coordinats = []
for location in locations:
geocode = get_location_by_name(location.location)
coordinats.append(
[geocode[1].raw.get("lat"), geocode[1].raw.get("lon")],
)
def get(travel_id: int):
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row( builder.row(
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="📝 Change title", text="📝 Change title",
callback_data=f"travel_change_{travel_id}_title", callback_data=f"travel_change_{travel.id}_title",
), ),
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="️ Change description", text="️ Change description",
callback_data=f"travel_change_{travel_id}_description", callback_data=f"travel_change_{travel.id}_description",
), ),
) )
builder.row( builder.row(
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="🗺️ Locations", text="🗺️ Locations",
callback_data=f"travel_locations_{travel_id}", callback_data=f"travel_locations_page_{travel.id}_0",
), ),
types.InlineKeyboardButton( types.InlineKeyboardButton(
text=" Add location", text=" Add location",
callback_data=f"travel_add_location_{travel_id}", callback_data=f"travel_add_location_{travel.id}",
), ),
) )
builder.row( builder.row(
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="👤 Users", text="👤 Users",
callback_data=f"travel_users_{travel_id}", callback_data=f"travel_users_page_{travel.id}_0",
), ),
types.InlineKeyboardButton( types.InlineKeyboardButton(
text=" Add user", text=" Add user",
callback_data=f"travel_add_user_{travel_id}", callback_data=f"travel_add_user_{travel.id}",
), ),
) )
builder.row( builder.row(
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="📝 Notes", text="📝 Notes",
callback_data=f"travel_notes_{travel_id}", callback_data=f"travel_notes_page_{travel.id}_0",
), ),
types.InlineKeyboardButton( types.InlineKeyboardButton(
text=" Add note", text=" Add note",
callback_data=f"travel_add_note_{travel_id}", callback_data=f"travel_add_note_{travel.id}",
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route by car",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="car",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route on foot",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="foot",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route by bike",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="bike",
),
),
), ),
) )
builder.row( builder.row(
types.InlineKeyboardButton( types.InlineKeyboardButton(
text="❌ Delete travel", text="❌ Delete travel",
callback_data=f"travel_delete_{travel_id}", callback_data=f"travel_delete_{travel.id}",
),
)
builder.row(
types.InlineKeyboardButton(
text="⬅️",
callback_data="travels",
),
)
return builder.as_markup()
def get_public(travel: Travel):
locations = Travel().get_sorted_locations(travel, asc=False)
coordinats = []
for location in locations:
geocode = get_location_by_name(location.location)
coordinats.append(
[geocode[1].raw.get("lat"), geocode[1].raw.get("lon")],
)
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="🗺️ Locations",
callback_data=f"travel_locations_{travel.id}",
),
)
builder.row(
types.InlineKeyboardButton(
text="👤 Users",
callback_data=f"travel_users_{travel.id}",
),
)
builder.row(
types.InlineKeyboardButton(
text="📝 Notes",
callback_data=f"travel_notes_{travel.id}",
),
types.InlineKeyboardButton(
text=" Add note",
callback_data=f"travel_add_note_{travel.id}",
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route by car",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="car",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route on foot",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="foot",
),
),
),
)
builder.row(
types.InlineKeyboardButton(
text="🗺️ Route by bike",
web_app=types.WebAppInfo(
url=get_url_map(
coordinats=coordinats,
profile="bike",
),
),
), ),
) )
builder.row( builder.row(
+34 -4
View File
@@ -1,18 +1,48 @@
# flake8: noqa # flake8: noqa
MENU = "<b>Menu:</b>" MENU = "<b>Menu:</b>"
NOTES = "📝 <b>Notes:</b>\n"
NOTE_DETAIL = "📝 <b>Note detail:</b>\n\n\tFile name: <b>{file_name}</b>\n\tFile type: <b>{file_type}</b>\n\tPublic: <b>{public}</b>"
NOTE_ADDED = "✅ Note <b>{file_name}</b> added successfully."
NOTE_DELETED = "❌ Note <b>{file_name}</b> deleted."
LOCATIONS = "🗺️ <b>Locations:</b>"
LOCATION_DELETED = "❌ Location deleted."
LOCATION_DETAIL = (
"🗺️ <b>Location detail:</b>\n\n"
"\t<b>Location:</b> {location}\n"
"\t<b>Date start:</b> {date_start}\n"
"\t<b>Date end:</b> {date_end}"
)
LOCATION_WEATHER = (
"🌤️ <b>{location} weather:</b>\n\n"
"\t<b>☁️ Weather:</b> {weather_main}\n"
"\t<b>🌡️ Current tempurature:</b> {temp} °C\n"
"\t<b>🤗 Feels like:</b> {feels_like} °C\n"
"\t<b>❄️ Min. tempurature:</b> {temp_min} °C\n"
"\t<b>🔥 Max. tempurature:</b> {temp_max} °C\n"
"\t<b>⬇️ Pressure:</b> {pressure} hektopascals\n"
"\t<b>💨 Humidity:</b> {humidity}%\n"
)
SIGHTS_HEADER = "🗺️ <b>Sights:</b>\n"
SIGHTS_FOOTER = "Found {sights_count} sights within {distance} m from: <b>{location}</b>."
NO_SIGHTS_FOUND = (
"No sights found within {distance} m from: <b>{location}</b>."
)
SIGHT_DETAIL = "🗺️ <b>Sight detail:</b>\n\n"
CREATE_LOCATION = "✈️ Lets create new location!" CREATE_LOCATION = "✈️ Lets create new location!"
ENTER_LOCATION = "Enter location:" ENTER_LOCATION = "Enter location:\n<i>Format: country, city, ... etc</i>\n<i>Example: Kremlin, Moscow, Russia</i>\n<i>Enter /cancel to cancel creating.</i>"
CONFIRM_LOCATION = "Is this location correct: <b>{location}</b>?" CONFIRM_LOCATION = "Is this location correct: <b>{location}</b>?"
CONFIRMATION_REEJECTED = ( CONFIRMATION_REEJECTED = (
"❌ Confirmation rejected. Please re-enter the location." "❌ Confirmation rejected. Please re-enter the location."
) )
OVERLAPPING_LOCATION = "Dates overlap with another location in the same travel(enter /cancel if you cant fix this)."
ENTER_LOCATION_DATE_START = "Enter location start datetime(in UTC) in this format:\n<i>Format: YYYY-MM-DD HH:MM</i>\n<i>Example: 2022-01-01 00:00</i>" ENTER_LOCATION_DATE_START = "Enter location start datetime(in UTC) in this format:\n<i>Format: YYYY-MM-DD HH:MM</i>\n<i>Example: 2022-01-01 00:00</i>"
ENTER_LOCATION_DATE_END = "Enter location end datetime(in UTC) in this format:\n<i>Format: YYYY-MM-DD HH:MM</i>\n<i>Example: 2022-01-01 00:00</i>" ENTER_LOCATION_DATE_END = "Enter location end datetime(in UTC) in this format:\n<i>Format: YYYY-MM-DD HH:MM</i>\n<i>Example: 2022-01-01 00:00</i>"
INVALID_DATE_END = "End date can't be earlier or equal to start date." INVALID_DATE_END = "End date can't be earlier or equal to start date."
LOCATION_ADDED = "✅ Location added" LOCATION_ADDED = "✅ Location added"
ADD_NOTE = "✏️ Send me file or photo to add note.\n<i>Enter /cancel to cancel creating.</i>"
DELETED_TRAVEL = "✅ Travel deleted" DELETED_TRAVEL = "✅ Travel deleted"
TRAVELS = "📃 <b>Travels:</b>\n<i>👑 - owner</i>" 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."
@@ -47,7 +77,7 @@ HELP_MESSAGE = (
"/help - Show this message\n" "/help - Show this message\n"
"/menu - Show the main menu\n" "/menu - Show the main menu\n"
"/profile - View and edit your profile\n" "/profile - View and edit your profile\n"
"/create_travel - Create new travel\n" "/create_travel - Create a new travel\n"
"/travels - View and edit your travels\n" "/travels - View and edit your travels\n"
"/cancel - Cancel the current action\n\n" "/cancel - Cancel the current action\n\n"
"❓ If you have any questions/issues, feel free to contact us via @itq_travel_agent_support_bot on Telegram." "❓ If you have any questions/issues, feel free to contact us via @itq_travel_agent_support_bot on Telegram."
@@ -78,7 +108,7 @@ PROFILE = (
NOT_SET = "<i>Not set</i>" NOT_SET = "<i>Not set</i>"
EDIT_USERNAME = "Enter your username:\n<i>Allowed characters: a-z, A-Z, 0-9, _</i>\n<i>Length: 5-20 characters</i>" EDIT_USERNAME = "Enter your username:\n<i>Allowed characters: a-z, A-Z, 0-9, _</i>\n<i>Length: 5-20 characters</i>"
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 {key} updated\n\t<i>Old value: {old_value}</i>\n\t<i>New value: {new_value}</i>"
CHANGE_CANCELED = "❌ Change canceled" CHANGE_CANCELED = "❌ Change canceled"
PROCCESSING = "⌛️ Processing..." PROCCESSING = "⌛️ Processing..."
@@ -1,8 +1,8 @@
"""Added travel models """Added travel models
Revision ID: 4dea8f302149 Revision ID: 78ab1b779ca8
Revises: 4914f00ae14a Revises: 4914f00ae14a
Create Date: 2024-03-24 17:56:20.975589 Create Date: 2024-03-25 17:23:37.917899
""" """
@@ -13,7 +13,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '4dea8f302149' revision: str = '78ab1b779ca8'
down_revision: Union[str, None] = '4914f00ae14a' down_revision: Union[str, None] = '4914f00ae14a'
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
@@ -28,8 +28,7 @@ def upgrade() -> None:
sa.Column('description', sa.String(length=100), nullable=True), sa.Column('description', sa.String(length=100), nullable=True),
sa.Column('author_id', sa.BigInteger(), nullable=False), sa.Column('author_id', sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
['author_id'], ['author_id'], ['users.telegram_id'], ondelete='CASCADE'
['users.telegram_id'],
), ),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('title'), sa.UniqueConstraint('title'),
@@ -43,8 +42,7 @@ def upgrade() -> None:
sa.Column('date_end', sa.DateTime(timezone=True), nullable=False), sa.Column('date_end', sa.DateTime(timezone=True), nullable=False),
sa.Column('travel_id', sa.Integer(), nullable=False), sa.Column('travel_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
['travel_id'], ['travel_id'], ['travels.id'], ondelete='CASCADE'
['travels.id'],
), ),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
) )
@@ -59,12 +57,10 @@ def upgrade() -> None:
sa.Column('author_id', sa.BigInteger(), nullable=False), sa.Column('author_id', sa.BigInteger(), nullable=False),
sa.Column('travel_id', sa.Integer(), nullable=False), sa.Column('travel_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
['author_id'], ['author_id'], ['users.telegram_id'], ondelete='CASCADE'
['users.telegram_id'],
), ),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
['travel_id'], ['travel_id'], ['travels.id'], ondelete='CASCADE'
['travels.id'],
), ),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
) )
@@ -74,12 +70,10 @@ def upgrade() -> None:
sa.Column('user_id', sa.BigInteger(), nullable=True), sa.Column('user_id', sa.BigInteger(), nullable=True),
sa.Column('travel_id', sa.Integer(), nullable=True), sa.Column('travel_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
['travel_id'], ['travel_id'], ['travels.id'], ondelete='CASCADE'
['travels.id'],
), ),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
['user_id'], ['user_id'], ['users.telegram_id'], ondelete='CASCADE'
['users.telegram_id'],
), ),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
+75 -10
View File
@@ -14,8 +14,16 @@ from app.utils.geo import get_location_by_name
association_table = sa.Table( association_table = sa.Table(
"user_travel_association", "user_travel_association",
Base.metadata, Base.metadata,
sa.Column("user_id", sa.BigInteger, sa.ForeignKey("users.telegram_id")), sa.Column(
sa.Column("travel_id", sa.Integer, sa.ForeignKey("travels.id")), "user_id",
sa.BigInteger,
sa.ForeignKey("users.telegram_id", ondelete="CASCADE"),
),
sa.Column(
"travel_id",
sa.Integer,
sa.ForeignKey("travels.id", ondelete="CASCADE"),
),
) )
@@ -42,7 +50,7 @@ class Travel(Base):
author_id = sa.Column( author_id = sa.Column(
sa.BigInteger, sa.BigInteger,
sa.ForeignKey(User.telegram_id), sa.ForeignKey(User.telegram_id, ondelete="CASCADE"),
nullable=False, nullable=False,
) )
@@ -83,6 +91,18 @@ class Travel(Base):
), ),
) )
@classmethod
def get_notes(cls, author_id, travel, public=True):
return (
session.query(Note)
.filter(
Note.author_id == author_id,
Note.travel_id == travel.id,
Note.public == public,
)
.all()
)
@classmethod @classmethod
def get_travel_by_id(cls, travel_id): def get_travel_by_id(cls, travel_id):
return session.query(Travel).filter(Travel.id == travel_id).first() return session.query(Travel).filter(Travel.id == travel_id).first()
@@ -91,6 +111,14 @@ class Travel(Base):
def get_travel_queryset_by_id(cls, travel_id): def get_travel_queryset_by_id(cls, travel_id):
return session.query(Travel).filter(Travel.id == travel_id) return session.query(Travel).filter(Travel.id == travel_id)
@classmethod
def get_sorted_locations(cls, travel, asc=True):
return sorted(
travel.locations,
key=lambda location: location.date_end,
reverse=asc,
)
class Location(Base): class Location(Base):
__tablename__ = "locations" __tablename__ = "locations"
@@ -114,17 +142,16 @@ class Location(Base):
travel_id = sa.Column( travel_id = sa.Column(
sa.Integer, sa.Integer,
sa.ForeignKey("travels.id"), sa.ForeignKey("travels.id", ondelete="CASCADE"),
nullable=False, nullable=False,
) )
@validates("location")
def validate_location(self, key, value): def validate_location(self, key, value):
geocoder = get_location_by_name(value) geocode = get_location_by_name(value)
assert geocoder[0], "Invalid location." assert geocode[0], "Invalid location."
return geocoder[1].raw["display_name"] return geocode[1].raw["display_name"]
def validate_date_start(self, key, value): def validate_date_start(self, key, value):
try: try:
@@ -158,6 +185,29 @@ class Location(Base):
return value_datetime return value_datetime
def get_location_text(self):
return messages.LOCATION_DETAIL.format(
location=self.location,
date_start=datetime.datetime.strftime(
self.date_start,
"%Y-%m-%d %H:%M",
),
date_end=datetime.datetime.strftime(
self.date_end,
"%Y-%m-%d %H:%M",
),
)
@classmethod
def get_location_by_id(cls, location_id):
return (
session.query(Location).filter(Location.id == location_id).first()
)
@classmethod
def get_location_queryset_by_id(cls, location_id):
return session.query(Location).filter(Location.id == location_id)
class Note(Base): class Note(Base):
__tablename__ = "notes" __tablename__ = "notes"
@@ -181,11 +231,26 @@ class Note(Base):
author_id = sa.Column( author_id = sa.Column(
sa.BigInteger, sa.BigInteger,
sa.ForeignKey(User.telegram_id), sa.ForeignKey(User.telegram_id, ondelete="CASCADE"),
nullable=False, nullable=False,
) )
travel_id = sa.Column( travel_id = sa.Column(
sa.Integer, sa.Integer,
sa.ForeignKey("travels.id"), sa.ForeignKey("travels.id", ondelete="CASCADE"),
nullable=False, nullable=False,
) )
def get_note_text(self):
return messages.NOTE_DETAIL.format(
file_name=self.file_name,
file_type=self.file_type,
public="Yes" if self.public else "No",
)
@classmethod
def get_note_by_id(cls, note_id):
return session.query(Note).filter(Note.id == note_id).first()
@classmethod
def get_note_queryset_by_id(cls, note_id):
return session.query(Note).filter(Note.id == note_id)
+7
View File
@@ -28,3 +28,10 @@ class CreateLocationState(StatesGroup):
location = State() location = State()
date_start = State() date_start = State()
date_end = State() date_end = State()
class CreateNoteState(StatesGroup):
travel_id = State()
file_id = State()
file_type = State()
file_name = State()
+1 -2
View File
@@ -1,5 +1,5 @@
# type: ignore # type: ignore
__all__ = ("validate_country", "validate_city", "get_location_by_name") __all__ = ("get_location_by_name", "validate_country", "validate_city")
from geopy.exc import GeocoderTimedOut from geopy.exc import GeocoderTimedOut
from geopy.geocoders import Nominatim from geopy.geocoders import Nominatim
@@ -81,7 +81,6 @@ def get_location_by_name(location: str) -> None:
try: try:
geocode = geolocator.geocode( geocode = geolocator.geocode(
location, location,
featuretype="city",
) )
break break
except GeocoderTimedOut: except GeocoderTimedOut:
+12
View File
@@ -0,0 +1,12 @@
__all__ = ("get_url_map",)
def get_url_map(coordinats: list, profile: str):
result_url = "https://graphhopper.com/maps/?"
for coordinat in coordinats:
result_url += f"point={coordinat[0]}, {coordinat[1]}&"
result_url += f"profile={profile}&layer=OpenStreetMap"
return result_url
+108
View File
@@ -0,0 +1,108 @@
__all__ = ("find_trips", "get_info_by_xid")
from aiogram.types import CallbackQuery, Message
import requests
from app import messages
from app.config import Config
def find_trips(lat, lon, type_of_trip="unclassified_objects"):
api_key = Config.OPENTRIPMAP_API_KEY
radius = Config.NEARBY_SIGHTS_RADIUS
result_url = (
"https://api.opentripmap.com/0.1/ru/places/radius"
f"?radius={radius}&"
f"kinds={type_of_trip}&"
f"lon={lon}&"
f"lat={lat}&"
f"limit=20&"
f"apikey={api_key}"
)
data = requests.get(result_url).json()
if data["features"]:
sights = []
for feature in data["features"]:
button_text = (
feature["properties"]["name"]
+ " ("
+ str(round(feature["properties"]["dist"]))
+ "m)"
)
sights.append((button_text, feature["properties"]["xid"]))
return sights
return None
async def get_info_by_xid(callback: CallbackQuery, xid):
if not isinstance(callback.message, Message):
return
api_key = Config.OPENTRIPMAP_API_KEY
result_url = (
f"https://api.opentripmap.com/0.1/ru/places/xid/{xid}?apikey={api_key}"
)
data = requests.get(result_url).json()
text = messages.SIGHT_DETAIL
if data.get("name", ""):
text += (
"\n\t<b>📝 Name:</b> " + data.get("name", "<i>Missing</i>") + "\n"
)
if data.get("address", ""):
address_string = ""
key_order = [
"country",
"state",
"city",
"city_district",
"suburb",
"road",
"house_number",
]
address = data["address"]
for key in key_order:
if address.get(key, ""):
address_string += f" {address.get(key)}, "
text += "\t<b>📫 Address:</b> " + address_string + "\n"
if "wikipedia_extracts" in data:
wikipedia_extracts = data["wikipedia_extracts"]
wikipedia_title = wikipedia_extracts.get("title", "<i>Missing</i>")
wikipedia_description = wikipedia_extracts.get(
"text",
"<i>Missing</i>",
)
text += f"\n\t<b>📝 Wikipedia title:</b> {wikipedia_title}\n"
text += f"\t<b>️ Wikipedia description:</b> {wikipedia_description}\n"
if "wikipedia" in data:
wikipedia_link = data["wikipedia"]
text += f"\t<b>🔗 Wikipedia link:</b> {wikipedia_link}\n"
if "image" in data:
await callback.message.answer_photo(
data["image"],
text,
reply_to_message_id=callback.message.message_id,
)
else:
await callback.message.answer(
text,
reply_to_message_id=callback.message.message_id,
)
+12
View File
@@ -0,0 +1,12 @@
__all__ = "get_weather"
import requests
from app.config import Config
def get_current_weather(lat, lot):
api_key = Config.OPENWEATHERMAP_API_KEY
result_url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lot}&appid={api_key}&lang=en&units=metric" # noqa
return requests.get(result_url).json()
+2
View File
@@ -41,6 +41,8 @@ services:
BOT_TOKEN: ${BOT_TOKEN:-6943803094:AAFxMjuiaqLlQbITUOVPlKx6SKIofKrThwk} 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}
OPENTRIPMAP_API_KEY: ${OPENTRIPMAP_API_KEY:-5ae2e3f221c38a28845f05b65004e18feef4a0f1a06c9554424e9f14}
OPENWEATHERMAP_API_KEY: ${OPENWEATHERMAP_API_KEY:-3673b2ff64c097b3f79fc93e429e7ed1}
entrypoint: ["bash", "-c"] entrypoint: ["bash", "-c"]
command: ["alembic -c app/alembic.ini upgrade head && python -m app"] command: ["alembic -c app/alembic.ini upgrade head && python -m app"]
+1
View File
@@ -1,6 +1,7 @@
black black
mypy mypy
sort-requirements sort-requirements
types-requests==2.31
-r prod.txt -r prod.txt
-r test.txt -r test.txt
+1
View File
@@ -5,4 +5,5 @@ geopy==2.4.1
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
python-dotenv==1.0.1 python-dotenv==1.0.1
redis==5.0.3 redis==5.0.3
requests==2.31.0
sqlalchemy==2.0.28 sqlalchemy==2.0.28
+2
View File
@@ -3,6 +3,8 @@
BOT_TOKEN = <your_bot_token> # default: 6943803094:AAFxMjuiaqLlQbITUOVPlKx6SKIofKrThwk 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
OPENTRIPMAP_API_KEY = <api_key> # get it from https://dev.opentripmap.org/
OPENWEATHERMAP_API_KEY = <api_key> # get it from https://openweathermap.org/
# For docker(remove if you want to keep defaults) # For docker(remove if you want to keep defaults)