refactoring

This commit is contained in:
Alexander Kalinovsky
2025-01-09 13:11:10 +01:00
parent 7793a0cb77
commit 3898a333fa
29 changed files with 1065 additions and 381 deletions

View File

@@ -1,24 +1,35 @@
from types import NoneType, UnionType
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from babel.support import LazyProxy
from inspect import signature
from sqlmodel.ext.asyncio.session import AsyncSession
from typing import Any, get_args, get_origin
from typing import Any, get_args, get_origin, TYPE_CHECKING
import ujson as json
from ..context import ContextData, CallbackCommand, CommandContext
from ...command_context_filter import CallbackCommandFilter
from ....model.user import UserBase
from ....model.settings import Settings
from ....main import QBotApp
from ....model.bot_entity import BotEntity
from ....model.bot_enum import BotEnum
from ....model.view_setting import ViewSetting
from ....utils import get_local_text, deserialize
from ....model.descriptors import (EntityFieldDescriptor,
EntityDescriptor,
EntityCaptionCallable,
EntityItemCaptionCallable,
EntityFieldCaptionCallable)
if TYPE_CHECKING:
from ....main import QBotApp
router = Router()
def get_send_message(message: Message | CallbackQuery):
if isinstance(message, Message):
@@ -27,52 +38,43 @@ def get_send_message(message: Message | CallbackQuery):
return message.message.edit_text
def get_local_text(text: str, lang: str):
try:
text_obj = json.loads(text) #@IgnoreException
return text_obj.get(lang, text_obj[list(text_obj.keys())[0]])
except:
return text
# def get_local_text(text: str, lang: str):
# try:
# text_obj = json.loads(text) #@IgnoreException
# return text_obj.get(lang, text_obj[list(text_obj.keys())[0]])
# except:
# return text
def get_value_repr(value: Any, field_descriptor: EntityFieldDescriptor, locale: str | None = None) -> str:
type_ = field_descriptor.type_
origin = get_origin(type_)
type_ = field_descriptor.type_base
if value is None:
return ""
if origin == UnionType:
args = get_args(type_)
if args[1] == NoneType:
type_ = args[0]
if isinstance(value, bool):
return "【✔︎】" if value else "【 】"
elif origin == list:
arg_type = None
args = get_args(type_)
if args:
arg_type = args[0]
if arg_type and issubclass(arg_type, BotEntity):
if locale and arg_type.bot_entity_descriptor.fields_descriptors["name"].localizable:
return "[" + ", ".join([get_local_text(value = item.name, locale = locale) for item in value]) + "]"
elif field_descriptor.is_list:
if issubclass(type_, BotEntity):
if locale and type_.bot_entity_descriptor.fields_descriptors["name"].localizable:
return "[" + ", ".join([get_local_text(text = item.name, locale = locale) for item in value]) + "]"
else:
return "[" + ", ".join([str(item.name) for item in value]) + "]"
elif arg_type and issubclass(arg_type, BotEnum):
elif issubclass(type_, BotEnum):
return "[" + ", ".join(item.localized(locale) for item in value) + "]"
elif arg_type == str:
elif type_ == str:
return "[" + ", ".join([f"\"{item}\"" for item in value]) + "]"
else:
return "[" + ", ".join([str(item) for item in value]) + "]"
elif issubclass(type_, BotEntity):
if type_.bot_entity_descriptor.fields_descriptors["name"].localizable:
return get_local_text(value = value.name, locale = locale)
return get_local_text(text = value.name, locale = locale)
return value.name
elif issubclass(type_, BotEnum):
return value.localized(locale)
elif isinstance(value, str):
if field_descriptor and field_descriptor.localizable:
return get_local_text(value, locale)
return get_local_text(text = value, locale = locale)
return value
elif isinstance(value, int):
return str(value)
@@ -92,7 +94,13 @@ def get_callable_str(callable_str: str | LazyProxy | EntityCaptionCallable | Ent
elif isinstance(callable_str, LazyProxy):
return callable_str.value
elif callable(callable_str):
return callable_str(*(descriptor, entity, value))
args = signature(callable_str).parameters
if len(args) == 1:
return callable_str(descriptor)
elif len(args) == 2:
return callable_str(descriptor, entity)
elif len(args) == 3:
return callable_str(descriptor, entity, value)
async def authorize_command(user: UserBase,
@@ -100,24 +108,24 @@ async def authorize_command(user: UserBase,
if (callback_data.command == CallbackCommand.MENU_ENTRY_PARAMETERS or
callback_data.context == CommandContext.SETTING_EDIT):
allowed_roles = (await Settings.get(Settings.SECURITY_SETTINGS_ROLES))
allowed_roles = (await Settings.get(Settings.SECURITY_PARAMETERS_ROLES))
return any(role in user.roles for role in allowed_roles)
return False
def get_entity_descriptor(app: QBotApp, callback_data: ContextData) -> EntityDescriptor | None:
def get_entity_descriptor(app: "QBotApp", callback_data: ContextData) -> EntityDescriptor | None:
if callback_data.entity_name:
return app.entity_metadata.entity_descriptors[callback_data.entity_name]
return None
def get_field_descriptor(app: QBotApp, callback_data: ContextData) -> EntityFieldDescriptor | None:
def get_field_descriptor(app: "QBotApp", callback_data: ContextData) -> EntityFieldDescriptor | None:
if callback_data.context == CommandContext.SETTING_EDIT:
return Settings.list_params()[callback_data.field_name]
elif callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT]:
elif callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT, CommandContext.ENTITY_FIELD_EDIT]:
entity_descriptor = get_entity_descriptor(app, callback_data)
if entity_descriptor:
return entity_descriptor.fields_descriptors.get(callback_data.field_name)
@@ -192,4 +200,144 @@ def add_pagination_controls(keyboard_builder: InlineKeyboardBuilder,
data = str(total_pages) if page != total_pages else "skip",
save_state = True).pack()))
keyboard_builder.row(*navigation_buttons)
keyboard_builder.row(*navigation_buttons)
def add_filter_controls(keyboard_builder: InlineKeyboardBuilder,
entity_descriptor: EntityDescriptor,
filter: str = None,
page: int = 1):
field_name_descriptor = entity_descriptor.fields_descriptors["name"]
if field_name_descriptor.caption:
caption = get_callable_str(field_name_descriptor.caption, field_name_descriptor)
else:
caption = field_name_descriptor.name
keyboard_builder.row(
InlineKeyboardButton(
text = f"🔎 {caption}{f": \"{filter}\"" if filter else ""}",
callback_data = ContextData(
command = CallbackCommand.VIEW_FILTER_EDIT,
entity_name = entity_descriptor.name,
data = str(page)).pack()))
@router.callback_query(ContextData.filter(F.command == CallbackCommand.VIEW_FILTER_EDIT))
async def view_filter_edit(query: CallbackQuery, **kwargs):
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
args = callback_data.data.split("&")
page = int(args[0])
cmd = None
if len(args) > 1:
cmd = args[1]
db_session: AsyncSession = kwargs["db_session"]
app: "QBotApp" = kwargs["app"]
user: UserBase = kwargs["user"]
entity_descriptor = get_entity_descriptor(app = app, callback_data = callback_data)
if cmd in ["cancel", "clear"]:
if cmd == "clear":
await ViewSetting.set_filter(session = db_session, user_id = user.id, entity_name = entity_descriptor.class_name, filter = None)
context_data_bak = state_data.pop("context_data_bak")
if context_data_bak:
state_data["context_data"] = context_data_bak
context_data = ContextData.unpack(context_data_bak)
field_descriptor = get_field_descriptor(app, context_data)
edit_prompt = state_data["edit_prompt"]
current_value = await deserialize(session = db_session,
type_ = field_descriptor.type_,
value = state_data["value"])
page = int(state_data.pop("page"))
kwargs.pop("callback_data")
return await render_entity_picker(field_descriptor = field_descriptor,
message = query,
callback_data = context_data,
current_value = current_value,
edit_prompt = edit_prompt,
page = page,
**kwargs)
else:
return await route_callback(message = query, back = False, **kwargs)
#await save_navigation_context(callback_data = callback_data, state = state)
old_context_data = state_data.get("context_data")
await state.update_data({"context_data": callback_data.pack(),
"context_data_bak": old_context_data,
"page": page})
send_message = get_send_message(query)
await send_message(text = await Settings.get(Settings.APP_STRINGS_VIEW_FILTER_EDIT_PROMPT),
reply_markup = InlineKeyboardBuilder().row(
InlineKeyboardButton(
text = await Settings.get(Settings.APP_STRINGS_CANCEL_BTN),
callback_data = ContextData(
command = CallbackCommand.VIEW_FILTER_EDIT,
entity_name = entity_descriptor.name,
data = f"{page}&cancel").pack()),
InlineKeyboardButton(
text = await Settings.get(Settings.APP_STRINGS_CLEAR_BTN),
callback_data = ContextData(
command = CallbackCommand.VIEW_FILTER_EDIT,
entity_name = entity_descriptor.name,
data = f"{page}&clear").pack())).as_markup())
@router.message(CallbackCommandFilter(command = CallbackCommand.VIEW_FILTER_EDIT))
async def view_filter_edit_input(message: Message, **kwargs):
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
callback_data = ContextData.unpack(state_data["context_data"])
db_session: AsyncSession = kwargs["db_session"]
user: UserBase = kwargs["user"]
app: "QBotApp" = kwargs["app"]
entity_descriptor = get_entity_descriptor(app = app, callback_data = callback_data)
filter = message.text
await ViewSetting.set_filter(session = db_session, user_id = user.id, entity_name = entity_descriptor.class_name, filter = filter)
#state_data.pop("context_data")
#return await route_callback(message = message, back = False, **kwargs)
context_data_bak = state_data.pop("context_data_bak")
if context_data_bak:
state_data["context_data"] = context_data_bak
context_data = ContextData.unpack(context_data_bak)
field_descriptor = get_field_descriptor(app, context_data)
edit_prompt = state_data["edit_prompt"]
current_value = await deserialize(session = db_session,
type_ = field_descriptor.type_,
value = state_data["value"])
page = int(state_data.pop("page"))
return await render_entity_picker(field_descriptor = field_descriptor,
message = message,
callback_data = context_data,
current_value = current_value,
edit_prompt = edit_prompt,
page = page,
**kwargs)
else:
return await route_callback(message = message, back = False, **kwargs)
from ..navigation import route_callback, save_navigation_context, clear_state, get_navigation_context
from ..editors.entity import render_entity_picker

