You've already forked Travel-Agent
feat: Added travel models, travel creation command, travels list command with pagination, set help message, improvements and fixes
This commit is contained in:
+7
-1
@@ -6,13 +6,15 @@ from aiogram import Bot, Dispatcher
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.fsm.storage.redis import RedisStorage
|
||||
|
||||
from app.callbacks import menu, profile
|
||||
from app.callbacks import menu, profile, travels
|
||||
from app.config import Config
|
||||
from app.handlers import (
|
||||
create_travel_command,
|
||||
help_command,
|
||||
menu_command,
|
||||
profile_command,
|
||||
start_command,
|
||||
travels_command,
|
||||
)
|
||||
from app.middlewares.throttling import ThrottlingMiddleware
|
||||
|
||||
@@ -34,8 +36,12 @@ async def main() -> None:
|
||||
help_command.router,
|
||||
menu_command.router,
|
||||
profile_command.router,
|
||||
create_travel_command.router,
|
||||
travels_command.router,
|
||||
|
||||
menu.router, # type: ignore
|
||||
profile.router, # type: ignore
|
||||
travels.router, # type: ignore
|
||||
)
|
||||
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
|
||||
+60
-1
@@ -3,18 +3,25 @@ __all__ = ()
|
||||
|
||||
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
|
||||
|
||||
from app import messages
|
||||
from app.config import Config
|
||||
from app.filters.user import RegisteredCallback
|
||||
from app.keyboards.builders import travels_keyboard
|
||||
from app.keyboards.profile import get
|
||||
from app.models.user import User
|
||||
from app.states.travel import TravelCreationState
|
||||
|
||||
|
||||
router = Router(name="menu_callback")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "menu_profile", RegisteredCallback())
|
||||
@router.callback_query(
|
||||
F.data == "menu_profile", RegisteredCallback(), StateFilter(None),
|
||||
)
|
||||
async def profile_callback(callback: CallbackQuery) -> None:
|
||||
if callback.data is None or callback.message is None:
|
||||
return
|
||||
@@ -39,3 +46,55 @@ async def profile_callback(callback: CallbackQuery) -> None:
|
||||
await callback.message.delete()
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data == "menu_create_travel", RegisteredCallback(), StateFilter(None),
|
||||
)
|
||||
async def create_travel_callback(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
if callback.data is None or callback.message is None:
|
||||
return
|
||||
|
||||
await callback.message.answer(
|
||||
messages.CREATE_TRAVEL,
|
||||
)
|
||||
await callback.message.answer(
|
||||
messages.INPUT_TRAVEL_TITLE,
|
||||
)
|
||||
await state.set_state(TravelCreationState.title)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
try:
|
||||
await callback.message.delete()
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data == "menu_travels", RegisteredCallback(), StateFilter(None),
|
||||
)
|
||||
async def travels_callback(
|
||||
callback: CallbackQuery,
|
||||
) -> None:
|
||||
page = 0
|
||||
|
||||
user = User().get_user_by_telegram_id(callback.from_user.id)
|
||||
|
||||
travels = user.get_user_travels()
|
||||
|
||||
if not travels or travels == []:
|
||||
await callback.message.answer(messages.NO_TRAVELS)
|
||||
else:
|
||||
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
await callback.message.answer(
|
||||
messages.TRAVELS,
|
||||
reply_markup=travels_keyboard(travels, page, pages),
|
||||
)
|
||||
|
||||
await callback.message.delete()
|
||||
await callback.answer()
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# type: ignore
|
||||
__all__ = ()
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
from app import messages
|
||||
from app.config import Config
|
||||
from app.filters.user import RegisteredCallback
|
||||
from app.keyboards.builders import travels_keyboard
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
router = Router(name="menu_callback")
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travels_page"), RegisteredCallback(), StateFilter(None),
|
||||
)
|
||||
async def travels_callback(callback: CallbackQuery) -> None:
|
||||
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
|
||||
|
||||
await callback.message.edit_text(
|
||||
messages.TRAVELS,
|
||||
reply_markup=travels_keyboard(travels, page, pages),
|
||||
)
|
||||
@@ -17,3 +17,4 @@ class Config:
|
||||
"REDIS_URL",
|
||||
"redis://localhost:6379",
|
||||
)
|
||||
PAGE_SIZE = 6
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
__all__ = ()
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
|
||||
from app import messages, session
|
||||
from app.filters.user import Registered
|
||||
from app.models.travel import Travel
|
||||
from app.states.travel import TravelCreationState
|
||||
from app.utils.states import delete_message_from_state, handle_validation_error
|
||||
|
||||
|
||||
router = Router(name="create_travel_command")
|
||||
|
||||
|
||||
@router.message(Command("create_travel"), Registered(), StateFilter(None))
|
||||
async def command_create_travel_handler(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
if message.from_user is None:
|
||||
return
|
||||
|
||||
await message.answer(
|
||||
messages.CREATE_TRAVEL,
|
||||
)
|
||||
await message.answer(
|
||||
messages.INPUT_TRAVEL_TITLE,
|
||||
)
|
||||
|
||||
await state.set_state(TravelCreationState.title)
|
||||
|
||||
|
||||
@router.message(TravelCreationState.title, F.text)
|
||||
async def name_handler(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
if message.text is None:
|
||||
return
|
||||
|
||||
title = message.text.strip()
|
||||
|
||||
if title == "/cancel":
|
||||
await message.answer(messages.ACTION_CANCELED)
|
||||
await message.delete()
|
||||
|
||||
await delete_message_from_state(state, message.chat.id, message.bot)
|
||||
await state.clear()
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
validated_title = Travel().validate_title(key="title", value=title)
|
||||
except AssertionError as e:
|
||||
await handle_validation_error(message, state, e)
|
||||
|
||||
return
|
||||
|
||||
await delete_message_from_state(state, message.chat.id, message.bot)
|
||||
|
||||
await state.update_data(title=validated_title)
|
||||
await state.set_state(TravelCreationState.description)
|
||||
|
||||
await message.answer(
|
||||
messages.INPUT_TRAVEL_CALLBACK.format(
|
||||
key="title",
|
||||
value=validated_title,
|
||||
),
|
||||
)
|
||||
await message.answer(
|
||||
messages.INPUT_TRAVEL_DESCRIPTION,
|
||||
)
|
||||
|
||||
|
||||
@router.message(TravelCreationState.description, F.text)
|
||||
async def description_handler(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
if message.text is None or message.from_user is None:
|
||||
return
|
||||
|
||||
description = message.text.strip()
|
||||
|
||||
if description == "/cancel":
|
||||
await message.answer(messages.ACTION_CANCELED)
|
||||
await message.delete()
|
||||
|
||||
await delete_message_from_state(state, message.chat.id, message.bot)
|
||||
await state.clear()
|
||||
|
||||
return
|
||||
|
||||
if description == "/skip":
|
||||
await state.update_data(description=None)
|
||||
|
||||
await message.answer(messages.INPUT_TRAVEL_DESCRIPTION_SKIPPED)
|
||||
else:
|
||||
try:
|
||||
validated_description = Travel().validate_description(
|
||||
key="description",
|
||||
value=description,
|
||||
)
|
||||
except AssertionError as e:
|
||||
await handle_validation_error(message, state, e)
|
||||
|
||||
return
|
||||
|
||||
await state.update_data(description=validated_description)
|
||||
await state.set_state(TravelCreationState.error_message_id)
|
||||
|
||||
await message.answer(
|
||||
messages.INPUT_TRAVEL_CALLBACK.format(
|
||||
key="description",
|
||||
value=validated_description,
|
||||
),
|
||||
)
|
||||
|
||||
await delete_message_from_state(state, message.chat.id, message.bot)
|
||||
|
||||
data = await state.get_data()
|
||||
await state.clear()
|
||||
|
||||
if "error_message_id" in data:
|
||||
del data["error_message_id"]
|
||||
|
||||
data["author_id"] = message.from_user.id
|
||||
|
||||
session.add(Travel(**data))
|
||||
session.commit()
|
||||
|
||||
await message.answer(messages.TRAVEL_CREATED.format(title=data["title"]))
|
||||
@@ -0,0 +1,36 @@
|
||||
__all__ = ()
|
||||
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
from app import messages
|
||||
from app.config import Config
|
||||
from app.filters.user import Registered
|
||||
from app.keyboards.builders import travels_keyboard
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
router = Router(name="travels_command")
|
||||
|
||||
|
||||
@router.message(Command("travels"), Registered(), StateFilter(None))
|
||||
async def command_help_handler(message: Message) -> None:
|
||||
page = 0
|
||||
|
||||
if message.from_user is None:
|
||||
return
|
||||
|
||||
user = User().get_user_by_telegram_id(message.from_user.id)
|
||||
|
||||
travels = user.get_user_travels()
|
||||
|
||||
if not travels or travels == []:
|
||||
await message.answer(messages.NO_TRAVELS)
|
||||
else:
|
||||
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
await message.answer(
|
||||
messages.TRAVELS,
|
||||
reply_markup=travels_keyboard(travels, page, pages),
|
||||
)
|
||||
@@ -1,13 +1,76 @@
|
||||
__all__ = ("sex_keyboard",)
|
||||
|
||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
|
||||
|
||||
from app.config import Config
|
||||
|
||||
|
||||
def sex_keyboard(text: str | list):
|
||||
def sex_keyboard(choices: str | list):
|
||||
builder = ReplyKeyboardBuilder()
|
||||
|
||||
if isinstance(text, str):
|
||||
text = [text]
|
||||
if isinstance(choices, str):
|
||||
text = [choices]
|
||||
|
||||
[builder.button(text=txt) for txt in text]
|
||||
return builder.as_markup(resize_keyboard=True)
|
||||
|
||||
|
||||
def travels_keyboard(travels: list, page: int, pages: int):
|
||||
builder = InlineKeyboardBuilder()
|
||||
rows = []
|
||||
|
||||
start_index = page * Config.PAGE_SIZE
|
||||
end_index = min((page + 1) * Config.PAGE_SIZE, len(travels))
|
||||
|
||||
for travel in travels[start_index:end_index]:
|
||||
rows.append(
|
||||
InlineKeyboardButton(
|
||||
text=travel.title,
|
||||
callback_data=f"travel_detail_{travel.id}",
|
||||
),
|
||||
)
|
||||
|
||||
for _ in range(0, Config.PAGE_SIZE - len(rows)):
|
||||
rows.append(InlineKeyboardButton(text=" ", callback_data="pass"))
|
||||
|
||||
builder.row(*rows, width=2)
|
||||
|
||||
if pages > 1:
|
||||
navigation_row = []
|
||||
|
||||
if page > 0:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️", callback_data=f"travels_page_{page - 1}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=" ", callback_data="pass",
|
||||
),
|
||||
)
|
||||
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=f"{page + 1}/{pages}", callback_data="pass",
|
||||
),
|
||||
)
|
||||
|
||||
if page < pages - 1:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text="➡️", callback_data=f"travels_page_{page + 1}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=" ", callback_data="pass",
|
||||
),
|
||||
)
|
||||
|
||||
builder.row(*navigation_row)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
+26
-2
@@ -2,10 +2,34 @@
|
||||
|
||||
MENU = "<b>Menu:</b>"
|
||||
|
||||
TRAVELS = "📃 <b>Travels:</b>"
|
||||
NO_TRAVELS = "No travels yet. You can create one with /create_travel command."
|
||||
CREATE_TRAVEL = "🧳 Let's create new travel!\n<i>Enter /cancel to cancel creating.</i>"
|
||||
INPUT_TRAVEL_TITLE = "Enter travel title:\n<i>Maximum length: 30 characters</i>"
|
||||
INPUT_TRAVEL_CALLBACK = (
|
||||
"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_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)."
|
||||
ACTION_CANCELED = "❌ Action canceled"
|
||||
|
||||
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."
|
||||
|
||||
HELP_MESSAGE = "Help message text."
|
||||
HELP_MESSAGE = (
|
||||
"Welcome to the ✈️ Travel Agent bot! Here is list of commands you can use:\n\n"
|
||||
"/start - Start the bot\n"
|
||||
"/help - Show this message\n"
|
||||
"/menu - Show the main menu\n"
|
||||
"/profile - View and edit your profile\n"
|
||||
"/create_travel - Create new travel\n"
|
||||
"/travels - View and edit your travels\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."
|
||||
)
|
||||
|
||||
REGISTERED_MESSAGE = "You have successfully registered. Welcome to the ✈️ Travel Agent bot! \nYou can view and edit your profile using the /profile command."
|
||||
|
||||
@@ -27,7 +51,7 @@ PROFILE = (
|
||||
"\tCountry: <b>{country}</b>\n"
|
||||
"\tCity: <b>{city}</b>\n"
|
||||
"\tBio: <b>{bio}</b>\n"
|
||||
"\tDate joined: <b>{date_joined}</b>\n"
|
||||
"\tDate joined: <b>{date_joined} UTC</b>\n"
|
||||
)
|
||||
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>"
|
||||
|
||||
@@ -8,7 +8,7 @@ from dotenv import load_dotenv
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from app.models.user import Base
|
||||
from app.models import Base
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Added travel models
|
||||
|
||||
Revision ID: fe4ace4196fb
|
||||
Revises: 4914f00ae14a
|
||||
Create Date: 2024-03-22 19:19:36.662090
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'fe4ace4196fb'
|
||||
down_revision: Union[str, None] = '4914f00ae14a'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
'travels',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('title', sa.String(length=50), nullable=False),
|
||||
sa.Column('description', sa.String(length=100), nullable=True),
|
||||
sa.Column('author_id', sa.BigInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
['author_id'],
|
||||
['users.telegram_id'],
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('title'),
|
||||
)
|
||||
op.create_index(op.f('ix_travels_id'), 'travels', ['id'], unique=True)
|
||||
op.create_table(
|
||||
'locations',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('date_start', sa.Date(), nullable=False),
|
||||
sa.Column('date_end', sa.Date(), nullable=False),
|
||||
sa.Column('travel_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
['travel_id'],
|
||||
['travels.id'],
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index(op.f('ix_locations_id'), 'locations', ['id'], unique=True)
|
||||
op.create_table(
|
||||
'notes',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('file_id', sa.Text(), nullable=False),
|
||||
sa.Column('file_name', sa.Text(), nullable=False),
|
||||
sa.Column('file_type', sa.Text(), nullable=False),
|
||||
sa.Column('public', sa.Boolean(), nullable=False),
|
||||
sa.Column('author_id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('travel_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
['author_id'],
|
||||
['users.telegram_id'],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['travel_id'],
|
||||
['travels.id'],
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index(op.f('ix_notes_id'), 'notes', ['id'], unique=True)
|
||||
op.create_table(
|
||||
'user_travel_association',
|
||||
sa.Column('user_id', sa.BigInteger(), nullable=True),
|
||||
sa.Column('travel_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
['travel_id'],
|
||||
['travels.id'],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['user_id'],
|
||||
['users.telegram_id'],
|
||||
),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('user_travel_association')
|
||||
op.drop_index(op.f('ix_notes_id'), table_name='notes')
|
||||
op.drop_table('notes')
|
||||
op.drop_index(op.f('ix_locations_id'), table_name='locations')
|
||||
op.drop_table('locations')
|
||||
op.drop_index(op.f('ix_travels_id'), table_name='travels')
|
||||
op.drop_table('travels')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,7 @@
|
||||
# flake8: noqa
|
||||
|
||||
from app.models.base import Base
|
||||
import app.models.user
|
||||
import app.models.travel
|
||||
|
||||
Base.registry.configure()
|
||||
@@ -0,0 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base: Any = declarative_base()
|
||||
@@ -0,0 +1,124 @@
|
||||
__all__ = ("Travel", "Location")
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import relationship, validates
|
||||
|
||||
from app import session
|
||||
from app.models import Base
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
association_table = sa.Table(
|
||||
"user_travel_association",
|
||||
Base.metadata,
|
||||
sa.Column("user_id", sa.BigInteger, sa.ForeignKey("users.telegram_id")),
|
||||
sa.Column("travel_id", sa.Integer, sa.ForeignKey("travels.id")),
|
||||
)
|
||||
|
||||
|
||||
class Travel(Base):
|
||||
__tablename__ = "travels"
|
||||
|
||||
id = sa.Column( # noqa: A003
|
||||
sa.Integer,
|
||||
unique=True,
|
||||
primary_key=True,
|
||||
nullable=False,
|
||||
autoincrement=True,
|
||||
index=True,
|
||||
)
|
||||
title = sa.Column(
|
||||
sa.String(50),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
)
|
||||
description = sa.Column(
|
||||
sa.String(100),
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
author_id = sa.Column(
|
||||
sa.BigInteger,
|
||||
sa.ForeignKey(User.telegram_id),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
users = relationship(
|
||||
User,
|
||||
secondary=association_table,
|
||||
backref="travels",
|
||||
)
|
||||
|
||||
locations = relationship("Location", backref="travel")
|
||||
notes = relationship("Note", backref="travel")
|
||||
|
||||
@validates("title")
|
||||
def validate_title(self, key, value):
|
||||
assert len(value) <= 30, "Title must be 30 characters or fewer."
|
||||
|
||||
if session.query(Travel).filter(Travel.title == value).first():
|
||||
raise AssertionError("This title is already taken.")
|
||||
|
||||
return value
|
||||
|
||||
@validates("description")
|
||||
def validate_description(self, key, value):
|
||||
if value is not None:
|
||||
assert (
|
||||
len(value) <= 100
|
||||
), "Description must be 100 characters or fewer."
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class Location(Base):
|
||||
__tablename__ = "locations"
|
||||
|
||||
id = sa.Column( # noqa: A003
|
||||
sa.Integer,
|
||||
unique=True,
|
||||
primary_key=True,
|
||||
autoincrement=True,
|
||||
index=True,
|
||||
)
|
||||
name = sa.Column(sa.Text, nullable=False)
|
||||
date_start = sa.Column(sa.Date(), nullable=False)
|
||||
date_end = sa.Column(sa.Date(), nullable=False)
|
||||
|
||||
travel_id = sa.Column(
|
||||
sa.Integer,
|
||||
sa.ForeignKey("travels.id"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
|
||||
class Note(Base):
|
||||
__tablename__ = "notes"
|
||||
|
||||
id = sa.Column( # noqa: A003
|
||||
sa.Integer,
|
||||
unique=True,
|
||||
primary_key=True,
|
||||
autoincrement=True,
|
||||
index=True,
|
||||
)
|
||||
|
||||
file_id = sa.Column(sa.Text, nullable=False)
|
||||
file_name = sa.Column(sa.Text, nullable=False)
|
||||
file_type = sa.Column(sa.Text, nullable=False)
|
||||
public: sa.Column[bool] = sa.Column(
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
)
|
||||
|
||||
author_id = sa.Column(
|
||||
sa.BigInteger,
|
||||
sa.ForeignKey(User.telegram_id),
|
||||
nullable=False,
|
||||
)
|
||||
travel_id = sa.Column(
|
||||
sa.Integer,
|
||||
sa.ForeignKey("travels.id"),
|
||||
nullable=False,
|
||||
)
|
||||
+8
-6
@@ -1,20 +1,16 @@
|
||||
__all__ = ("User",)
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import validates
|
||||
from sqlalchemy.orm import relationship, validates
|
||||
|
||||
from app import session
|
||||
from app.models import Base
|
||||
from app.utils import geo
|
||||
from app.utils.db import utcnow
|
||||
|
||||
|
||||
Base: Any = declarative_base()
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
@@ -38,6 +34,9 @@ class User(Base):
|
||||
server_default=utcnow(),
|
||||
)
|
||||
|
||||
notes = relationship("Note", backref="author")
|
||||
owned_travels = relationship("Travel", backref="author")
|
||||
|
||||
@validates("username")
|
||||
def validate_username(self, key, value):
|
||||
regex_pattern = re.compile(r"^[a-zA-Z0-9_]{5,20}$")
|
||||
@@ -98,6 +97,9 @@ class User(Base):
|
||||
|
||||
return normalized_value
|
||||
|
||||
def get_user_travels(self):
|
||||
return self.owned_travels + self.travels
|
||||
|
||||
def get_human_readable_datejoined(self):
|
||||
return self.date_joined.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
__all__ = ()
|
||||
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
|
||||
class TravelCreationState(StatesGroup):
|
||||
error_message_id = State()
|
||||
title = State()
|
||||
description = State()
|
||||
Reference in New Issue
Block a user