Category rendering, DB columns refined.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
"""Optional constraints fixed, price argument added for product
|
||||
|
||||
Revision ID: 8756e344268a
|
||||
Revises: af58496e0468
|
||||
Create Date: 2026-02-04 17:23:47.783119
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "8756e344268a"
|
||||
down_revision: Union[str, Sequence[str], None] = "af58496e0468"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column("categories", "name", existing_type=sa.TEXT(), nullable=False)
|
||||
op.add_column("products", sa.Column("price", sa.Integer(), nullable=False))
|
||||
op.alter_column("products", "name", existing_type=sa.TEXT(), nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column("products", "name", existing_type=sa.TEXT(), nullable=True)
|
||||
op.drop_column("products", "price")
|
||||
op.alter_column("categories", "name", existing_type=sa.TEXT(), nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
@@ -8,5 +8,6 @@ logger = logging.getLogger(__name__)
|
||||
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
|
||||
|
||||
POSTGRES_URL = os.getenv(
|
||||
"POSTGRES_URL", "postgresql+asyncpg://postgres:postgres@localhost:5432/postgres"
|
||||
"POSTGRES_URL",
|
||||
"postgresql+asyncpg://postgres:HEXDEVFUCKINGSUCKS!@localhost:5432/postgres",
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ class Category(Base):
|
||||
__tablename__ = "categories"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[Optional[str]] = mapped_column(Text)
|
||||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("categories.id"))
|
||||
|
||||
parent: Mapped["Category"] = relationship("Category", remote_side=[id])
|
||||
@@ -20,8 +20,9 @@ class Product(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
category_id: Mapped[Optional[int]] = mapped_column(ForeignKey("categories.id"))
|
||||
name: Mapped[Optional[str]] = mapped_column(Text)
|
||||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
price: Mapped[int] = mapped_column()
|
||||
|
||||
search_vector: Mapped[str] = mapped_column(TSVECTOR)
|
||||
|
||||
|
||||
0
dto/__init__.py
Normal file
0
dto/__init__.py
Normal file
7
dto/categories.py
Normal file
7
dto/categories.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CategoryDTO:
|
||||
id: int
|
||||
name: str
|
||||
@@ -1,10 +1,12 @@
|
||||
from aiogram import Router
|
||||
from aiogram.types import Message
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from misc.kb import main_menu_kb
|
||||
from misc.kb import main_menu_kb, render_category
|
||||
from repositories.categories import CategoriesRepository
|
||||
from services.categories import CategoryService
|
||||
from services.orders import OrderService
|
||||
|
||||
router = Router()
|
||||
@@ -19,6 +21,15 @@ async def main_menu(msg: Message, state: FSMContext):
|
||||
) # TODO: Write a welcome message
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("menu:"))
|
||||
async def main_menu_cb(cb: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
|
||||
await cb.message.edit_text(
|
||||
"hii!", reply_markup=await main_menu_kb(cb.from_user.id)
|
||||
) # TODO: Write a welcome message
|
||||
|
||||
|
||||
@router.message(Command("orders"))
|
||||
async def get_orders(
|
||||
msg: Message,
|
||||
@@ -30,3 +41,33 @@ async def get_orders(
|
||||
|
||||
count = await order_service.get_order_count(session, msg.from_user.id)
|
||||
await msg.answer(f"you have {count} orders.")
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("cat:"))
|
||||
async def subcatalogue(
|
||||
cb: CallbackQuery,
|
||||
state: FSMContext,
|
||||
category_service: CategoryService,
|
||||
session: AsyncSession,
|
||||
categories_repo: CategoriesRepository,
|
||||
):
|
||||
await state.clear()
|
||||
|
||||
cat_id = cb.data.split(":")[1]
|
||||
|
||||
if cat_id == "root":
|
||||
children = await category_service.get_children(session)
|
||||
kb = await render_category(children, show_menu=True)
|
||||
|
||||
else:
|
||||
cat_id = int(cat_id)
|
||||
|
||||
children = await category_service.get_children(session, cat_id)
|
||||
category = await categories_repo.get_category_by_id(session, cat_id)
|
||||
parent_id = category.parent_id or "root"
|
||||
kb = await render_category(children, parent_id)
|
||||
|
||||
await cb.message.edit_text(
|
||||
"category selection",
|
||||
reply_markup=kb,
|
||||
)
|
||||
|
||||
11
main.py
11
main.py
@@ -8,9 +8,11 @@ from handlers import init_handlers
|
||||
from middlewares.di import DIMiddleware
|
||||
from middlewares.repository import RepositoryMiddleware
|
||||
from middlewares.session import DBSessionMiddleware
|
||||
from repositories.categories import CategoriesRepository
|
||||
from repositories.orders import OrderRepository
|
||||
from services import OrderService
|
||||
from db.base import async_session
|
||||
from services.categories import CategoryService
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
@@ -26,11 +28,18 @@ async def main():
|
||||
order_repository = OrderRepository()
|
||||
order_service = OrderService(order_repository)
|
||||
|
||||
category_repository = CategoriesRepository()
|
||||
category_service = CategoryService(category_repository)
|
||||
|
||||
# DI Middleware Init
|
||||
dp.message.middleware(DIMiddleware(order_service))
|
||||
dp.message.middleware(DIMiddleware(order_service, category_service))
|
||||
dp.message.middleware(DBSessionMiddleware(async_session))
|
||||
dp.message.middleware(RepositoryMiddleware())
|
||||
|
||||
dp.callback_query.middleware(DIMiddleware(order_service, category_service))
|
||||
dp.callback_query.middleware(DBSessionMiddleware(async_session))
|
||||
dp.callback_query.middleware(RepositoryMiddleware())
|
||||
|
||||
init_handlers(dp)
|
||||
|
||||
await dp.start_polling(bot)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from aiogram import BaseMiddleware
|
||||
from typing import Any, Callable
|
||||
|
||||
from services.categories import CategoryService
|
||||
from services.orders import OrderService
|
||||
|
||||
|
||||
class DIMiddleware(BaseMiddleware):
|
||||
def __init__(self, order_service: OrderService):
|
||||
def __init__(self, order_service: OrderService, category_service: CategoryService):
|
||||
self._order_service = order_service
|
||||
self._category_service = category_service
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
@@ -15,5 +17,6 @@ class DIMiddleware(BaseMiddleware):
|
||||
data: dict[str, Any],
|
||||
) -> Any:
|
||||
data["order_service"] = self._order_service
|
||||
data["category_service"] = self._category_service
|
||||
|
||||
return await handler(event, data)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from aiogram import BaseMiddleware
|
||||
|
||||
from repositories.orders import OrderRepository
|
||||
from repositories import OrderRepository, CategoriesRepository
|
||||
|
||||
|
||||
class RepositoryMiddleware(BaseMiddleware):
|
||||
async def __call__(self, handler, event, data):
|
||||
data["order_repo"] = OrderRepository()
|
||||
data["categories_repo"] = CategoriesRepository()
|
||||
return await handler(event, data)
|
||||
|
||||
@@ -1,20 +1,58 @@
|
||||
from typing import Optional
|
||||
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from dto.categories import CategoryDTO
|
||||
|
||||
async def cart_btn(user_id: int) -> InlineKeyboardButton:
|
||||
|
||||
def cart_btn(user_id: int) -> InlineKeyboardButton:
|
||||
return InlineKeyboardButton(
|
||||
text="🛒 Корзина ()", callback_data="cart"
|
||||
) # TODO: Database integration, get cart items count
|
||||
|
||||
|
||||
def get_back_btn(
|
||||
data: str = "menu:main", text: Optional[str] = "⬅️ Назад"
|
||||
) -> InlineKeyboardButton:
|
||||
return InlineKeyboardButton(text=text, callback_data=data)
|
||||
|
||||
|
||||
async def main_menu_kb(user_id: int) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardBuilder(
|
||||
[
|
||||
[
|
||||
InlineKeyboardButton(text="📦 Каталог", callback_data="catalogue"),
|
||||
InlineKeyboardButton(text="📦 Каталог", callback_data="cat:root"),
|
||||
InlineKeyboardButton(text="🔍", callback_data="find"),
|
||||
],
|
||||
[await cart_btn(user_id)],
|
||||
[cart_btn(user_id)],
|
||||
]
|
||||
).as_markup()
|
||||
|
||||
|
||||
async def render_category(
|
||||
children: list[CategoryDTO],
|
||||
parent_id: Optional[int] = None,
|
||||
show_menu: bool = False,
|
||||
) -> InlineKeyboardMarkup:
|
||||
btns = [
|
||||
InlineKeyboardButton(
|
||||
text=category.name,
|
||||
callback_data=f"cat:{category.id}",
|
||||
)
|
||||
for category in children
|
||||
]
|
||||
|
||||
nav = []
|
||||
|
||||
if parent_id is not None:
|
||||
nav.append(
|
||||
InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}")
|
||||
)
|
||||
|
||||
if show_menu:
|
||||
nav.append(InlineKeyboardButton(text="⬅️ Назад", callback_data="menu:main"))
|
||||
|
||||
return InlineKeyboardBuilder(
|
||||
[btns[i : i + 2] for i in range(0, len(children), 2)] + [nav],
|
||||
).as_markup()
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
docker run --name test-postgres -e POSTGRES_PASSWORD=HEXDEVFUCKINGSUCKS! -p 5432:5432 -d postgres
|
||||
docker start test-postgres
|
||||
pg_isready -h localhost -p 5432
|
||||
@@ -0,0 +1,4 @@
|
||||
from .orders import OrderRepository
|
||||
from .categories import CategoriesRepository
|
||||
|
||||
__all__ = ["OrderRepository", "CategoriesRepository"]
|
||||
|
||||
21
repositories/categories.py
Normal file
21
repositories/categories.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
|
||||
from db.models import Category
|
||||
|
||||
|
||||
class CategoriesRepository:
|
||||
async def get_category_by_id(
|
||||
self, session: AsyncSession, category_id: int
|
||||
) -> Optional[Category]:
|
||||
stmt = select(Category).where(Category.id == category_id)
|
||||
return await session.scalar(stmt)
|
||||
|
||||
async def get_category_by_parent_id(
|
||||
self, session: AsyncSession, parent_id: Optional[int] = None
|
||||
) -> list[Category]:
|
||||
stmt = select(Category).where(Category.parent_id == parent_id)
|
||||
result = await session.scalars(stmt)
|
||||
|
||||
return list(result)
|
||||
@@ -1,8 +1,8 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Optional, Union, List
|
||||
from typing import Optional
|
||||
|
||||
from db.models import Order, OrderItem, OrderStatus
|
||||
from db.models import Order
|
||||
|
||||
|
||||
class OrderRepository:
|
||||
|
||||
18
services/categories.py
Normal file
18
services/categories.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from dto.categories import CategoryDTO
|
||||
from repositories import CategoriesRepository
|
||||
|
||||
|
||||
class CategoryService:
|
||||
def __init__(self, category_repo: CategoriesRepository):
|
||||
self.category_repo = category_repo
|
||||
|
||||
async def get_children(
|
||||
self, session: AsyncSession, parent_id: Optional[int] = None
|
||||
) -> list[CategoryDTO]:
|
||||
result = await self.category_repo.get_category_by_parent_id(session, parent_id)
|
||||
return [
|
||||
CategoryDTO(x.id, x.name) for x in result
|
||||
] # FIXME: Optimization may be required here - O(n)
|
||||
19
startup.sh
Normal file
19
startup.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "formatting..."
|
||||
black .
|
||||
|
||||
echo "checking psql..."
|
||||
if ! pg_isready -h localhost -p 5432; then
|
||||
echo "psql is down, try again" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "psql is up and is recieving connections."
|
||||
|
||||
echo "running migrations..."
|
||||
alembic upgrade head
|
||||
|
||||
echo "starting the bot..."
|
||||
exec python main.py
|
||||
Reference in New Issue
Block a user