View File

@@ -16,15 +16,18 @@ class CallbackCommand(StrEnum):
SET_LANGUAGE = "ls"
DATE_PICKER_MONTH = "dm"
DATE_PICKER_YEAR = "dy"
STRING_EDITOR_LOCALE = "sl"
#STRING_EDITOR_LOCALE = "sl"
ENTITY_PICKER_PAGE = "ep"
ENTITY_PICKER_TOGGLE_ITEM = "et"
VIEW_FILTER_EDIT = "vf"
USER_COMMAND = "uc"
class CommandContext(StrEnum):
SETTING_EDIT = "se"
ENTITY_CREATE = "ec"
ENTITY_EDIT = "ee"
ENTITY_FIELD_EDIT = "ef"
class ContextData(BaseCallbackData, prefix = "cd"):
command: CallbackCommand
@@ -32,5 +35,6 @@ class ContextData(BaseCallbackData, prefix = "cd"):
entity_name: str | None = None
entity_id: int | None = None
field_name: str | None = None
user_command: str | None = None
data: str | None = None
back: bool = False

View File

@@ -1,7 +1,7 @@
from datetime import datetime
from decimal import Decimal
from types import NoneType, UnionType
from typing import get_args, get_origin
from typing import Union, get_args, get_origin, TYPE_CHECKING
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
@@ -9,7 +9,6 @@ from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
import ujson as json
from ....main import QBotApp
from ....model import EntityPermission
from ....model.bot_entity import BotEntity
from ....model.owned_bot_entity import OwnedBotEntity
@@ -21,14 +20,16 @@ from ....model.descriptors import EntityFieldDescriptor
from ....utils import deserialize, get_user_permissions, serialize
from ...command_context_filter import CallbackCommandFilter
from ..context import ContextData, CallbackCommand, CommandContext
from ..common import (get_value_repr, authorize_command, get_callable_str,
get_entity_descriptor, get_field_descriptor)
from ..menu.parameters import parameters_menu
from .string import string_editor, router as string_editor_router
from .date import date_picker, router as date_picker_router
from .boolean import bool_editor, router as bool_editor_router
from .entity import entity_picker, router as entity_picker_router
if TYPE_CHECKING:
from ....main import QBotApp
logger = getLogger(__name__)
router = Router()
@@ -42,21 +43,22 @@ router.include_routers(
@router.callback_query(ContextData.filter(F.command == CallbackCommand.FIELD_EDITOR))
async def settings_field_editor(message: Message | CallbackQuery, **kwargs):
async def field_editor(message: Message | CallbackQuery, **kwargs):
callback_data: ContextData = kwargs.get("callback_data", None)
db_session: AsyncSession = kwargs["db_session"]
user: UserBase = kwargs["user"]
app: QBotApp = kwargs["app"]
app: "QBotApp" = kwargs["app"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
entity_data = state_data.get("entity_data")
for key in ["current_value", "value", "locale_index"]:
if key in state_data:
state_data.pop(key)
await state.clear()
await state.update_data(state_data)
kwargs["state_data"] = state_data
entity_descriptor = None
@@ -69,41 +71,69 @@ async def settings_field_editor(message: Message | CallbackQuery, **kwargs):
else:
return await message.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
stack, context = await get_navigation_context(state = state)
stack, context = get_navigation_context(state_data = state_data)
return await parameters_menu(message = message,
navigation_stack = stack,
**kwargs)
current_value = await Settings.get(field_descriptor)
current_value = await Settings.get(field_descriptor, all_locales = True)
else:
entity_descriptor = get_entity_descriptor(app, callback_data)
field_descriptor = get_field_descriptor(app, callback_data)
entity_descriptor = field_descriptor.entity_descriptor
current_value = None
user_permissions = get_user_permissions(user, entity_descriptor)
if not entity_data and callback_data.context == CommandContext.ENTITY_EDIT:
if (EntityPermission.READ_ALL in get_user_permissions(user, entity_descriptor) or
(EntityPermission.READ in get_user_permissions(user, entity_descriptor) and
not issubclass(entity_descriptor.type_, OwnedBotEntity)) or
(EntityPermission.READ in get_user_permissions(user, entity_descriptor) and
issubclass(entity_descriptor.type_, OwnedBotEntity) and
entity_data.user_id == user.id)):
if field_descriptor.type_base == bool and callback_data.context == CommandContext.ENTITY_FIELD_EDIT:
entity = await entity_descriptor.type_.get(session = db_session, id = int(callback_data.entity_id))
if (EntityPermission.UPDATE_ALL in user_permissions or
(EntityPermission.UPDATE in user_permissions and
not isinstance(entity, OwnedBotEntity)) or
(EntityPermission.UPDATE in user_permissions and
isinstance(entity, OwnedBotEntity) and
entity.user_id == user.id)):
current_value: bool = getattr(entity, field_descriptor.field_name) or False
setattr(entity, field_descriptor.field_name, not current_value)
await db_session.commit()
stack, context = get_navigation_context(state_data = state_data)
return await entity_item(query = message, navigation_stack = stack, **kwargs)
if not entity_data and callback_data.context in [CommandContext.ENTITY_EDIT, CommandContext.ENTITY_FIELD_EDIT]:
entity = await entity_descriptor.type_.get(session = kwargs["db_session"], id = int(callback_data.entity_id))
if (EntityPermission.READ_ALL in user_permissions or
(EntityPermission.READ in user_permissions and
not isinstance(entity, OwnedBotEntity)) or
(EntityPermission.READ in user_permissions and
isinstance(entity, OwnedBotEntity) and
entity.user_id == user.id)):
entity = await entity_descriptor.type_.get(session = kwargs["db_session"], id = int(callback_data.entity_id))
if entity:
entity_data = {key: serialize(getattr(entity, key), entity_descriptor.fields_descriptors[key]) for key in entity_descriptor.field_sequence}
await state.update_data({"entity_data": entity_data})
entity_data = {key: serialize(getattr(entity, key), entity_descriptor.fields_descriptors[key])
for key in (entity_descriptor.field_sequence if callback_data.context == CommandContext.ENTITY_EDIT
else [callback_data.field_name])}
state_data.update({"entity_data": entity_data})
if entity_data:
current_value = await deserialize(session = db_session,
type_= field_descriptor.type_,
value = entity_data.get(callback_data.field_name))
kwargs.update({"field_descriptor": field_descriptor})
save_navigation_context(state_data = state_data, callback_data = callback_data)
await show_editor(message = message,
field_descriptor = field_descriptor,
entity_descriptor = entity_descriptor,
current_value = current_value,
**kwargs)
@@ -116,14 +146,15 @@ async def show_editor(message: Message | CallbackQuery,
user: UserBase = kwargs["user"]
callback_data: ContextData = kwargs.get("callback_data", None)
state: FSMContext = kwargs["state"]
state_data: dict = kwargs["state_data"]
value_type = field_descriptor.type_
value_type = field_descriptor.type_base
if field_descriptor.edit_prompt:
edit_prompt = get_callable_str(field_descriptor.edit_prompt, field_descriptor, None, current_value)
else:
if field_descriptor.caption_str:
caption_str = get_callable_str(field_descriptor.caption_str, field_descriptor, None, current_value)
if field_descriptor.caption:
caption_str = get_callable_str(field_descriptor.caption, field_descriptor, None, current_value)
else:
caption_str = field_descriptor.name
if callback_data.context == CommandContext.ENTITY_EDIT:
@@ -135,15 +166,15 @@ async def show_editor(message: Message | CallbackQuery,
kwargs["edit_prompt"] = edit_prompt
type_origin = get_origin(value_type)
# type_origin = get_origin(value_type)
if type_origin == UnionType:
args = get_args(value_type)
if args[1] == NoneType:
value_type = args[0]
# if type_origin in [UnionType, Union]:
# args = get_args(value_type)
# if args[1] == NoneType:
# value_type = args[0]
if value_type not in [int, float, Decimal, str]:
await state.update_data({"context_data": callback_data.pack()})
state_data.update({"context_data": callback_data.pack()})
if value_type == str:
await string_editor(message = message, **kwargs)
@@ -157,12 +188,12 @@ async def show_editor(message: Message | CallbackQuery,
elif value_type == datetime:
await date_picker(message = message, **kwargs)
elif type_origin == list:
type_args = get_args(value_type)
if type_args and issubclass(type_args[0], BotEntity) or issubclass(type_args[0], BotEnum):
await entity_picker(message = message, **kwargs)
else:
await string_editor(message = message, **kwargs)
# elif type_origin == list:
# type_args = get_args(value_type)
# if type_args and issubclass(type_args[0], BotEntity) or issubclass(type_args[0], BotEnum):
# await entity_picker(message = message, **kwargs)
# else:
# await string_editor(message = message, **kwargs)
elif issubclass(value_type, BotEntity) or issubclass(value_type, BotEnum):
await entity_picker(message = message, **kwargs)
@@ -175,26 +206,22 @@ async def show_editor(message: Message | CallbackQuery,
@router.callback_query(ContextData.filter(F.command == CallbackCommand.FIELD_EDITOR_CALLBACK))
async def field_editor_callback(message: Message | CallbackQuery, **kwargs):
callback_data: ContextData = kwargs.get("callback_data", None)
app: QBotApp = kwargs["app"]
app: "QBotApp" = kwargs["app"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
if isinstance(message, Message):
callback_data: ContextData = kwargs.get("callback_data", None)
context_data = state_data.get("context_data")
if context_data:
context_data = ContextData.unpack(context_data)
callback_data = context_data
callback_data = ContextData.unpack(context_data)
value = message.text
field_descriptor = get_field_descriptor(app, callback_data)
base_type = field_descriptor.type_
if get_origin(base_type) == UnionType:
args = get_args(base_type)
if args[1] == NoneType:
base_type = args[0]
type_base = field_descriptor.type_base
if base_type == str and field_descriptor.localizable:
if type_base == str and field_descriptor.localizable:
locale_index = int(state_data.get("locale_index"))
if locale_index < len(LanguageBase.all_members.values()) - 1:
@@ -209,9 +236,9 @@ async def field_editor_callback(message: Message | CallbackQuery, **kwargs):
value = {}
value[list(LanguageBase.all_members.values())[locale_index]] = message.text
value = json.dumps(value)
value = json.dumps(value, ensure_ascii = False)
await state.update_data({"value": value})
state_data.update({"value": value})
entity_descriptor = get_entity_descriptor(app, callback_data)
@@ -231,16 +258,20 @@ async def field_editor_callback(message: Message | CallbackQuery, **kwargs):
else:
value = {}
value[list(LanguageBase.all_members.values())[locale_index]] = message.text
value = json.dumps(value)
value = json.dumps(value, ensure_ascii = False)
elif (base_type in [int, float, Decimal]):
elif (type_base in [int, float, Decimal]):
try:
_ = base_type(value) #@IgnoreException
_ = type_base(value) #@IgnoreException
except:
return await message.answer(text = (await Settings.get(Settings.APP_STRINGS_INVALID_INPUT)))
else:
callback_data: ContextData = kwargs["callback_data"]
if callback_data.data:
value = callback_data.data
if callback_data.data == "skip":
value = None
else:
value = callback_data.data
else:
value = state_data.get("value")
field_descriptor = get_field_descriptor(app, callback_data)
@@ -258,13 +289,14 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
user: UserBase = kwargs["user"]
db_session: AsyncSession = kwargs["db_session"]
callback_data: ContextData = kwargs.get("callback_data", None)
state: FSMContext = kwargs["state"]
# state: FSMContext = kwargs["state"]
state_data: dict = kwargs["state_data"]
value = kwargs["value"]
field_descriptor: EntityFieldDescriptor = kwargs["field_descriptor"]
if callback_data.context == CommandContext.SETTING_EDIT:
await clear_state(state = state)
# clear_state(state_data = state_data)
if callback_data.data != "cancel":
if await authorize_command(user = user, callback_data = callback_data):
@@ -273,28 +305,31 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
else:
return await message.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
stack, context = await get_navigation_context(state = state)
# stack, context = get_navigation_context(state_data = state_data)
return await parameters_menu(message = message,
navigation_stack = stack,
**kwargs)
return await route_callback(message = message, back = True, **kwargs)
# return await parameters_menu(message = message,
# navigation_stack = stack,
# **kwargs)
elif callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT]:
elif callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT, CommandContext.ENTITY_FIELD_EDIT]:
app: QBotApp = kwargs["app"]
app: "QBotApp" = kwargs["app"]
entity_descriptor = get_entity_descriptor(app, callback_data)
field_sequence = entity_descriptor.field_sequence
current_index = field_sequence.index(callback_data.field_name)
current_index = (field_sequence.index(callback_data.field_name)
if callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT] else 0)
state_data = await state.get_data()
entity_data = state_data.get("entity_data", {})
if current_index < len(field_sequence) - 1:
if (callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT] and
current_index < len(field_sequence) - 1):
entity_data[field_descriptor.field_name] = value
await state.update_data({"entity_data": entity_data})
state_data.update({"entity_data": entity_data})
next_field_name = field_sequence[current_index + 1]
next_field_descriptor = entity_descriptor.fields_descriptors[next_field_name]
@@ -319,7 +354,7 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
if ((callback_data.context == CommandContext.ENTITY_CREATE and
EntityPermission.CREATE not in user_permissions and
EntityPermission.CREATE_ALL not in user_permissions) or
(callback_data.context == CommandContext.ENTITY_EDIT and
(callback_data.context in [CommandContext.ENTITY_EDIT, CommandContext.ENTITY_FIELD_EDIT] and
EntityPermission.UPDATE not in user_permissions and
EntityPermission.UPDATE_ALL not in user_permissions)):
return await message.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
@@ -341,13 +376,20 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
obj_in = entity_type(**deser_entity_data),
commit = True)
await save_navigation_context(state = state, callback_data = ContextData(
state_data["navigation_context"] = ContextData(
command = CallbackCommand.ENTITY_ITEM,
entity_name = entity_descriptor.name,
entity_id = str(new_entity.id)
))
entity_id = str(new_entity.id)).pack()
state_data.update(state_data)
elif callback_data.context == CommandContext.ENTITY_EDIT:
# await save_navigation_context(state = state, callback_data = ContextData(
# command = CallbackCommand.ENTITY_ITEM,
# entity_name = entity_descriptor.name,
# entity_id = str(new_entity.id)
# ))
elif callback_data.context in [CommandContext.ENTITY_EDIT, CommandContext.ENTITY_FIELD_EDIT]:
entity_id = int(callback_data.entity_id)
entity = await entity_type.get(session = db_session, id = entity_id)
@@ -363,9 +405,12 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
await db_session.commit()
await clear_state(state = state)
clear_state(state_data = state_data)
await route_callback(message = message, back = False, **kwargs)
await route_callback(message = message, back = True, **kwargs)
from ..navigation import get_navigation_context, route_callback, clear_state, save_navigation_context
from ..common import (get_value_repr, authorize_command, get_callable_str,
get_entity_descriptor, get_field_descriptor)
from ..navigation import get_navigation_context, route_callback, clear_state, save_navigation_context, pop_navigation_context
from ..forms.entity_form import entity_item

View File

@@ -17,22 +17,21 @@ router = Router()
async def bool_editor(message: Message | CallbackQuery,
edit_prompt: str,
entity_descriptor: EntityDescriptor,
field_descriptor: EntityFieldDescriptor,
callback_data: ContextData,
**kwargs):
keyboard_builder = InlineKeyboardBuilder()
if isinstance(field_descriptor.bool_true_value_btn, LazyProxy):
true_caption = field_descriptor.bool_true_value_btn.value
if isinstance(field_descriptor.bool_true_value, LazyProxy):
true_caption = field_descriptor.bool_true_value.value
else:
true_caption = field_descriptor.bool_true_value_btn
true_caption = field_descriptor.bool_true_value
if isinstance(field_descriptor.bool_false_value_btn, LazyProxy):
false_caption = field_descriptor.bool_false_value_btn.value
if isinstance(field_descriptor.bool_false_value, LazyProxy):
false_caption = field_descriptor.bool_false_value.value
else:
false_caption = field_descriptor.bool_false_value_btn
false_caption = field_descriptor.bool_false_value
keyboard_builder.row(
InlineKeyboardButton(text = true_caption,
@@ -55,11 +54,15 @@ async def bool_editor(message: Message | CallbackQuery,
save_state = True).pack())
)
state_data = kwargs["state_data"]
await wrap_editor(keyboard_builder = keyboard_builder,
field_descriptor = field_descriptor,
entity_descriptor = entity_descriptor,
callback_data = callback_data,
state = kwargs["state"])
state_data = state_data)
state: FSMContext = kwargs["state"]
await state.set_data(state_data)
send_message = get_send_message(message)

View File

@@ -7,21 +7,24 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ....model.settings import Settings
from ..context import ContextData, CallbackCommand, CommandContext
from ..navigation import get_navigation_context
from ..navigation import get_navigation_context, pop_navigation_context
async def wrap_editor(keyboard_builder: InlineKeyboardBuilder,
field_descriptor: EntityFieldDescriptor,
entity_descriptor: EntityDescriptor,
callback_data: ContextData,
state: FSMContext):
state_data: dict):
if callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT]:
if callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT, CommandContext.ENTITY_FIELD_EDIT]:
btns = []
field_index = entity_descriptor.field_sequence.index(field_descriptor.name)
entity_descriptor = field_descriptor.entity_descriptor
field_index = (entity_descriptor.field_sequence.index(field_descriptor.name)
if callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT]
else 0)
stack, context = await get_navigation_context(state)
stack, context = get_navigation_context(state_data = state_data)
context = pop_navigation_context(stack)
if field_index > 0:
btns.append(InlineKeyboardButton(
@@ -31,21 +34,18 @@ async def wrap_editor(keyboard_builder: InlineKeyboardBuilder,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = entity_descriptor.field_sequence[field_index - 1],
save_state = True).pack()))
field_name = entity_descriptor.field_sequence[field_index - 1]).pack()))
if get_origin(field_descriptor.type_) == UnionType:
args = get_args(field_descriptor.type_)
if args[1] == NoneType:
btns.append(InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_SKIP_BTN)),
callback_data = ContextData(
command = CallbackCommand.FIELD_EDITOR_CALLBACK,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
save_state = True).pack()))
if field_descriptor.is_optional:
btns.append(InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_SKIP_BTN)),
callback_data = ContextData(
command = CallbackCommand.FIELD_EDITOR_CALLBACK,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = "skip").pack()))
keyboard_builder.row(*btns)

