Unification of pagination, admins control and more
This commit is contained in:
@@ -25,6 +25,8 @@ POSTGRES_URL = os.getenv(
|
|||||||
|
|
||||||
card_info = CardInfo(os.getenv("CARD_NUMBER", ""), os.getenv("CARD_HOLDER", ""))
|
card_info = CardInfo(os.getenv("CARD_NUMBER", ""), os.getenv("CARD_HOLDER", ""))
|
||||||
|
|
||||||
|
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/1")
|
||||||
|
|
||||||
### Constants ###
|
### Constants ###
|
||||||
|
|
||||||
PAGE_SIZE = 4
|
PAGE_SIZE = 4
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
from .cart import CartItemDTO
|
from .cart import CartItemDTO
|
||||||
|
from .control import EditProductContext
|
||||||
|
from .checkout import CheckoutContext
|
||||||
|
from .catalogue import CatalogueView, CatalogueType
|
||||||
|
|
||||||
__all__ = ["CartItemDTO"]
|
__all__ = [
|
||||||
|
"CartItemDTO",
|
||||||
|
"EditProductContext",
|
||||||
|
"CheckoutContext",
|
||||||
|
"CatalogueView",
|
||||||
|
"CatalogueType",
|
||||||
|
]
|
||||||
|
|||||||
24
dto/catalogue.py
Normal file
24
dto/catalogue.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from db.models import Category
|
||||||
|
from db.models.products import Product
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogueType(Enum):
|
||||||
|
CATEGORIES = "categories"
|
||||||
|
PRODUCTS = "products"
|
||||||
|
EMPTY = "empty"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CatalogueView:
|
||||||
|
view_type: Optional[CatalogueType] = None
|
||||||
|
category: Optional[Category] = None
|
||||||
|
children: Optional[list[Category]] = None
|
||||||
|
products: Optional[list[Product]] = None
|
||||||
|
page: int = 0
|
||||||
|
parent_id: Union[int, str, None] = None
|
||||||
|
has_next: bool = False
|
||||||
|
show_menu: bool = False
|
||||||
@@ -24,6 +24,14 @@ if ! pg_isready -h localhost -p 5432; then
|
|||||||
fi
|
fi
|
||||||
echo "psql is up and is recieving connections."
|
echo "psql is up and is recieving connections."
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "checking redis..."
|
||||||
|
if ! redis-cli ping | grep PONG > /dev/null; then
|
||||||
|
echo "redis is down, try again" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "redis is up and is recieving connections."
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "running migrations..."
|
echo "running migrations..."
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
|
|||||||
@@ -3,14 +3,11 @@ from aiogram.types import CallbackQuery
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import PAGE_SIZE
|
from config import VERIFIED_ACCOUNTS
|
||||||
from misc.kb import render_category
|
|
||||||
from misc.kb.client import (
|
from misc.kb.client import (
|
||||||
get_back_to_catalogue,
|
render_catalogue,
|
||||||
render_products,
|
|
||||||
)
|
)
|
||||||
from repositories.categories import CategoriesRepository
|
from services.catalogue import CatalogueService
|
||||||
from repositories.products import ProductRepository
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -20,80 +17,23 @@ async def subcatalogue(
|
|||||||
cb: CallbackQuery,
|
cb: CallbackQuery,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
categories_repo: CategoriesRepository,
|
catalogue_service: CatalogueService,
|
||||||
products_repo: ProductRepository,
|
|
||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
cat_id = cb.data.split(":")[1]
|
cat_id = cb.data.split(":")[1]
|
||||||
|
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
|
||||||
|
|
||||||
if cat_id == "root":
|
view = await catalogue_service.build_category_view(session, cat_id)
|
||||||
children = await categories_repo.get_categories_by_parent_id(session)
|
kb = render_catalogue(view, is_admin=is_admin)
|
||||||
kb = render_category(
|
|
||||||
children, show_menu=True, category_cb_factory=lambda c: f"cat:{c.id}"
|
if cb.message.photo:
|
||||||
)
|
await cb.message.delete()
|
||||||
await cb.message.edit_text(
|
await cb.answer(
|
||||||
"category selection",
|
"category selection",
|
||||||
reply_markup=kb,
|
reply_markup=kb,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
cat_id = int(cat_id)
|
|
||||||
|
|
||||||
children = await categories_repo.get_categories_by_parent_id(session, cat_id)
|
|
||||||
category = await categories_repo.get_category_by_id(session, cat_id)
|
|
||||||
parent_id = category.parent_id or "root"
|
|
||||||
|
|
||||||
if not children:
|
|
||||||
products = await products_repo.get_product_by_category(
|
|
||||||
session, cat_id, limit=PAGE_SIZE + 1, offset=0
|
|
||||||
)
|
|
||||||
products_len = len(products)
|
|
||||||
|
|
||||||
if not products:
|
|
||||||
await cb.message.edit_text(
|
|
||||||
"🍃 Здесь ничего нет...",
|
|
||||||
reply_markup=get_back_to_catalogue(parent_id),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if cb.message.photo:
|
|
||||||
await cb.message.delete()
|
|
||||||
await cb.message.answer(
|
|
||||||
"hya",
|
|
||||||
reply_markup=render_products(
|
|
||||||
products[:PAGE_SIZE],
|
|
||||||
product_cb_factory=lambda p: f"product:{p.id}",
|
|
||||||
back_cb=f"cat:{parent_id}",
|
|
||||||
next_cb=(
|
|
||||||
f"products:{cat_id}:{0 + 1}"
|
|
||||||
if products_len > PAGE_SIZE
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
columns=2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
await cb.message.edit_text(
|
|
||||||
"hya",
|
|
||||||
reply_markup=render_products(
|
|
||||||
products[:PAGE_SIZE],
|
|
||||||
product_cb_factory=lambda p: f"product:{p.id}",
|
|
||||||
back_cb=f"cat:{parent_id}",
|
|
||||||
prev_cb=f"products:{cat_id}:{0 - 1}" if products_len < 0 else None,
|
|
||||||
next_cb=(
|
|
||||||
f"products:{cat_id}:{0 + 1}" if products_len > PAGE_SIZE else None
|
|
||||||
),
|
|
||||||
columns=2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
kb = render_category(
|
|
||||||
children, parent_id, category_cb_factory=lambda c: f"cat:{c.id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
await cb.message.edit_text(
|
await cb.message.edit_text(
|
||||||
"category selection",
|
"category selection",
|
||||||
reply_markup=kb,
|
reply_markup=kb,
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ from aiogram.types import CallbackQuery, FSInputFile
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from config import PAGE_SIZE, VERIFIED_ACCOUNTS
|
from config import VERIFIED_ACCOUNTS
|
||||||
from misc.kb import admins
|
from misc.kb import admins
|
||||||
from misc.kb.client import (
|
from misc.kb.client import (
|
||||||
|
render_catalogue,
|
||||||
render_product_interactions,
|
render_product_interactions,
|
||||||
render_products,
|
|
||||||
)
|
)
|
||||||
from misc.texts import get_product_description
|
from misc.texts import get_product_description
|
||||||
from repositories.categories import CategoriesRepository
|
|
||||||
from repositories.products import ProductRepository
|
from repositories.products import ProductRepository
|
||||||
|
from services.catalogue import CatalogueService
|
||||||
from services.orders import OrderService
|
from services.orders import OrderService
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
@@ -29,35 +29,22 @@ async def products_pagination(
|
|||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
pagination_match: Match[str],
|
pagination_match: Match[str],
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
products_repo: ProductRepository,
|
catalogue_service: CatalogueService,
|
||||||
categories_repo: CategoriesRepository,
|
|
||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
pagination_data = pagination_match.groupdict()
|
pagination_data = pagination_match.groupdict()
|
||||||
category_id = int(pagination_data.get("category_id"))
|
category_id = int(pagination_data.get("category_id"))
|
||||||
category = await categories_repo.get_category_by_id(session, category_id)
|
|
||||||
page_id = int(pagination_data.get("page_id", 0))
|
page_id = int(pagination_data.get("page_id", 0))
|
||||||
|
|
||||||
products = await products_repo.get_product_by_category(
|
view = await catalogue_service.build_category_view(
|
||||||
session, category_id, limit=PAGE_SIZE + 1, offset=page_id * PAGE_SIZE
|
session, category_id, page=page_id
|
||||||
)
|
)
|
||||||
products_len = len(products)
|
is_admin = cb.from_user.id in VERIFIED_ACCOUNTS
|
||||||
|
|
||||||
await cb.message.edit_reply_markup(
|
kb = render_catalogue(view, is_admin=is_admin)
|
||||||
reply_markup=render_products(
|
|
||||||
products[:PAGE_SIZE],
|
await cb.message.edit_reply_markup(reply_markup=kb)
|
||||||
product_cb_factory=lambda p: f"product:{p.id}",
|
|
||||||
back_cb=f"cat:{category.parent_id}",
|
|
||||||
prev_cb=(f"products:{category_id}:{page_id - 1}" if page_id > 0 else None),
|
|
||||||
next_cb=(
|
|
||||||
f"products:{category_id}:{page_id + 1}"
|
|
||||||
if products_len > PAGE_SIZE
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
columns=2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("product:"))
|
@router.callback_query(F.data.startswith("product:"))
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from config import PAGE_SIZE
|
||||||
from misc.kb.client import render_products
|
from misc.kb.client import render_products
|
||||||
from misc.kb.common import return_menu
|
from misc.kb.common import return_menu
|
||||||
|
from misc.redis import RedisClient
|
||||||
from misc.states import SearchStorage
|
from misc.states import SearchStorage
|
||||||
from repositories.products import ProductRepository
|
from repositories.products import ProductRepository
|
||||||
|
|
||||||
@@ -28,31 +31,62 @@ async def searching(
|
|||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
products_repo: ProductRepository,
|
products_repo: ProductRepository,
|
||||||
|
redis_client: RedisClient,
|
||||||
):
|
):
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
orig_msg: Optional[Message] = data.get("msg")
|
await msg.delete()
|
||||||
|
orig_msg: Optional[Message] = data.get("msg") or await msg.answer("⏳")
|
||||||
|
await orig_msg.edit_text("⏳ Поиск...")
|
||||||
results = await products_repo.search(
|
results = await products_repo.search(
|
||||||
session,
|
session,
|
||||||
msg.text,
|
msg.text,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not orig_msg:
|
next_cb = None
|
||||||
await msg.answer(
|
if len(results) > PAGE_SIZE:
|
||||||
"here",
|
query_hash = hashlib.sha256(msg.text.encode()).hexdigest()[:16]
|
||||||
reply_markup=render_products(
|
await redis_client.set_search_query(query_hash, msg.text)
|
||||||
results,
|
next_cb = f"search:{query_hash}:1"
|
||||||
product_cb_factory=lambda p: f"product:{p.id}",
|
|
||||||
back_cb="menu:main",
|
|
||||||
next_cb="",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
await orig_msg.edit_text(
|
await orig_msg.edit_text(
|
||||||
"here",
|
"here",
|
||||||
reply_markup=render_products(
|
reply_markup=render_products(
|
||||||
results, product_cb_factory=lambda p: f"product:{p.id}", back_cb="menu:main"
|
results[:PAGE_SIZE],
|
||||||
|
product_cb_factory=lambda p: f"product:{p.id}",
|
||||||
|
back_cb="menu:main",
|
||||||
|
next_cb=next_cb,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("search:"))
|
||||||
|
async def search_pagination(
|
||||||
|
cb: CallbackQuery,
|
||||||
|
state: FSMContext,
|
||||||
|
session: AsyncSession,
|
||||||
|
products_repo: ProductRepository,
|
||||||
|
redis_client: RedisClient,
|
||||||
|
):
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
data = cb.data.split(":")
|
||||||
|
q_hash = data[1]
|
||||||
|
page = int(data[2])
|
||||||
|
|
||||||
|
query = await redis_client.get_search_query(q_hash)
|
||||||
|
if not query:
|
||||||
|
await cb.message.edit_text("🍃 Попробуйте ещё раз...")
|
||||||
|
|
||||||
|
results = await products_repo.search(session, query, offset=PAGE_SIZE * page)
|
||||||
|
|
||||||
|
await cb.message.edit_reply_markup(
|
||||||
|
reply_markup=render_products(
|
||||||
|
results[:PAGE_SIZE],
|
||||||
|
product_cb_factory=lambda p: f"product:{p.id}",
|
||||||
|
back_cb="menu:main",
|
||||||
|
next_cb=f"search:{q_hash}:{page + 1}" if len(results) > PAGE_SIZE else None,
|
||||||
|
prev_cb=f"search:{q_hash}:{page - 1}" if page > 0 else None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
13
main.py
13
main.py
@@ -3,13 +3,15 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
from config import BOT_TOKEN
|
from config import BOT_TOKEN, REDIS_URL
|
||||||
from handlers import admins_routers, client_routers
|
from handlers import admins_routers, client_routers
|
||||||
|
|
||||||
from middlewares.di import DIMiddleware
|
from middlewares.di import DIMiddleware
|
||||||
from middlewares.repository import RepositoryMiddleware
|
from middlewares.repository import RepositoryMiddleware
|
||||||
from middlewares.session import DBSessionMiddleware
|
from middlewares.session import DBSessionMiddleware
|
||||||
|
from misc.redis import RedisClient
|
||||||
from repositories.categories import CategoriesRepository
|
from repositories.categories import CategoriesRepository
|
||||||
from repositories.invoices import InvoiceRepository
|
from repositories.invoices import InvoiceRepository
|
||||||
from repositories.order_items import OrderItemRepository
|
from repositories.order_items import OrderItemRepository
|
||||||
@@ -17,6 +19,7 @@ from repositories.orders import OrderRepository
|
|||||||
from repositories.products import ProductRepository
|
from repositories.products import ProductRepository
|
||||||
from services import OrderService
|
from services import OrderService
|
||||||
from db.base import async_session
|
from db.base import async_session
|
||||||
|
from services.catalogue import CatalogueService
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
@@ -36,13 +39,17 @@ async def main():
|
|||||||
order_service = OrderService(order_repository, order_items_repository)
|
order_service = OrderService(order_repository, order_items_repository)
|
||||||
|
|
||||||
category_repository = CategoriesRepository()
|
category_repository = CategoriesRepository()
|
||||||
|
|
||||||
product_repository = ProductRepository()
|
product_repository = ProductRepository()
|
||||||
|
catalogue_service = CatalogueService(product_repository, category_repository)
|
||||||
|
|
||||||
invoice_repository = InvoiceRepository()
|
invoice_repository = InvoiceRepository()
|
||||||
|
|
||||||
|
# Additional Initiations
|
||||||
|
r = Redis.from_url(REDIS_URL)
|
||||||
|
redis_client = RedisClient(r)
|
||||||
|
|
||||||
# DI Middleware Init
|
# DI Middleware Init
|
||||||
dp.update.middleware(DIMiddleware(order_service))
|
dp.update.middleware(DIMiddleware(order_service, catalogue_service, redis_client))
|
||||||
dp.update.middleware(DBSessionMiddleware(async_session))
|
dp.update.middleware(DBSessionMiddleware(async_session))
|
||||||
dp.update.middleware(
|
dp.update.middleware(
|
||||||
RepositoryMiddleware(
|
RepositoryMiddleware(
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
from aiogram import BaseMiddleware
|
from aiogram import BaseMiddleware
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
from services.orders import OrderService
|
from services.orders import OrderService
|
||||||
|
from services.catalogue import CatalogueService
|
||||||
|
|
||||||
|
|
||||||
class DIMiddleware(BaseMiddleware):
|
class DIMiddleware(BaseMiddleware):
|
||||||
def __init__(self, order_service: OrderService):
|
def __init__(
|
||||||
|
self,
|
||||||
|
order_service: OrderService,
|
||||||
|
catalogue_service: CatalogueService,
|
||||||
|
redis_client: Redis,
|
||||||
|
):
|
||||||
self._order_service = order_service
|
self._order_service = order_service
|
||||||
|
self._redis_client = redis_client
|
||||||
|
self._catalogue_service = catalogue_service
|
||||||
|
|
||||||
async def __call__(
|
async def __call__(
|
||||||
self,
|
self,
|
||||||
@@ -15,5 +25,7 @@ class DIMiddleware(BaseMiddleware):
|
|||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
) -> Any:
|
) -> Any:
|
||||||
data["order_service"] = self._order_service
|
data["order_service"] = self._order_service
|
||||||
|
data["catalogue_service"] = self._catalogue_service
|
||||||
|
data["redis_client"] = self._redis_client
|
||||||
|
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ from typing import Optional, Callable
|
|||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
from config import PAGE_SIZE
|
||||||
from db.models import Category, Product
|
from db.models import Category, Product
|
||||||
from dto.cart import CartItemDTO
|
from dto.cart import CartItemDTO
|
||||||
|
from dto.catalogue import CatalogueView, CatalogueType
|
||||||
|
from .common import return_menu
|
||||||
|
|
||||||
|
|
||||||
def pagination_row(
|
def pagination_row(
|
||||||
@@ -59,6 +62,7 @@ def render_category(
|
|||||||
show_menu: bool = False,
|
show_menu: bool = False,
|
||||||
*,
|
*,
|
||||||
category_cb_factory: Callable[[Category], str],
|
category_cb_factory: Callable[[Category], str],
|
||||||
|
is_admin: bool = False,
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
btns = [
|
btns = [
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
@@ -68,18 +72,27 @@ def render_category(
|
|||||||
for category in children
|
for category in children
|
||||||
]
|
]
|
||||||
|
|
||||||
nav = []
|
nav: list[list[InlineKeyboardButton]] = []
|
||||||
|
|
||||||
if parent_id is not None:
|
if is_admin:
|
||||||
nav.append(
|
nav.append(
|
||||||
InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}")
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="📄 Добавить категорию",
|
||||||
|
callback_data=f"new:category:{parent_id}",
|
||||||
|
)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if show_menu:
|
if show_menu:
|
||||||
nav.append(InlineKeyboardButton(text="⬅️ Назад", callback_data="menu:main"))
|
nav.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="menu:main")])
|
||||||
|
elif parent_id is not None:
|
||||||
|
nav.append(
|
||||||
|
[InlineKeyboardButton(text="⬅️ Назад", callback_data=f"cat:{parent_id}")]
|
||||||
|
)
|
||||||
|
|
||||||
return InlineKeyboardBuilder(
|
return InlineKeyboardBuilder(
|
||||||
[btns[i : i + 2] for i in range(0, len(children), 2)] + [nav],
|
[btns[i : i + 2] for i in range(0, len(children), 2)] + nav,
|
||||||
).as_markup()
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
@@ -91,6 +104,8 @@ def render_products(
|
|||||||
prev_cb: Optional[str] = None,
|
prev_cb: Optional[str] = None,
|
||||||
next_cb: Optional[str] = None,
|
next_cb: Optional[str] = None,
|
||||||
columns: int = 2,
|
columns: int = 2,
|
||||||
|
is_admin: bool = False,
|
||||||
|
cat_id: Optional[int] = None,
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
@@ -107,6 +122,13 @@ def render_products(
|
|||||||
]
|
]
|
||||||
builder.row(*row)
|
builder.row(*row)
|
||||||
|
|
||||||
|
if is_admin:
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="📦 Добавить товар", callback_data=f"new:product:{cat_id}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
nav_buttons = []
|
nav_buttons = []
|
||||||
if prev_cb:
|
if prev_cb:
|
||||||
nav_buttons.append(InlineKeyboardButton(text="◀️", callback_data=prev_cb))
|
nav_buttons.append(InlineKeyboardButton(text="◀️", callback_data=prev_cb))
|
||||||
@@ -119,6 +141,32 @@ def render_products(
|
|||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def render_catalogue(view: CatalogueView, *, is_admin: bool):
|
||||||
|
if view.view_type == CatalogueType.CATEGORIES:
|
||||||
|
return render_category(
|
||||||
|
children=view.children[:PAGE_SIZE], # type: ignore
|
||||||
|
parent_id=view.parent_id,
|
||||||
|
show_menu=view.show_menu,
|
||||||
|
category_cb_factory=lambda c: f"cat:{c.id}",
|
||||||
|
is_admin=is_admin,
|
||||||
|
)
|
||||||
|
if view.view_type == CatalogueType.PRODUCTS:
|
||||||
|
return render_products(
|
||||||
|
products=view.products[:PAGE_SIZE], # type: ignore
|
||||||
|
product_cb_factory=lambda p: f"product:{p.id}",
|
||||||
|
back_cb=f"cat:{view.parent_id}",
|
||||||
|
prev_cb=(
|
||||||
|
f"products:{view.category}:{view.page - 1}" if view.page > 0 else None
|
||||||
|
),
|
||||||
|
next_cb=(
|
||||||
|
f"products:{view.category}:{view.page + 1}" if view.has_next else None
|
||||||
|
),
|
||||||
|
is_admin=is_admin,
|
||||||
|
)
|
||||||
|
|
||||||
|
return return_menu
|
||||||
|
|
||||||
|
|
||||||
def render_product_interactions(
|
def render_product_interactions(
|
||||||
*, product_id: int, category_id: int, show_cart: bool = False, cart_amount: int = 0
|
*, product_id: int, category_id: int, show_cart: bool = False, cart_amount: int = 0
|
||||||
) -> InlineKeyboardMarkup:
|
) -> InlineKeyboardMarkup:
|
||||||
|
|||||||
27
misc/redis.py
Normal file
27
misc/redis.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RedisClient:
|
||||||
|
def __init__(self, r: Redis):
|
||||||
|
self.r = r
|
||||||
|
|
||||||
|
async def set_search_query(self, query_hash: str, q: str, ttl: int = 3600) -> bool:
|
||||||
|
try:
|
||||||
|
await self.r.set(f"search:{query_hash}", q, ex=ttl)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_search_query(self, query_hash: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
res = await self.r.get(f"search:{query_hash}")
|
||||||
|
return res.decode()
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return None
|
||||||
@@ -7,3 +7,4 @@ sqlalchemy[asyncio]
|
|||||||
psycopg2
|
psycopg2
|
||||||
phonenumbers
|
phonenumbers
|
||||||
ruff
|
ruff
|
||||||
|
redis
|
||||||
54
services/catalogue.py
Normal file
54
services/catalogue.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from typing import Union, Optional
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from dto.catalogue import CatalogueType, CatalogueView
|
||||||
|
from repositories import ProductRepository, CategoriesRepository
|
||||||
|
from config import PAGE_SIZE
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogueService:
|
||||||
|
def __init__(
|
||||||
|
self, product_repo: ProductRepository, categories_repo: CategoriesRepository
|
||||||
|
) -> None:
|
||||||
|
self.product_repo = product_repo
|
||||||
|
self.categories_repo = categories_repo
|
||||||
|
|
||||||
|
async def build_category_view(
|
||||||
|
self, session: AsyncSession, cat_id: Optional[Union[int, str]], page: int = 0
|
||||||
|
) -> CatalogueView:
|
||||||
|
cat_id = (
|
||||||
|
int(cat_id)
|
||||||
|
if isinstance(cat_id, (int, str)) and str(cat_id).isdigit()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
view = CatalogueView(page=page)
|
||||||
|
children = await self.categories_repo.get_categories_by_parent_id(
|
||||||
|
session, cat_id
|
||||||
|
)
|
||||||
|
category = await self.categories_repo.get_category_by_id(session, cat_id)
|
||||||
|
|
||||||
|
if not category: # Root category
|
||||||
|
view.category = "root"
|
||||||
|
view.show_menu = True
|
||||||
|
else: # Subcategories
|
||||||
|
view.category = cat_id
|
||||||
|
view.parent_id = category.parent_id or "root"
|
||||||
|
|
||||||
|
if not children: # Products
|
||||||
|
view.view_type = CatalogueType.PRODUCTS
|
||||||
|
products = await self.product_repo.get_product_by_category(
|
||||||
|
session, cat_id, limit=PAGE_SIZE + 1, offset=view.page * PAGE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
if not products:
|
||||||
|
view.view_type = CatalogueType.EMPTY
|
||||||
|
|
||||||
|
view.products = products[:PAGE_SIZE]
|
||||||
|
view.has_next = len(products) > PAGE_SIZE
|
||||||
|
return view
|
||||||
|
|
||||||
|
view.view_type = CatalogueType.CATEGORIES
|
||||||
|
view.children = children[:PAGE_SIZE]
|
||||||
|
|
||||||
|
return view
|
||||||
Reference in New Issue
Block a user