View File

@@ -4,13 +4,16 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from typing import TYPE_CHECKING
from ....main import QBotApp
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ..context import ContextData, CallbackCommand
from ..common import get_send_message, get_field_descriptor, get_entity_descriptor
from .common import wrap_editor
if TYPE_CHECKING:
from ....main import QBotApp
logger = getLogger(__name__)
router = Router()
@@ -18,7 +21,6 @@ router = Router()
async def date_picker(message: Message | CallbackQuery,
field_descriptor: EntityFieldDescriptor,
entity_descriptor: EntityDescriptor,
callback_data: ContextData,
current_value: datetime,
state: FSMContext,
@@ -82,11 +84,14 @@ async def date_picker(message: Message | CallbackQuery,
keyboard_builder.row(*buttons)
state_data = kwargs["state_data"]
await wrap_editor(keyboard_builder = keyboard_builder,
field_descriptor = field_descriptor,
entity_descriptor = entity_descriptor,
callback_data = callback_data,
state = state)
state_data = state_data)
await state.set_data(state_data)
if edit_prompt:
send_message = get_send_message(message)
@@ -98,7 +103,7 @@ async def date_picker(message: Message | CallbackQuery,
@router.callback_query(ContextData.filter(F.command == CallbackCommand.DATE_PICKER_YEAR))
async def date_picker_year(query: CallbackQuery,
callback_data: ContextData,
app: QBotApp,
app: "QBotApp",
state: FSMContext,
**kwargs):
@@ -142,11 +147,9 @@ async def date_picker_year(query: CallbackQuery,
save_state = True).pack()))
field_descriptor = get_field_descriptor(app, callback_data)
entity_descriptor = get_entity_descriptor(app, callback_data)
await wrap_editor(keyboard_builder = keyboard_builder,
field_descriptor = field_descriptor,
entity_descriptor = entity_descriptor,
callback_data = callback_data,
state = state)
@@ -154,14 +157,12 @@ async def date_picker_year(query: CallbackQuery,
@router.callback_query(ContextData.filter(F.command == CallbackCommand.DATE_PICKER_MONTH))
async def date_picker_month(query: CallbackQuery, callback_data: ContextData, app: QBotApp, **kwargs):
entity_descriptor = get_entity_descriptor(app, callback_data)
async def date_picker_month(query: CallbackQuery, callback_data: ContextData, app: "QBotApp", **kwargs):
field_descriptor = get_field_descriptor(app, callback_data)
await date_picker(query.message,
field_descriptor = field_descriptor,
entity_descriptor = entity_descriptor,
callback_data = callback_data,
current_value = datetime.strptime(callback_data.data, "%Y-%m-%d"),
**kwargs)

View File

@@ -5,20 +5,24 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from typing import get_args, get_origin
from typing import get_args, get_origin, TYPE_CHECKING
from ....main import QBotApp
from ....model.bot_entity import BotEntity
from ....model.owned_bot_entity import OwnedBotEntity
from ....model.bot_enum import BotEnum
from ....model.settings import Settings
from ....model.user import UserBase
from ....model.view_setting import ViewSetting
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ....utils import serialize, deserialize
from ....model import EntityPermission
from ....utils import serialize, deserialize, get_user_permissions
from ..context import ContextData, CallbackCommand
from ..common import (get_send_message, get_local_text, get_field_descriptor,
get_entity_descriptor, add_pagination_controls)
get_entity_descriptor, add_pagination_controls, add_filter_controls)
from .common import wrap_editor
if TYPE_CHECKING:
from ....main import QBotApp
logger = getLogger(__name__)
router = Router()
@@ -28,24 +32,28 @@ async def entity_picker(message: Message | CallbackQuery,
field_descriptor: EntityFieldDescriptor,
edit_prompt: str,
current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
state: FSMContext,
**kwargs):
await state.update_data({"current_value": serialize(current_value, field_descriptor),
state_data: dict = kwargs["state_data"]
state_data.update({"current_value": serialize(current_value, field_descriptor),
"value": serialize(current_value, field_descriptor),
"edit_prompt": edit_prompt})
await render_entity_picker(field_descriptor = field_descriptor,
message = message,
state = state,
current_value = current_value,
edit_prompt = edit_prompt,
**kwargs)
def calc_total_pages(items_count: int, page_size: int) -> int:
return max(items_count // page_size + (1 if items_count % page_size else 0), 1)
async def render_entity_picker(*,
field_descriptor: EntityFieldDescriptor,
entity_descriptor: EntityDescriptor,
message: Message | CallbackQuery,
callback_data: ContextData,
user: UserBase,
@@ -60,18 +68,21 @@ async def render_entity_picker(*,
if callback_data.command in [CallbackCommand.ENTITY_PICKER_PAGE, CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM]:
page = int(callback_data.data.split("&")[0])
is_list = False
# is_list = False
type_origin = get_origin(field_descriptor.type_)
if type_origin == UnionType:
type_ = get_args(field_descriptor.type_)[0]
# type_origin = get_origin(field_descriptor.type_)
# if type_origin == UnionType:
# type_ = get_args(field_descriptor.type_)[0]
elif type_origin == list:
type_ = get_args(field_descriptor.type_)[0]
is_list = True
# elif type_origin == list:
# type_ = get_args(field_descriptor.type_)[0]
# is_list = True
else:
type_ = field_descriptor.type_
# else:
# type_ = field_descriptor.type_
type_ = field_descriptor.type_base
is_list = field_descriptor.is_list
if not issubclass(type_, BotEntity) and not issubclass(type_, BotEnum):
raise ValueError("Unsupported type")
@@ -80,18 +91,43 @@ async def render_entity_picker(*,
if issubclass(type_, BotEnum):
items_count = len(type_.all_members)
total_pages = calc_total_pages(items_count, page_size)
page = min(page, total_pages)
enum_items = list(type_.all_members.values())[page_size * (page - 1):page_size * page]
items = [{"text": f"{"" if not is_list else "【✔︎】 " if item in (current_value or []) else "【 】 "}{item.localized(user.lang)}",
"value": item.value} for item in enum_items]
else:
items_count = await type_.get_count(session = db_session)
entity_items = await type_.get_multi(session = db_session, order_by = type_.name, skip = page_size * (page - 1), limit = page_size)
permissions = get_user_permissions(user, type_.bot_entity_descriptor)
entity_filter = await ViewSetting.get_filter(session = db_session, user_id = user.id, entity_name = type_.bot_entity_descriptor.class_name)
if (EntityPermission.LIST_ALL in permissions or
(EntityPermission.LIST in permissions and
not issubclass(type_, OwnedBotEntity))):
items_count = await type_.get_count(session = db_session, filter = entity_filter)
total_pages = calc_total_pages(items_count, page_size)
page = min(page, total_pages)
entity_items = await type_.get_multi(
session = db_session, order_by = type_.name, filter = entity_filter,
skip = page_size * (page - 1), limit = page_size)
elif (EntityPermission.LIST in permissions and
issubclass(type_, OwnedBotEntity)):
items_count = await type_.get_count_by_user(session = db_session, user_id = user.id, filter = entity_filter)
total_pages = calc_total_pages(items_count, page_size)
page = min(page, total_pages)
entity_items = await type_.get_multi_by_user(
session = db_session, user_id = user.id, order_by = type_.name, filter = entity_filter,
skip = page_size * (page - 1), limit = page_size)
else:
items_count = 0
total_pages = 1
page = 1
entity_items = list[BotEntity]()
items = [{"text": f"{"" if not is_list else "【✔︎】 " if item in (current_value or []) else "【 】 "}{
type_.bot_entity_descriptor.item_caption_btn(type_.bot_entity_descriptor, item) if type_.bot_entity_descriptor.item_caption_btn
else get_local_text(item.name, user.lang) if field_descriptor.localizable else item.name}",
type_.bot_entity_descriptor.item_caption(type_.bot_entity_descriptor, item) if type_.bot_entity_descriptor.item_caption
else get_local_text(item.name, user.lang) if type_.bot_entity_descriptor.fields_descriptors["name"].localizable else item.name}",
"value": str(item.id)} for item in entity_items]
total_pages = items_count // page_size + (1 if items_count % page_size else 0)
# total_pages = items_count // page_size + (1 if items_count % page_size else 0)
keyboard_builder = InlineKeyboardBuilder()
@@ -112,6 +148,11 @@ async def render_entity_picker(*,
total_pages = total_pages,
command = CallbackCommand.ENTITY_PICKER_PAGE,
page = page)
if issubclass(type_, BotEntity):
add_filter_controls(keyboard_builder = keyboard_builder,
entity_descriptor = type_.bot_entity_descriptor,
filter = entity_filter)
if is_list:
keyboard_builder.row(
@@ -124,11 +165,14 @@ async def render_entity_picker(*,
field_name = callback_data.field_name,
save_state = True).pack()))
await wrap_editor(keyboard_builder = keyboard_builder,
field_descriptor = field_descriptor,
entity_descriptor = entity_descriptor,
callback_data = callback_data,
state = state)
state_data = kwargs["state_data"]
await wrap_editor(keyboard_builder = keyboard_builder,
field_descriptor = field_descriptor,
callback_data = callback_data,
state_data = state_data)
await state.set_data(state_data)
send_message = get_send_message(message)
@@ -140,14 +184,14 @@ async def render_entity_picker(*,
async def entity_picker_callback(query: CallbackQuery,
callback_data: ContextData,
db_session: AsyncSession,
app: QBotApp,
app: "QBotApp",
state: FSMContext,
**kwargs):
state_data = await state.get_data()
kwargs["state_data"] = state_data
field_descriptor = get_field_descriptor(app = app, callback_data = callback_data)
entity_descriptor = get_entity_descriptor(app = app, callback_data = callback_data)
current_value = await deserialize(session = db_session, type_ = field_descriptor.type_, value = state_data["current_value"])
edit_prompt = state_data["edit_prompt"]
@@ -156,7 +200,7 @@ async def entity_picker_callback(query: CallbackQuery,
if callback_data.command == CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM:
page, id_value = callback_data.data.split("&")
page = int(page)
type_ = get_args(field_descriptor.type_)[0]
type_ = field_descriptor.type_base
if issubclass(type_, BotEnum):
item = type_(id_value)
if item in value:
@@ -170,7 +214,7 @@ async def entity_picker_callback(query: CallbackQuery,
else:
value.append(item)
await state.update_data({"value": serialize(value, field_descriptor)})
state_data.update({"value": serialize(value, field_descriptor)})
elif callback_data.command == CallbackCommand.ENTITY_PICKER_PAGE:
if callback_data.data == "skip":
return
@@ -179,7 +223,6 @@ async def entity_picker_callback(query: CallbackQuery,
raise ValueError("Unsupported command")
await render_entity_picker(field_descriptor = field_descriptor,
entity_descriptor = entity_descriptor,
message = query,
callback_data = callback_data,
current_value = value,

View File

@@ -21,7 +21,6 @@ router = Router()
async def string_editor(message: Message | CallbackQuery,
field_descriptor: EntityFieldDescriptor,
entity_descriptor: EntityDescriptor,
callback_data: ContextData,
current_value: Any,
edit_prompt: str,
@@ -31,14 +30,16 @@ async def string_editor(message: Message | CallbackQuery,
keyboard_builder = InlineKeyboardBuilder()
state_data: dict = kwargs["state_data"]
_edit_prompt = edit_prompt
type_ = field_descriptor.type_
type_origin = get_origin(type_)
if type_origin == UnionType:
type_ = get_args(type_)[0]
# type_ = field_descriptor.type_
# type_origin = get_origin(type_)
# if type_origin == UnionType:
# type_ = get_args(type_)[0]
if type_ == str and field_descriptor.localizable:
if field_descriptor.type_base == str and field_descriptor.localizable:
current_locale = list(LanguageBase.all_members.values())[locale_index]
context_data = ContextData(
@@ -51,9 +52,9 @@ async def string_editor(message: Message | CallbackQuery,
_edit_prompt = f"{edit_prompt}\n{(await Settings.get(
Settings.APP_STRINGS_STRING_EDITOR_LOCALE_TEMPLATE_P_NAME)).format(name = current_locale)}"
_current_value = get_local_text(current_value, current_locale)
_current_value = get_local_text(current_value, current_locale) if current_value else None
await state.update_data({
state_data.update({
"context_data": context_data.pack(),
"edit_prompt": edit_prompt,
"locale_index": str(locale_index),
@@ -70,7 +71,7 @@ async def string_editor(message: Message | CallbackQuery,
_current_value = serialize(current_value, field_descriptor)
await state.update_data({
state_data.update({
"context_data": context_data.pack()})
if _current_value:
@@ -79,16 +80,19 @@ async def string_editor(message: Message | CallbackQuery,
keyboard_builder.row(InlineKeyboardButton(text = _current_value_caption,
copy_text = CopyTextButton(text = _current_value)))
state_data = kwargs["state_data"]
await wrap_editor(keyboard_builder = keyboard_builder,
field_descriptor = field_descriptor,
entity_descriptor = entity_descriptor,
callback_data = callback_data,
state = state)
state_data = state_data)
await state.set_data(state_data)
send_message = get_send_message(message)
await send_message(text = _edit_prompt, reply_markup = keyboard_builder.as_markup())
async def context_command_fiter(*args, **kwargs):
print(args, kwargs)
return True
# async def context_command_fiter(*args, **kwargs):
# print(args, kwargs)
# return True

View File

@@ -1,4 +1,4 @@
from typing import get_args, get_origin
from typing import get_args, get_origin, TYPE_CHECKING
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
@@ -9,7 +9,6 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....main import QBotApp
from ....model.bot_entity import BotEntity
from ....model.bot_enum import BotEnum
from ....model.owned_bot_entity import OwnedBotEntity
@@ -21,25 +20,33 @@ from ....utils import serialize, deserialize, get_user_permissions
from ..context import ContextData, CallbackCommand, CommandContext
from ..common import get_send_message, get_local_text, get_entity_descriptor, get_callable_str, get_value_repr
if TYPE_CHECKING:
from ....main import QBotApp
logger = getLogger(__name__)
router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_ITEM))
async def entity_item_callback(query: CallbackQuery, callback_data: ContextData, **kwargs):
async def entity_item_callback(query: CallbackQuery, **kwargs):
await clear_state(state = kwargs["state"])
stack = await save_navigation_context(callback_data = callback_data, state = kwargs["state"])
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
clear_state(state_data = state_data)
stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
await entity_item(query = query, callback_data = callback_data, navigation_stack = stack, **kwargs)
await entity_item(query = query, navigation_stack = stack, **kwargs)
async def entity_item(query: CallbackQuery,
callback_data: ContextData,
db_session: AsyncSession,
user: UserBase,
app: QBotApp,
app: "QBotApp",
navigation_stack: list[ContextData],
**kwargs):
@@ -77,8 +84,40 @@ async def entity_item(query: CallbackQuery,
(EntityPermission.DELETE in user_permissions and is_owned and
entity_item.user_id == user.id))
edit_delete_row = []
if can_edit:
for edit_buttons_row in entity_descriptor.edit_buttons:
btn_row = []
for field_name in edit_buttons_row:
field_name, btn_caption = field_name if isinstance(field_name, tuple) else (field_name, None)
if field_name in entity_descriptor.fields_descriptors:
field_descriptor = entity_descriptor.fields_descriptors[field_name]
# if field_descriptor.is_list and issubclass(field_descriptor.type_base, BotEntity):
# await field_descriptor.type_base.
field_value = getattr(entity_item, field_descriptor.field_name)
if btn_caption:
btn_text = get_callable_str(btn_caption, field_descriptor, entity_item, field_value)
else:
if field_descriptor.type_base == bool:
btn_text = f"{"【✔︎】 " if field_value else "【 】 "}{
get_callable_str(field_descriptor.caption, field_descriptor, entity_item, field_value) if field_descriptor.caption
else field_name}"
else:
btn_text = (f"✏️ {get_callable_str(field_descriptor.caption, field_descriptor, entity_item, field_value)}"
if field_descriptor.caption else f"✏️ {field_name}")
btn_row.append(
InlineKeyboardButton(
text = btn_text,
callback_data = ContextData(
command = CallbackCommand.FIELD_EDITOR,
context = CommandContext.ENTITY_FIELD_EDIT,
entity_name = entity_descriptor.name,
entity_id = str(entity_item.id),
field_name = field_name).pack()))
if btn_row:
keyboard_builder.row(*btn_row)
edit_delete_row = []
if can_edit and entity_descriptor.edit_button_visible:
edit_delete_row.append(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_EDIT_BTN)),
@@ -87,8 +126,7 @@ async def entity_item(query: CallbackQuery,
context = CommandContext.ENTITY_EDIT,
entity_name = entity_descriptor.name,
entity_id = str(entity_item.id),
field_name = entity_descriptor.field_sequence[0],
save_state = True).pack()))
field_name = entity_descriptor.field_sequence[0]).pack()))
if can_delete:
edit_delete_row.append(
@@ -102,15 +140,15 @@ async def entity_item(query: CallbackQuery,
if edit_delete_row:
keyboard_builder.row(*edit_delete_row)
entity_caption = get_callable_str(entity_descriptor.caption_msg, entity_descriptor, entity_item)
entity_caption = get_callable_str(entity_descriptor.caption, entity_descriptor, entity_item)
entity_item_name = get_local_text(entity_item.name, user.lang) if entity_descriptor.fields_descriptors["name"].localizable else entity_item.name
item_text = f"<b><u><i>{entity_caption or entity_descriptor.name}:</i></u></b> <b>{entity_item_name}</b>"
for field_descriptor in entity_descriptor.fields_descriptors.values():
if field_descriptor.name in ["name", "id"] or not field_descriptor.is_visible:
if field_descriptor.name == "name" or not field_descriptor.is_visible:
continue
field_caption = get_callable_str(field_descriptor.caption_str, field_descriptor, entity_item)
field_caption = get_callable_str(field_descriptor.caption, field_descriptor, entity_item)
value = get_value_repr(value = getattr(entity_item, field_descriptor.name),
field_descriptor = field_descriptor,
locale = user.lang)
@@ -123,6 +161,10 @@ async def entity_item(query: CallbackQuery,
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
await state.set_data(state_data)
send_message = get_send_message(query)
await send_message(text = item_text, reply_markup = keyboard_builder.as_markup())
@@ -134,7 +176,7 @@ async def entity_delete_callback(query: CallbackQuery, **kwargs):
callback_data: ContextData = kwargs["callback_data"]
user: UserBase = kwargs["user"]
db_session: AsyncSession = kwargs["db_session"]
app: QBotApp = kwargs["app"]
app: "QBotApp" = kwargs["app"]
entity_descriptor = get_entity_descriptor(app = app, callback_data = callback_data)
user_permissions = get_user_permissions(user, entity_descriptor)
@@ -147,8 +189,18 @@ async def entity_delete_callback(query: CallbackQuery, **kwargs):
entity.user_id == user.id)):
return await query.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
if callback_data.data == "yes":
if not callback_data.data:
await entity_descriptor.type_.remove(
session = db_session, id = int(callback_data.entity_id), commit = True)
await route_callback(message = query, **kwargs)
elif callback_data.data == "no":
await route_callback(message = query, back = False, **kwargs)
elif not callback_data.data:
field_descriptor = entity_descriptor.fields_descriptors["name"]
@@ -175,16 +227,6 @@ async def entity_delete_callback(query: CallbackQuery, **kwargs):
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
data = "no").pack())).as_markup())
if callback_data.data == "yes":
await entity_descriptor.type_.remove(
session = db_session, id = int(callback_data.entity_id), commit = True)
await route_callback(message = query, **kwargs)
if callback_data.data == "no":
await route_callback(message = query, back = False, **kwargs)

View File

@@ -1,4 +1,4 @@
from typing import get_args, get_origin
from typing import get_args, get_origin, TYPE_CHECKING
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
@@ -9,18 +9,21 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....main import QBotApp
from ....model.bot_entity import BotEntity
from ....model.bot_enum import BotEnum
from ....model.owned_bot_entity import OwnedBotEntity
from ....model.settings import Settings
from ....model.user import UserBase
from ....model.view_setting import ViewSetting
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ....model import EntityPermission
from ....utils import serialize, deserialize, get_user_permissions
from ..context import ContextData, CallbackCommand, CommandContext
from ..common import (add_pagination_controls, get_local_text, get_entity_descriptor,
get_callable_str, get_send_message)
get_callable_str, get_send_message, add_filter_controls)
if TYPE_CHECKING:
from ....main import QBotApp
logger = getLogger(__name__)
@@ -28,22 +31,28 @@ router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_LIST))
async def entity_list_callback(query: CallbackQuery, callback_data: ContextData, **kwargs):
async def entity_list_callback(query: CallbackQuery, **kwargs):
callback_data: ContextData = kwargs["callback_data"]
if callback_data.data == "skip":
return
await clear_state(state = kwargs["state"])
stack = await save_navigation_context(callback_data = callback_data, state = kwargs["state"])
await entity_list(message = query, callback_data = callback_data, navigation_stack = stack, **kwargs)
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
clear_state(state_data = state_data)
stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
await entity_list(message = query, navigation_stack = stack, **kwargs)
async def entity_list(message: CallbackQuery | Message,
callback_data: ContextData,
db_session: AsyncSession,
user: UserBase,
app: QBotApp,
app: "QBotApp",
navigation_stack: list[ContextData],
**kwargs):
@@ -68,38 +77,54 @@ async def entity_list(message: CallbackQuery | Message,
page_size = await Settings.get(Settings.PAGE_SIZE)
entity_filter = await ViewSetting.get_filter(session = db_session, user_id = user.id, entity_name = entity_descriptor.class_name)
def calc_total_pages(items_count: int, page_size: int) -> int:
return max(items_count // page_size + (1 if items_count % page_size else 0), 1)
if issubclass(entity_type, OwnedBotEntity):
if EntityPermission.READ_ALL in user_permissions or EntityPermission.LIST_ALL in user_permissions:
items_count = await entity_type.get_count(session = db_session, filter = entity_filter)
total_pages = calc_total_pages(items_count, page_size)
page = min(page, total_pages)
items = await entity_type.get_multi(
session = db_session, order_by = entity_type.name,
skip = page_size * (page - 1), limit = page_size)
items_count = await entity_type.get_count(session = db_session)
session = db_session, order_by = entity_type.name, filter = entity_filter,
skip = page_size * (page - 1), limit = page_size)
elif EntityPermission.READ in user_permissions or EntityPermission.LIST in user_permissions:
items_count = await entity_type.get_count_by_user(session = db_session, user_id = user.id, filter = entity_filter)
total_pages = calc_total_pages(items_count, page_size)
page = min(page, total_pages)
items = await entity_type.get_multi_by_user(
session = db_session, user_id = user.id, order_by = entity_type.name,
session = db_session, user_id = user.id, order_by = entity_type.name, filter = entity_filter,
skip = page_size * (page - 1), limit = page_size)
items_count = await entity_type.get_count_by_user(session = db_session, user_id = user.id)
else:
items = list[OwnedBotEntity]()
items_count = 0
total_pages = 1
page = 1
elif issubclass(entity_type, BotEntity):
if (EntityPermission.READ in user_permissions or EntityPermission.LIST in user_permissions or
EntityPermission.READ_ALL in user_permissions or EntityPermission.LIST_ALL in user_permissions):
items_count = await entity_type.get_count(session = db_session, filter = entity_filter)
total_pages = calc_total_pages(items_count, page_size)
page = min(page, total_pages)
items = await entity_type.get_multi(
session = db_session, order_by = entity_type.name,
session = db_session, order_by = entity_type.name, filter = entity_filter,
skip = page_size * (page - 1), limit = page_size)
items_count = await entity_type.get_count(session = db_session)
else:
items = list[BotEntity]()
total_pages = 1
page = 1
items_count = 0
else:
raise ValueError(f"Unsupported entity type: {entity_type}")
total_pages = items_count // page_size + (1 if items_count % page_size else 0)
for item in items:
if entity_descriptor.item_caption_btn:
caption = entity_descriptor.item_caption_btn(entity_descriptor, item)
if entity_descriptor.item_caption:
caption = entity_descriptor.item_caption(entity_descriptor, item)
elif entity_descriptor.fields_descriptors["name"].localizable:
caption = get_local_text(item.name, user.lang)
else:
@@ -118,6 +143,10 @@ async def entity_list(message: CallbackQuery | Message,
command = CallbackCommand.ENTITY_LIST,
page = page)
add_filter_controls(keyboard_builder = keyboard_builder,
entity_descriptor = entity_descriptor,
filter = entity_filter)
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(
@@ -125,8 +154,8 @@ async def entity_list(message: CallbackQuery | Message,
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
if entity_descriptor.caption_msg:
entity_text = get_callable_str(entity_descriptor.caption_msg, entity_descriptor)
if entity_descriptor.caption:
entity_text = get_callable_str(entity_descriptor.caption_plural, entity_descriptor)
else:
entity_text = entity_descriptor.name
if entity_descriptor.description:
@@ -134,6 +163,10 @@ async def entity_list(message: CallbackQuery | Message,
else:
entity_desciption = None
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
await state.set_data(state_data)
send_message = get_send_message(message)
await send_message(text = f"{entity_text}{f"\n{entity_desciption}" if entity_desciption else ""}",

View File

@@ -5,14 +5,16 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from babel.support import LazyProxy
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....main import QBotApp
from typing import TYPE_CHECKING
from ....model.settings import Settings
from ....model.user import UserBase
from ..context import ContextData, CallbackCommand
from ..common import get_send_message
from ....model.descriptors import EntityCaptionCallable
if TYPE_CHECKING:
from ....main import QBotApp
logger = getLogger(__name__)
router = Router()
@@ -21,16 +23,18 @@ router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_ENTITIES))
async def menu_entry_entities(message: CallbackQuery, **kwargs):
stack = await save_navigation_context(
callback_data = kwargs["callback_data"],
state = kwargs["state"])
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
await entities_menu(message = message, navigation_stack = stack, **kwargs)
async def entities_menu(message: Message | CallbackQuery,
callback_data: ContextData,
app: QBotApp,
app: "QBotApp",
state: FSMContext,
navigation_stack: list[ContextData],
**kwargs):
@@ -40,12 +44,12 @@ async def entities_menu(message: Message | CallbackQuery,
entity_metadata = app.entity_metadata
for entity in entity_metadata.entity_descriptors.values():
if entity.caption_btn.__class__ == EntityCaptionCallable:
caption = entity.caption_btn(entity) or entity.name
elif entity.caption_btn.__class__ == LazyProxy:
caption = f"{f"{entity.icon} " if entity.icon else ""}{entity.caption_btn.value or entity.name}"
if entity.caption_plural.__class__ == EntityCaptionCallable:
caption = entity.caption_plural(entity) or entity.name
elif entity.caption_plural.__class__ == LazyProxy:
caption = f"{f"{entity.icon} " if entity.icon else ""}{entity.caption_plural.value or entity.name}"
else:
caption = f"{f"{entity.icon} " if entity.icon else ""}{entity.caption_btn or entity.name}"
caption = f"{f"{entity.icon} " if entity.icon else ""}{entity.caption_plural or entity.name}"
keyboard_builder.row(
InlineKeyboardButton(
@@ -58,6 +62,9 @@ async def entities_menu(message: Message | CallbackQuery,
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
state_data = kwargs["state_data"]
await state.set_data(state_data)
send_message = get_send_message(message)

View File

@@ -1,17 +1,14 @@
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....main import QBotApp
from ....model.language import LanguageBase
from ....model.settings import Settings
from ....model.user import UserBase
from ..context import ContextData, CallbackCommand
from ..navigation import route_callback
from .settings import settings_menu
from ..common import get_send_message
@@ -22,8 +19,12 @@ router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_LANGUAGE))
async def menu_entry_language(message: CallbackQuery, **kwargs):
stack = await save_navigation_context(callback_data = kwargs["callback_data"],
state = kwargs["state"])
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
await language_menu(message, navigation_stack = stack, **kwargs)
@@ -45,18 +46,30 @@ async def language_menu(message: Message | CallbackQuery,
if context:
inline_keyboard.append([InlineKeyboardButton(text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack())])
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
await state.set_data(state_data)
await send_message(text = (await Settings.get(Settings.APP_STRINGS_LANGUAGE)),
reply_markup = InlineKeyboardMarkup(inline_keyboard = inline_keyboard))
@router.callback_query(ContextData.filter(F.command == CallbackCommand.SET_LANGUAGE))
async def set_language(message: CallbackQuery, user: UserBase, callback_data: ContextData, db_session: AsyncSession, **kwargs):
async def set_language(message: CallbackQuery, **kwargs):
user.lang = callback_data.data
user: UserBase = kwargs["user"]
callback_data: ContextData = kwargs["callback_data"]
db_session: AsyncSession = kwargs["db_session"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
user.lang = LanguageBase(callback_data.data)
await db_session.commit()
await route_callback(message, callback_data = callback_data, user = user, db_session = db_session, **kwargs)
await route_callback(message, **kwargs)
from ..navigation import pop_navigation_context, save_navigation_context

View File

@@ -5,8 +5,6 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....main import QBotApp
from ....model.settings import Settings
from ....model.user import UserBase
from ..context import ContextData, CallbackCommand
@@ -17,15 +15,15 @@ logger = getLogger(__name__)
router = Router()
@router.message(Command("menu"))
async def command_menu(message: Message, **kwargs):
# @router.message(Command("menu"))
# async def command_menu(message: Message, **kwargs):
await clear_state(state = kwargs["state"], clear_nav = True)
callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_MAIN)
stack = await save_navigation_context(callback_data = callback_data, state = kwargs["state"])
kwargs.update({"navigation_stack": stack, "callback_data": callback_data})
# await clear_state(state = kwargs["state"], clear_nav = True)
# callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_MAIN)
# stack = await save_navigation_context(callback_data = callback_data, state = kwargs["state"])
# kwargs.update({"navigation_stack": stack, "callback_data": callback_data})
await main_menu(message, **kwargs)
# await main_menu(message, **kwargs)
# @router.callback_query(CallbackData.filter(F.command == CallbackCommand.MENU_ENTRY))
@@ -77,6 +75,8 @@ from .language import router as language_router
from ..editors import router as editors_router
from ..forms.entity_list import router as entity_list_router
from ..forms.entity_form import router as entity_form_router
from ..common import router as common_router
from ..user_handlers import router as user_handlers_router
router.include_routers(
entities_router,
@@ -85,7 +85,9 @@ router.include_routers(
language_router,
editors_router,
entity_list_router,
entity_form_router
entity_form_router,
common_router,
user_handlers_router
)
from ..navigation import save_navigation_context, pop_navigation_context, clear_state

View File

@@ -8,11 +8,10 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....main import QBotApp
from ....model.settings import Settings
from ....model.user import UserBase
from ..context import ContextData, CallbackCommand, CommandContext
from ..common import get_send_message, get_value_repr, get_callable_str, authorize_command
from ..navigation import save_navigation_context, pop_navigation_context
@@ -23,9 +22,13 @@ router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_PARAMETERS))
async def menu_entry_parameters(message: CallbackQuery, **kwargs):
await clear_state(state = kwargs["state"])
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
stack = await save_navigation_context(callback_data = kwargs["callback_data"], state = kwargs["state"])
clear_state(state_data = state_data)
stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
await parameters_menu(message = message, navigation_stack = stack, **kwargs)
@@ -47,11 +50,11 @@ async def parameters_menu(message: Message | CallbackQuery,
if not key.is_visible:
continue
if key.caption_value_btn:
caption = get_callable_str(callable_str = key.caption_value_btn, descriptor = key, entity = None, value = value)
if key.caption_value:
caption = get_callable_str(callable_str = key.caption_value, descriptor = key, entity = None, value = value)
else:
if key.caption_btn:
caption = get_callable_str(callable_str = key.caption_btn, descriptor = key, entity = None, value = value)
if key.caption:
caption = get_callable_str(callable_str = key.caption, descriptor = key, entity = None, value = value)
else:
caption = key.name
@@ -72,9 +75,14 @@ async def parameters_menu(message: Message | CallbackQuery,
keyboard_builder.row(InlineKeyboardButton(text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
await state.set_data(state_data)
send_message = get_send_message(message)
await send_message(text = (await Settings.get(Settings.APP_STRINGS_PARAMETERS)), reply_markup = keyboard_builder.as_markup())
from ..navigation import pop_navigation_context, get_navigation_context, clear_state
from ..navigation import pop_navigation_context, get_navigation_context, clear_state
from ..common import get_send_message, get_value_repr, get_callable_str, authorize_command

View File

@@ -6,7 +6,6 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....main import QBotApp
from ....model.settings import Settings
from ....model.user import UserBase
from ..context import ContextData, CallbackCommand
@@ -20,7 +19,12 @@ router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_SETTINGS))
async def menu_entry_settings(message: CallbackQuery, **kwargs):
stack = await save_navigation_context(callback_data = kwargs["callback_data"], state = kwargs["state"])
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
await settings_menu(message, navigation_stack = stack, **kwargs)
@@ -49,9 +53,15 @@ async def settings_menu(message: Message | CallbackQuery,
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
await state.set_data(state_data)
send_message = get_send_message(message)
await send_message(text = (await Settings.get(Settings.APP_STRINGS_SETTINGS)), reply_markup = keyboard_builder.as_markup())

View File

@@ -4,23 +4,25 @@ from aiogram.types import Message, CallbackQuery
from .context import ContextData, CallbackCommand
async def save_navigation_context(callback_data: ContextData, state: FSMContext) -> list[ContextData]:
data = await state.get_data()
stack = [ContextData.unpack(item) for item in data.get("navigation_stack", [])]
data_nc = data.get("navigation_context")
def save_navigation_context(callback_data: ContextData, state_data: dict) -> list[ContextData]:
stack = [ContextData.unpack(item) for item in state_data.get("navigation_stack", [])]
data_nc = state_data.get("navigation_context")
navigation_context = ContextData.unpack(data_nc) if data_nc else None
if callback_data.back:
callback_data.back = False
if stack:
stack.pop()
else:
if stack and navigation_context and navigation_context.command == callback_data.command:
if (stack and navigation_context and
navigation_context.command == callback_data.command and
navigation_context.command != CallbackCommand.USER_COMMAND):
navigation_context = callback_data
elif navigation_context:
stack.append(navigation_context)
await state.update_data({"navigation_stack": [item.pack() for item in stack],
"navigation_context": callback_data.pack()})
state_data["navigation_stack"] = [item.pack() for item in stack]
state_data["navigation_context"] = callback_data.pack()
return stack
@@ -31,36 +33,33 @@ def pop_navigation_context(stack: list[ContextData]) -> ContextData | None:
return data
async def get_navigation_context(state: FSMContext) -> tuple[list[ContextData], ContextData | None]:
data = await state.get_data()
data_nc = data.get("navigation_context")
def get_navigation_context(state_data: dict) -> tuple[list[ContextData], ContextData | None]:
data_nc = state_data.get("navigation_context")
context = ContextData.unpack(data_nc) if data_nc else None
return ([ContextData.unpack(item) for item in data.get("navigation_stack", [])],
return ([ContextData.unpack(item) for item in state_data.get("navigation_stack", [])],
context)
async def clear_state(state: FSMContext, clear_nav: bool = False):
def clear_state(state_data: dict, clear_nav: bool = False):
if clear_nav:
await state.clear()
state_data.clear()
else:
state_data = await state.get_data()
stack = state_data.get("navigation_stack")
context = state_data.get("navigation_context")
update_data = {}
state_data.clear()
if stack:
update_data["navigation_stack"] = stack
state_data["navigation_stack"] = stack
if context:
update_data["navigation_context"] = context
await state.clear()
await state.update_data(update_data)
state_data["navigation_context"] = context
async def route_callback(message: Message | CallbackQuery, back: bool = True, **kwargs):
stack, context = await get_navigation_context(kwargs["state"])
state_data = kwargs["state_data"]
stack, context = get_navigation_context(state_data)
if back:
context = pop_navigation_context(stack)
stack = await save_navigation_context(callback_data = context, state = kwargs["state"])
stack = save_navigation_context(callback_data = context, state_data = state_data)
kwargs.update({"callback_data": context, "navigation_stack": stack})
if context:
if context.command == CallbackCommand.MENU_ENTRY_MAIN:
@@ -77,6 +76,8 @@ async def route_callback(message: Message | CallbackQuery, back: bool = True, **
await entity_list(message, **kwargs)
elif context.command == CallbackCommand.ENTITY_ITEM:
await entity_item(message, **kwargs)
elif context.command == CallbackCommand.FIELD_EDITOR:
await field_editor(message, **kwargs)
else:
raise ValueError(f"Unknown command {context.command}")
@@ -90,4 +91,5 @@ from .menu.parameters import parameters_menu
from .menu.language import language_menu
from .menu.entities import entities_menu
from .forms.entity_list import entity_list
from .forms.entity_form import entity_item
from .forms.entity_form import entity_item
from .editors import field_editor

View File

@@ -18,7 +18,8 @@ router = Router()
@router.message(CommandStart())
async def start(message: Message, db_session: AsyncSession, app: QBotApp, state: FSMContext):
await clear_state(state = state, clear_nav = True)
state_data = await state.get_data()
clear_state(state_data = state_data, clear_nav = True)
User = app.user_class

View File

@@ -0,0 +1,125 @@
from dataclasses import dataclass, field
from typing import Any, Callable, TYPE_CHECKING
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from aiogram.utils.i18n import I18n
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlmodel.ext.asyncio.session import AsyncSession
from ..context import ContextData, CallbackCommand
from ....model.user import UserBase
from ....model.settings import Settings
if TYPE_CHECKING:
from ....main import QBotApp
router = Router()
@dataclass(kw_only = True)
class CommandCallbackContext[UT: UserBase]:
keyboard_builder: InlineKeyboardBuilder = field(default_factory = InlineKeyboardBuilder)
message_text: str | None = None
register_navigation: bool = True
message: Message | CallbackQuery
callback_data: ContextData
db_session: AsyncSession
user: UT
app: "QBotApp"
state_data: dict[str, Any]
state: FSMContext
i18n: I18n
kwargs: dict[str, Any] = field(default_factory = dict)
@dataclass(kw_only = True)
class Command:
name: str
handler: Callable[[CommandCallbackContext], None]
caption: str | dict[str, str] | None = None
register_navigation: bool = True
clear_navigation: bool = False
clear_state: bool = True
@router.message(F.text.startswith("/"))
async def command_text(message: Message, **kwargs):
str_command = message.text.lstrip("/")
callback_data = ContextData(command = CallbackCommand.USER_COMMAND,
user_command = str_command)
await command_handler(message = message, callback_data = callback_data, **kwargs)
@router.callback_query(ContextData.filter(F.command == CallbackCommand.USER_COMMAND))
async def command_callback(message: CallbackQuery, **kwargs):
await command_handler(message = message, **kwargs)
async def command_handler(message: Message | CallbackQuery, **kwargs):
callback_data: ContextData = kwargs.pop("callback_data")
str_command = callback_data.user_command
app: "QBotApp" = kwargs.pop("app")
command = app.bot_commands.get(str_command)
if not command:
return
state: FSMContext = kwargs.pop("state")
state_data = await state.get_data()
if command.register_navigation:
clear_state(state_data = state_data)
if command.clear_navigation:
state_data.pop("navigation_stack", None)
state_data.pop("navigation_context", None)
if command.register_navigation:
stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
callback_context = CommandCallbackContext[app.user_class](
message = message,
callback_data = callback_data,
db_session = kwargs.pop("db_session"),
user = kwargs.pop("user"),
app = app,
state_data = state_data,
state = state,
i18n = kwargs.pop("i18n"),
kwargs = kwargs)
await command.handler(callback_context)
await state.set_data(state_data)
if command.register_navigation:
stack, navigation_context = get_navigation_context(state_data = state_data)
back_callback_data = pop_navigation_context(stack = stack)
if back_callback_data:
callback_context.keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = back_callback_data.pack()))
send_message = get_send_message(message)
if isinstance(message, CallbackCommand):
message = message.message
if callback_context.message_text:
await send_message(text = callback_context.message_text,
reply_markup = callback_context.keyboard_builder.as_markup())
else:
await message.edit_reply_markup(reply_markup = callback_context.keyboard_builder.as_markup())
from ..common import get_send_message
from ..navigation import save_navigation_context, get_navigation_context, clear_state, pop_navigation_context