add ruff format, ruff check, time_picker, project structure and imports reorganized

This commit is contained in:
Alexander Kalinovsky
2025-01-21 23:50:19 +01:00
parent ced47ac993
commit 9dd0708a5b
58 changed files with 3690 additions and 2583 deletions

View File

@@ -8,7 +8,6 @@ logger = getLogger(__name__)
class CallbackCommandFilter(Filter):
def __init__(self, command: CallbackCommand):
self.command = command
@@ -19,11 +18,9 @@ class CallbackCommandFilter(Filter):
if context_data:
try:
context_data = ContextData.unpack(context_data)
except Exception as e:
logger.error(f"Error unpacking context data", exc_info = True)
except Exception:
logger.error("Error unpacking context data", exc_info=True)
return False
else:
return context_data.command == self.command
return False

View File

@@ -1,345 +0,0 @@
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, 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 ....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):
return message.answer
else:
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_value_repr(value: Any, field_descriptor: EntityFieldDescriptor, locale: str | None = None) -> str:
type_ = field_descriptor.type_base
if value is None:
return ""
if isinstance(value, bool):
return "【✔︎】" if value else "【 】"
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 issubclass(type_, BotEnum):
return "[" + ", ".join(item.localized(locale) for item in value) + "]"
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(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(text = value, locale = locale)
return value
elif isinstance(value, int):
return str(value)
elif isinstance(value, float):
return str(value)
else:
return str(value)
def get_callable_str(callable_str: str | LazyProxy | EntityCaptionCallable | EntityItemCaptionCallable | EntityFieldCaptionCallable,
descriptor: EntityFieldDescriptor | EntityDescriptor,
entity: Any = None,
value: Any = None) -> str:
if isinstance(callable_str, str):
return callable_str
elif isinstance(callable_str, LazyProxy):
return callable_str.value
elif callable(callable_str):
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,
callback_data: ContextData):
if (callback_data.command == CallbackCommand.MENU_ENTRY_PARAMETERS or
callback_data.context == CommandContext.SETTING_EDIT):
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:
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:
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, 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)
return None
def add_pagination_controls(keyboard_builder: InlineKeyboardBuilder,
callback_data: ContextData,
total_pages: int,
command: CallbackCommand,
page: int):
if total_pages > 1:
navigation_buttons = []
ContextData(**callback_data.model_dump()).__setattr__
if total_pages > 10:
navigation_buttons.append(InlineKeyboardButton(text = "⏮️",
callback_data = ContextData(
command = command,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = "1" if page != 1 else "skip",
save_state = True).pack()))
navigation_buttons.append(InlineKeyboardButton(text = "⏪️",
callback_data = ContextData(
command = command,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = str(max(page - 10, 1)) if page > 1 else "skip",
save_state = True).pack()))
navigation_buttons.append(InlineKeyboardButton(text = f"◀️",
callback_data = ContextData(
command = command,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = str(max(page - 1, 1)) if page > 1 else "skip",
save_state = True).pack()))
navigation_buttons.append(InlineKeyboardButton(text = f"▶️",
callback_data = ContextData(
command = command,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = str(min(page + 1, total_pages)) if page < total_pages else "skip",
save_state = True).pack()))
if total_pages > 10:
navigation_buttons.append(InlineKeyboardButton(text = "⏩️",
callback_data = ContextData(
command = command,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = str(min(page + 10, total_pages)) if page < total_pages else "skip",
save_state = True).pack()))
navigation_buttons.append(InlineKeyboardButton(text = "⏭️",
callback_data = ContextData(
command = command,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = str(total_pages) if page != total_pages else "skip",
save_state = True).pack()))
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", None)
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:
state_data.pop("context_data", None)
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", None)
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:
state_data.pop("context_data", None)
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

@@ -0,0 +1,30 @@
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from ....model.descriptors import EntityDescriptor
from ....utils.main import get_callable_str
from ..context import ContextData, CallbackCommand
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(),
)
)

View File

@@ -0,0 +1,168 @@
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 sqlmodel.ext.asyncio.session import AsyncSession
from typing import TYPE_CHECKING
from ..context import ContextData, CallbackCommand
from ...command_context_filter import CallbackCommandFilter
from ....model.user import UserBase
from ....model.settings import Settings
from ....model.view_setting import ViewSetting
from ....utils.main import (
get_send_message,
get_entity_descriptor,
get_field_descriptor,
)
from ....utils.serialization import deserialize
from ..editors.entity import render_entity_picker
from .routing import route_callback
if TYPE_CHECKING:
from ....main import QBotApp
router = Router()
@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", None)
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:
state_data.pop("context_data", None)
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", None)
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:
state_data.pop("context_data", None)
return await route_callback(message=message, back=False, **kwargs)

View File

@@ -0,0 +1,114 @@
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from ..context import ContextData, CallbackCommand
def add_pagination_controls(
keyboard_builder: InlineKeyboardBuilder,
callback_data: ContextData,
total_pages: int,
command: CallbackCommand,
page: int,
):
if total_pages > 1:
navigation_buttons = []
ContextData(**callback_data.model_dump()).__setattr__
if total_pages > 10:
navigation_buttons.append(
InlineKeyboardButton(
text="⏮️",
callback_data=ContextData(
command=command,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data="1" if page != 1 else "skip",
).pack(),
)
)
navigation_buttons.append(
InlineKeyboardButton(
text="⏪️",
callback_data=ContextData(
command=command,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=str(max(page - 10, 1)) if page > 1 else "skip",
).pack(),
)
)
navigation_buttons.append(
InlineKeyboardButton(
text="◀️",
callback_data=ContextData(
command=command,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=str(max(page - 1, 1)) if page > 1 else "skip",
).pack(),
)
)
navigation_buttons.append(
InlineKeyboardButton(
text="▶️",
callback_data=ContextData(
command=command,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=(
str(min(page + 1, total_pages))
if page < total_pages
else "skip"
),
).pack(),
)
)
if total_pages > 10:
navigation_buttons.append(
InlineKeyboardButton(
text="⏩️",
callback_data=ContextData(
command=command,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=(
str(min(page + 10, total_pages))
if page < total_pages
else "skip"
),
).pack(),
)
)
navigation_buttons.append(
InlineKeyboardButton(
text="⏭️",
callback_data=ContextData(
command=command,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=str(total_pages) if page != total_pages else "skip",
).pack(),
)
)
keyboard_builder.row(*navigation_buttons)

View File

@@ -0,0 +1,48 @@
from aiogram.types import Message, CallbackQuery
from ..context import CallbackCommand
from ..navigation import (
get_navigation_context,
save_navigation_context,
pop_navigation_context,
)
import qbot.bot.handlers.menu.main as menu_main
import qbot.bot.handlers.menu.settings as menu_settings
import qbot.bot.handlers.menu.parameters as menu_parameters
import qbot.bot.handlers.menu.language as menu_language
import qbot.bot.handlers.menu.entities as menu_entities
import qbot.bot.handlers.forms.entity_list as form_list
import qbot.bot.handlers.forms.entity_form as form_item
import qbot.bot.handlers.editors.main as editor
async def route_callback(message: Message | CallbackQuery, back: bool = True, **kwargs):
state_data = kwargs["state_data"]
stack, context = get_navigation_context(state_data)
if back:
context = pop_navigation_context(stack)
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:
await menu_main.main_menu(message, **kwargs)
elif context.command == CallbackCommand.MENU_ENTRY_SETTINGS:
await menu_settings.settings_menu(message, **kwargs)
elif context.command == CallbackCommand.MENU_ENTRY_PARAMETERS:
await menu_parameters.parameters_menu(message, **kwargs)
elif context.command == CallbackCommand.MENU_ENTRY_LANGUAGE:
await menu_language.language_menu(message, **kwargs)
elif context.command == CallbackCommand.MENU_ENTRY_ENTITIES:
await menu_entities.entities_menu(message, **kwargs)
elif context.command == CallbackCommand.ENTITY_LIST:
await form_list.entity_list(message, **kwargs)
elif context.command == CallbackCommand.ENTITY_ITEM:
await form_item.entity_item(message, **kwargs)
elif context.command == CallbackCommand.FIELD_EDITOR:
await editor.field_editor(message, **kwargs)
else:
raise ValueError(f"Unknown command {context.command}")
else:
raise ValueError("No navigation context")

View File

@@ -1,8 +1,8 @@
from aiogram.filters.callback_data import CallbackData as BaseCallbackData
from enum import StrEnum
class CallbackCommand(StrEnum):
class CallbackCommand(StrEnum):
FIELD_EDITOR = "fe"
FIELD_EDITOR_CALLBACK = "fc"
ENTITY_LIST = "el"
@@ -16,25 +16,28 @@ class CallbackCommand(StrEnum):
SET_LANGUAGE = "ls"
DATE_PICKER_MONTH = "dm"
DATE_PICKER_YEAR = "dy"
#STRING_EDITOR_LOCALE = "sl"
TIME_PICKER = "tp"
# STRING_EDITOR_LOCALE = "sl"
ENTITY_PICKER_PAGE = "ep"
ENTITY_PICKER_TOGGLE_ITEM = "et"
VIEW_FILTER_EDIT = "vf"
USER_COMMAND = "uc"
class CommandContext(StrEnum):
class CommandContext(StrEnum):
SETTING_EDIT = "se"
ENTITY_CREATE = "ec"
ENTITY_EDIT = "ee"
ENTITY_FIELD_EDIT = "ef"
class ContextData(BaseCallbackData, prefix = "cd"):
class ContextData(BaseCallbackData, prefix="cd"):
command: CallbackCommand
context: CommandContext | None = None
entity_name: str | None = None
entity_id: int | None = None
field_name: str | None = None
form_params: str | None = None
user_command: str | None = None
data: str | None = None
back: bool = False

View File

@@ -1,416 +0,0 @@
from datetime import datetime
from decimal import Decimal
from types import NoneType, UnionType
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
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
import ujson as json
from ....model import EntityPermission
from ....model.bot_entity import BotEntity
from ....model.owned_bot_entity import OwnedBotEntity
from ....model.bot_enum import BotEnum
from ....model.language import LanguageBase
from ....model.settings import Settings
from ....model.user import UserBase
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 ..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()
router.include_routers(
string_editor_router,
date_picker_router,
bool_editor_router,
entity_picker_router,
)
@router.callback_query(ContextData.filter(F.command == CallbackCommand.FIELD_EDITOR))
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"]
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)
kwargs["state_data"] = state_data
entity_descriptor = None
if callback_data.context == CommandContext.SETTING_EDIT:
field_descriptor = get_field_descriptor(app, callback_data)
if field_descriptor.type_ == bool:
if await authorize_command(user = user, callback_data = callback_data):
await Settings.set_param(field_descriptor, not await Settings.get(field_descriptor))
else:
return await message.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
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, all_locales = True)
else:
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 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)):
if entity:
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,
current_value = current_value,
**kwargs)
async def show_editor(message: Message | CallbackQuery,
**kwargs):
field_descriptor: EntityFieldDescriptor = kwargs["field_descriptor"]
current_value = kwargs["current_value"]
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_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:
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:
edit_prompt = (await Settings.get(Settings.APP_STRINGS_FIELD_EDIT_PROMPT_TEMPLATE_P_NAME_VALUE)).format(
name = caption_str, value = get_value_repr(current_value, field_descriptor, user.lang))
else:
edit_prompt = (await Settings.get(Settings.APP_STRINGS_FIELD_CREATE_PROMPT_TEMPLATE_P_NAME)).format(
name = caption_str)
kwargs["edit_prompt"] = edit_prompt
# type_origin = get_origin(value_type)
# 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]:
state_data.update({"context_data": callback_data.pack()})
if value_type == str:
await string_editor(message = message, **kwargs)
elif value_type == bool:
await bool_editor(message = message, **kwargs)
elif value_type in [int, float, Decimal, str]:
await string_editor(message = message, **kwargs)
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 issubclass(value_type, BotEntity) or issubclass(value_type, BotEnum):
await entity_picker(message = message, **kwargs)
else:
raise ValueError(f"Unsupported field type: {value_type}")
@router.message(CallbackCommandFilter(CallbackCommand.FIELD_EDITOR_CALLBACK))
@router.callback_query(ContextData.filter(F.command == CallbackCommand.FIELD_EDITOR_CALLBACK))
async def field_editor_callback(message: Message | CallbackQuery, **kwargs):
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:
callback_data = ContextData.unpack(context_data)
value = message.text
field_descriptor = get_field_descriptor(app, callback_data)
type_base = field_descriptor.type_base
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:
#entity_data = state_data.get("entity_data", {})
#current_value = entity_data.get(field_descriptor.field_name)
current_value = state_data.get("current_value")
value = state_data.get("value")
if value:
value = json.loads(value)
else:
value = {}
value[list(LanguageBase.all_members.values())[locale_index]] = message.text
value = json.dumps(value, ensure_ascii = False)
state_data.update({"value": value})
entity_descriptor = get_entity_descriptor(app, callback_data)
kwargs.update({"callback_data": callback_data})
return await show_editor(message = message,
locale_index = locale_index + 1,
field_descriptor = field_descriptor,
entity_descriptor = entity_descriptor,
current_value = current_value,
value = value,
**kwargs)
else:
value = state_data.get("value")
if value:
value = json.loads(value)
else:
value = {}
value[list(LanguageBase.all_members.values())[locale_index]] = message.text
value = json.dumps(value, ensure_ascii = False)
elif (type_base in [int, float, Decimal]):
try:
_ = 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:
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)
kwargs.update({"callback_data": callback_data,})
await process_field_edit_callback(message = message,
value = value,
field_descriptor = field_descriptor,
**kwargs)
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_data: dict = kwargs["state_data"]
value = kwargs["value"]
field_descriptor: EntityFieldDescriptor = kwargs["field_descriptor"]
if callback_data.context == CommandContext.SETTING_EDIT:
# clear_state(state_data = state_data)
if callback_data.data != "cancel":
if await authorize_command(user = user, callback_data = callback_data):
value = await deserialize(session = db_session, type_ = field_descriptor.type_, value = value)
await Settings.set_param(field_descriptor, value)
else:
return await message.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
# stack, context = get_navigation_context(state_data = state_data)
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, CommandContext.ENTITY_FIELD_EDIT]:
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)
if callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT] else 0)
entity_data = state_data.get("entity_data", {})
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
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]
kwargs.update({"field_descriptor": next_field_descriptor})
callback_data.field_name = next_field_name
state_entity_val = entity_data.get(next_field_descriptor.field_name)
current_value = await deserialize(session = db_session, type_ = next_field_descriptor.type_,
value = state_entity_val) if state_entity_val else None
await show_editor(message = message,
entity_descriptor = entity_descriptor,
current_value = current_value,
**kwargs)
else:
entity_type: BotEntity = entity_descriptor.type_
user_permissions = get_user_permissions(user, entity_descriptor)
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 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)))
is_owned = issubclass(entity_type, OwnedBotEntity)
entity_data[field_descriptor.field_name] = value
if is_owned and EntityPermission.CREATE_ALL not in user_permissions:
entity_data["user_id"] = user.id
deser_entity_data = {key: await deserialize(
session = db_session,
type_ = entity_descriptor.fields_descriptors[key].type_,
value = value) for key, value in entity_data.items()}
if callback_data.context == CommandContext.ENTITY_CREATE:
new_entity = await entity_type.create(session = db_session,
obj_in = entity_type(**deser_entity_data),
commit = True)
state_data["navigation_context"] = ContextData(
command = CallbackCommand.ENTITY_ITEM,
entity_name = entity_descriptor.name,
entity_id = str(new_entity.id)).pack()
state_data.update(state_data)
# 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)
if not entity:
return await message.answer(text = (await Settings.get(Settings.APP_STRINGS_NOT_FOUND)))
if (is_owned and entity.user_id != user.id and
EntityPermission.UPDATE_ALL not in user_permissions):
return await message.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
for key, value in deser_entity_data.items():
setattr(entity, key, value)
await db_session.commit()
clear_state(state_data = state_data)
await route_callback(message = message, back = True, **kwargs)
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

@@ -5,22 +5,23 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from babel.support import LazyProxy
from logging import getLogger
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ....model.descriptors import EntityFieldDescriptor
from ..context import ContextData, CallbackCommand
from ..common import get_send_message
from .common import wrap_editor
from ....utils.main import get_send_message
from .wrapper import wrap_editor
logger = getLogger(__name__)
router = Router()
async def bool_editor(message: Message | CallbackQuery,
edit_prompt: str,
field_descriptor: EntityFieldDescriptor,
callback_data: ContextData,
**kwargs):
async def bool_editor(
message: Message | CallbackQuery,
edit_prompt: str,
field_descriptor: EntityFieldDescriptor,
callback_data: ContextData,
**kwargs,
):
keyboard_builder = InlineKeyboardBuilder()
if isinstance(field_descriptor.bool_true_value, LazyProxy):
@@ -34,41 +35,44 @@ async def bool_editor(message: Message | CallbackQuery,
false_caption = field_descriptor.bool_false_value
keyboard_builder.row(
InlineKeyboardButton(text = true_caption,
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 = str(True),
save_state = True).pack()),
InlineKeyboardButton(text = false_caption,
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 = str(False),
save_state = True).pack())
InlineKeyboardButton(
text=true_caption,
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,
form_params=callback_data.form_params,
data=str(True),
).pack(),
),
InlineKeyboardButton(
text=false_caption,
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,
form_params=callback_data.form_params,
data=str(False),
).pack(),
),
)
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 wrap_editor(
keyboard_builder=keyboard_builder,
field_descriptor=field_descriptor,
callback_data=callback_data,
state_data=state_data,
)
state: FSMContext = kwargs["state"]
await state.set_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())
await send_message(text=edit_prompt, reply_markup=keyboard_builder.as_markup())

View File

@@ -1,67 +1,75 @@
from types import NoneType, UnionType
from typing import get_args, get_origin
from aiogram.fsm.context import FSMContext
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import Message, CallbackQuery
from decimal import Decimal
from datetime import datetime, time
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ....model.bot_entity import BotEntity
from ....model.bot_enum import BotEnum
from ....model.descriptors import EntityFieldDescriptor
from ....model.settings import Settings
from ..context import ContextData, CallbackCommand, CommandContext
from ..navigation import get_navigation_context, pop_navigation_context
from ....model.user import UserBase
from ....utils.main import get_callable_str, get_value_repr
from ..context import ContextData, CommandContext
from .boolean import bool_editor
from .date import date_picker, time_picker
from .entity import entity_picker
from .string import string_editor
async def wrap_editor(keyboard_builder: InlineKeyboardBuilder,
field_descriptor: EntityFieldDescriptor,
callback_data: ContextData,
state_data: dict):
if callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT, CommandContext.ENTITY_FIELD_EDIT]:
btns = []
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 = get_navigation_context(state_data = state_data)
context = pop_navigation_context(stack)
async def show_editor(message: Message | CallbackQuery, **kwargs):
field_descriptor: EntityFieldDescriptor = kwargs["field_descriptor"]
current_value = kwargs["current_value"]
user: UserBase = kwargs["user"]
callback_data: ContextData = kwargs.get("callback_data", None)
state_data: dict = kwargs["state_data"]
if field_index > 0:
btns.append(InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = ContextData(
command = CallbackCommand.FIELD_EDITOR,
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]).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)
value_type = field_descriptor.type_base
keyboard_builder.row(InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_CANCEL_BTN)),
callback_data = context.pack()))
if field_descriptor.edit_prompt:
edit_prompt = get_callable_str(
field_descriptor.edit_prompt, field_descriptor, None, current_value
)
else:
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:
edit_prompt = (
await Settings.get(
Settings.APP_STRINGS_FIELD_EDIT_PROMPT_TEMPLATE_P_NAME_VALUE
)
).format(
name=caption_str,
value=get_value_repr(current_value, field_descriptor, user.lang),
)
else:
edit_prompt = (
await Settings.get(
Settings.APP_STRINGS_FIELD_CREATE_PROMPT_TEMPLATE_P_NAME
)
).format(name=caption_str)
elif callback_data.context == CommandContext.SETTING_EDIT:
kwargs["edit_prompt"] = edit_prompt
keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_CANCEL_BTN)),
callback_data = ContextData(
command = CallbackCommand.FIELD_EDITOR_CALLBACK,
context = callback_data.context,
field_name = callback_data.field_name,
data = "cancel").pack()))
if value_type not in [int, float, Decimal, str]:
state_data.update({"context_data": callback_data.pack()})
if value_type is bool:
await bool_editor(message=message, **kwargs)
elif value_type in [int, float, Decimal, str]:
await string_editor(message=message, **kwargs)
elif value_type is datetime:
await date_picker(message=message, **kwargs)
elif value_type is time:
await time_picker(message=message, **kwargs)
elif issubclass(value_type, BotEntity) or issubclass(value_type, BotEnum):
await entity_picker(message=message, **kwargs)
else:
raise ValueError(f"Unsupported field type: {value_type}")

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, time, timedelta
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
@@ -6,10 +6,11 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from typing import TYPE_CHECKING
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ....model.descriptors import EntityFieldDescriptor
from ....model.settings import Settings
from ..context import ContextData, CallbackCommand
from ..common import get_send_message, get_field_descriptor, get_entity_descriptor
from .common import wrap_editor
from ....utils.main import get_send_message, get_field_descriptor
from .wrapper import wrap_editor
if TYPE_CHECKING:
from ....main import QBotApp
@@ -19,154 +20,343 @@ logger = getLogger(__name__)
router = Router()
async def date_picker(message: Message | CallbackQuery,
field_descriptor: EntityFieldDescriptor,
callback_data: ContextData,
current_value: datetime,
state: FSMContext,
edit_prompt: str | None = None,
**kwargs):
if not current_value:
start_date = datetime.now()
else:
start_date = current_value
start_date = start_date.replace(day = 1)
previous_month = start_date - timedelta(days = 1)
next_month = start_date.replace(day = 28) + timedelta(days = 4)
keyboard_builder = InlineKeyboardBuilder()
keyboard_builder.row(InlineKeyboardButton(text = "◀️",
callback_data = ContextData(
command = CallbackCommand.DATE_PICKER_MONTH,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = previous_month.strftime("%Y-%m-%d"),
save_state = True).pack()),
InlineKeyboardButton(text = start_date.strftime("%b %Y"),
callback_data = ContextData(
command = CallbackCommand.DATE_PICKER_YEAR,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = start_date.strftime("%Y-%m-%d"),
save_state = True).pack()),
InlineKeyboardButton(text = "▶️",
callback_data = ContextData(
command = CallbackCommand.DATE_PICKER_MONTH,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = next_month.strftime("%Y-%m-%d"),
save_state = True).pack()))
first_day = start_date - timedelta(days = start_date.weekday())
weeks = (((start_date.replace(day = 28) + timedelta(days = 4)).replace(day = 1) - first_day).days - 1) // 7 + 1
for week in range(weeks):
buttons = []
for day in range(7):
current_day = first_day + timedelta(days = week * 7 + day)
buttons.append(InlineKeyboardButton(text = current_day.strftime("%d"),
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 = current_day.strftime("%Y-%m-%d"),
save_state = True).pack()))
keyboard_builder.row(*buttons)
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)
if edit_prompt:
send_message = get_send_message(message)
await send_message(text = edit_prompt, reply_markup = keyboard_builder.as_markup())
else:
await message.edit_reply_markup(reply_markup = keyboard_builder.as_markup())
@router.callback_query(ContextData.filter(F.command == CallbackCommand.DATE_PICKER_YEAR))
async def date_picker_year(query: CallbackQuery,
callback_data: ContextData,
app: "QBotApp",
state: FSMContext,
**kwargs):
start_date = datetime.strptime(callback_data.data, "%Y-%m-%d")
state_data = await state.get_data()
kwargs["state_data"] = state_data
keyboard_builder = InlineKeyboardBuilder()
keyboard_builder.row(InlineKeyboardButton(text = "🔼",
callback_data = ContextData(
command = CallbackCommand.DATE_PICKER_YEAR,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = start_date.replace(year = start_date.year - 20).strftime("%Y-%m-%d")).pack()))
for r in range(4):
buttons = []
for c in range(5):
current_date = start_date.replace(year = start_date.year + r * 5 + c - 10)
buttons.append(InlineKeyboardButton(text = current_date.strftime("%Y"),
callback_data = ContextData(
command = CallbackCommand.DATE_PICKER_MONTH,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = current_date.strftime("%Y-%m-%d"),
save_state = True).pack()))
keyboard_builder.row(*buttons)
keyboard_builder.row(InlineKeyboardButton(text = "🔽",
callback_data = ContextData(
command = CallbackCommand.DATE_PICKER_YEAR,
context = callback_data.context,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
field_name = callback_data.field_name,
data = start_date.replace(year = start_date.year + 20).strftime("%Y-%m-%d")).pack()))
field_descriptor = get_field_descriptor(app, callback_data)
await wrap_editor(keyboard_builder = keyboard_builder,
field_descriptor = field_descriptor,
callback_data = callback_data,
state_data = state_data)
await query.message.edit_reply_markup(reply_markup = keyboard_builder.as_markup())
@router.callback_query(ContextData.filter(F.command == CallbackCommand.DATE_PICKER_MONTH))
async def date_picker_month(query: CallbackQuery, callback_data: ContextData, app: "QBotApp", **kwargs):
@router.callback_query(ContextData.filter(F.command == CallbackCommand.TIME_PICKER))
async def time_picker_callback(
query: CallbackQuery, callback_data: ContextData, app: "QBotApp", **kwargs
):
if not callback_data.data:
return
field_descriptor = get_field_descriptor(app, callback_data)
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
await date_picker(query.message,
field_descriptor = field_descriptor,
callback_data = callback_data,
current_value = datetime.strptime(callback_data.data, "%Y-%m-%d"),
**kwargs)
await time_picker(
query.message,
field_descriptor=field_descriptor,
callback_data=callback_data,
current_value=datetime.strptime(callback_data.data, "%Y-%m-%d %H-%M")
if len(callback_data.data) > 10
else time.fromisoformat(callback_data.data.replace("-", ":")),
**kwargs,
)
async def time_picker(
message: Message | CallbackQuery,
field_descriptor: EntityFieldDescriptor,
callback_data: ContextData,
current_value: datetime | time,
state: FSMContext,
edit_prompt: str | None = None,
**kwargs,
):
keyboard_builder = InlineKeyboardBuilder()
for i in range(12):
keyboard_builder.row(
InlineKeyboardButton(
text=(
"▶︎ {v:02d} ◀︎" if i == (current_value.hour % 12) else "{v:02d}"
).format(v=i if current_value.hour < 12 else i + 12),
callback_data=ContextData(
command=CallbackCommand.TIME_PICKER,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=current_value.replace(
hour=i if current_value.hour < 12 else i + 12
).strftime(
"%Y-%m-%d %H-%M"
if isinstance(current_value, datetime)
else "%H-%M"
)
if i != current_value.hour % 12
else None,
).pack(),
),
InlineKeyboardButton(
text=(
"▶︎ {v:02d} ◀︎" if i == current_value.minute // 5 else "{v:02d}"
).format(v=i * 5),
callback_data=ContextData(
command=CallbackCommand.TIME_PICKER,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=current_value.replace(minute=i * 5).strftime(
"%Y-%m-%d %H-%M"
if isinstance(current_value, datetime)
else "%H-%M"
)
if i != current_value.minute // 5
else None,
).pack(),
),
)
keyboard_builder.row(
InlineKeyboardButton(
text="AM/PM",
callback_data=ContextData(
command=CallbackCommand.TIME_PICKER,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=current_value.replace(
hour=current_value.hour + 12
if current_value.hour < 12
else current_value.hour - 12
).strftime(
"%Y-%m-%d %H-%M" if isinstance(current_value, datetime) else "%H-%M"
),
).pack(),
),
InlineKeyboardButton(
text=await Settings.get(Settings.APP_STRINGS_DONE_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,
form_params=callback_data.form_params,
data=current_value.strftime(
"%Y-%m-%d %H-%M" if isinstance(current_value, datetime) else "%H-%M"
),
).pack(),
),
)
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)
if edit_prompt:
send_message = get_send_message(message)
await send_message(text=edit_prompt, reply_markup=keyboard_builder.as_markup())
else:
await message.edit_reply_markup(reply_markup=keyboard_builder.as_markup())
async def date_picker(
message: Message | CallbackQuery,
field_descriptor: EntityFieldDescriptor,
callback_data: ContextData,
current_value: datetime,
state: FSMContext,
edit_prompt: str | None = None,
**kwargs,
):
if not current_value:
start_date = datetime.now()
else:
start_date = current_value
start_date = current_value.replace(day=1)
previous_month = start_date - timedelta(days=1)
next_month = start_date.replace(day=28) + timedelta(days=4)
keyboard_builder = InlineKeyboardBuilder()
keyboard_builder.row(
InlineKeyboardButton(
text="◀️",
callback_data=ContextData(
command=CallbackCommand.DATE_PICKER_MONTH,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=previous_month.strftime("%Y-%m-%d %H-%M"),
).pack(),
),
InlineKeyboardButton(
text=start_date.strftime("%b %Y"),
callback_data=ContextData(
command=CallbackCommand.DATE_PICKER_YEAR,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=start_date.strftime("%Y-%m-%d %H-%M"),
).pack(),
),
InlineKeyboardButton(
text="▶️",
callback_data=ContextData(
command=CallbackCommand.DATE_PICKER_MONTH,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=next_month.strftime("%Y-%m-%d %H-%M"),
).pack(),
),
)
first_day = start_date - timedelta(days=start_date.weekday())
weeks = (
(
(start_date.replace(day=28) + timedelta(days=4)).replace(day=1) - first_day
).days
- 1
) // 7 + 1
for week in range(weeks):
buttons = []
for day in range(7):
current_day = first_day + timedelta(days=week * 7 + day)
buttons.append(
InlineKeyboardButton(
text=current_day.strftime("%d"),
callback_data=ContextData(
command=CallbackCommand.FIELD_EDITOR_CALLBACK
if field_descriptor.dt_type == "date"
else CallbackCommand.TIME_PICKER,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=current_day.strftime("%Y-%m-%d %H-%M"),
).pack(),
)
)
keyboard_builder.row(*buttons)
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)
if edit_prompt:
send_message = get_send_message(message)
await send_message(text=edit_prompt, reply_markup=keyboard_builder.as_markup())
else:
await message.edit_reply_markup(reply_markup=keyboard_builder.as_markup())
@router.callback_query(
ContextData.filter(F.command == CallbackCommand.DATE_PICKER_YEAR)
)
async def date_picker_year(
query: CallbackQuery,
callback_data: ContextData,
app: "QBotApp",
state: FSMContext,
**kwargs,
):
start_date = datetime.strptime(callback_data.data, "%Y-%m-%d %H-%M")
state_data = await state.get_data()
kwargs["state_data"] = state_data
keyboard_builder = InlineKeyboardBuilder()
keyboard_builder.row(
InlineKeyboardButton(
text="🔼",
callback_data=ContextData(
command=CallbackCommand.DATE_PICKER_YEAR,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=start_date.replace(year=start_date.year - 20).strftime(
"%Y-%m-%d %H-%M"
),
).pack(),
)
)
for r in range(4):
buttons = []
for c in range(5):
current_date = start_date.replace(year=start_date.year + r * 5 + c - 10)
buttons.append(
InlineKeyboardButton(
text=current_date.strftime("%Y"),
callback_data=ContextData(
command=CallbackCommand.DATE_PICKER_MONTH,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=current_date.strftime("%Y-%m-%d %H-%M"),
).pack(),
)
)
keyboard_builder.row(*buttons)
keyboard_builder.row(
InlineKeyboardButton(
text="🔽",
callback_data=ContextData(
command=CallbackCommand.DATE_PICKER_YEAR,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data=start_date.replace(year=start_date.year + 20).strftime(
"%Y-%m-%d %H-%M"
),
).pack(),
)
)
field_descriptor = get_field_descriptor(app, callback_data)
await wrap_editor(
keyboard_builder=keyboard_builder,
field_descriptor=field_descriptor,
callback_data=callback_data,
state_data=state_data,
)
await query.message.edit_reply_markup(reply_markup=keyboard_builder.as_markup())
@router.callback_query(
ContextData.filter(F.command == CallbackCommand.DATE_PICKER_MONTH)
)
async def date_picker_month(
query: CallbackQuery, callback_data: ContextData, app: "QBotApp", **kwargs
):
field_descriptor = get_field_descriptor(app, callback_data)
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
await date_picker(
query.message,
field_descriptor=field_descriptor,
callback_data=callback_data,
current_value=datetime.strptime(callback_data.data, "%Y-%m-%d %H-%M"),
**kwargs,
)

View File

@@ -1,25 +1,32 @@
from types import 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 logging import getLogger
from sqlmodel import column
from sqlmodel.ext.asyncio.session import AsyncSession
from typing import get_args, get_origin, TYPE_CHECKING
from typing import TYPE_CHECKING
from ....model.bot_entity import BotEntity
from ....model.owned_bot_entity import OwnedBotEntity
# 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 ....model.descriptors import EntityFieldDescriptor, Filter
from ....model import EntityPermission
from ....utils import serialize, deserialize, get_user_permissions
from ....utils.main import (
get_user_permissions,
get_send_message,
get_field_descriptor,
get_callable_str,
)
from ....utils.serialization import serialize, deserialize
from ..context import ContextData, CallbackCommand
from ..common import (get_send_message, get_local_text, get_field_descriptor,
get_entity_descriptor, add_pagination_controls, add_filter_controls)
from .common import wrap_editor
from ..common.pagination import add_pagination_controls
from ..common.filtering import add_filter_controls
from .wrapper import wrap_editor
if TYPE_CHECKING:
from ....main import QBotApp
@@ -28,179 +35,295 @@ logger = getLogger(__name__)
router = Router()
async def entity_picker(message: Message | CallbackQuery,
field_descriptor: EntityFieldDescriptor,
edit_prompt: str,
current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
**kwargs):
async def entity_picker(
message: Message | CallbackQuery,
field_descriptor: EntityFieldDescriptor,
edit_prompt: str,
current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
**kwargs,
):
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})
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,
current_value = current_value,
edit_prompt = edit_prompt,
**kwargs)
await render_entity_picker(
field_descriptor=field_descriptor,
message=message,
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,
message: Message | CallbackQuery,
callback_data: ContextData,
user: UserBase,
db_session: AsyncSession,
state: FSMContext,
current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
edit_prompt: str,
page: int = 1,
**kwargs):
if callback_data.command in [CallbackCommand.ENTITY_PICKER_PAGE, CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM]:
async def render_entity_picker(
*,
field_descriptor: EntityFieldDescriptor,
message: Message | CallbackQuery,
callback_data: ContextData,
user: UserBase,
db_session: AsyncSession,
state: FSMContext,
current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
edit_prompt: str,
page: int = 1,
**kwargs,
):
if callback_data.command in [
CallbackCommand.ENTITY_PICKER_PAGE,
CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM,
]:
page = int(callback_data.data.split("&")[0])
# is_list = False
# 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
# 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")
page_size = await Settings.get(Settings.PAGE_SIZE)
form_list = None
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:
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
]
elif issubclass(type_, BotEntity):
form_name = field_descriptor.ep_form or "default"
form_list = type_.bot_entity_descriptor.lists.get(
form_name, type_.bot_entity_descriptor.default_list
)
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)
if form_list.filtering:
entity_filter = await ViewSetting.get_filter(
session=db_session,
user_id=user.id,
entity_name=type_.bot_entity_descriptor.class_name,
)
else:
entity_filter = None
list_all = EntityPermission.LIST_ALL in permissions
if list_all or EntityPermission.LIST in permissions:
if (
field_descriptor.ep_parent_field
and field_descriptor.ep_parent_field
and callback_data.entity_id
):
entity = await field_descriptor.entity_descriptor.type_.get(
session=db_session, id=callback_data.entity_id
)
value = getattr(entity, field_descriptor.ep_parent_field)
ext_filter = column(field_descriptor.ep_child_field).__eq__(value)
else:
ext_filter = None
if form_list.pagination:
items_count = await type_.get_count(
session=db_session,
static_filter=(
[
Filter(
field_name=f.field_name,
operator=f.operator,
value_type="const",
value=f.value,
)
for f in form_list.static_filters
if f.value_type == "const"
]
if isinstance(form_list.static_filters, list)
else form_list.static_filters
),
ext_filter=ext_filter,
filter=entity_filter,
filter_fields=form_list.filtering_fields,
user=user if not list_all else None,
)
total_pages = calc_total_pages(items_count, page_size)
page = min(page, total_pages)
skip = page_size * (page - 1)
limit = page_size
else:
skip = 0
limit = None
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)
session=db_session,
order_by=form_list.order_by,
static_filter=(
[
Filter(
field_name=f.field_name,
operator=f.operator,
value_type="const",
value=f.value,
)
for f in form_list.static_filters
if f.value_type == "const"
]
if isinstance(form_list.static_filters, list)
else form_list.static_filters
),
ext_filter=ext_filter,
filter=entity_filter,
user=user if not list_all else None,
skip=skip,
limit=limit,
)
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(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)
items = [
{
"text": f"{
''
if not is_list
else '【✔︎】 '
if item in (current_value or [])
else '【 】 '
}{
type_.bot_entity_descriptor.item_repr(
type_.bot_entity_descriptor, item
)
if type_.bot_entity_descriptor.item_repr
else get_callable_str(
type_.bot_entity_descriptor.full_name,
type_.bot_entity_descriptor,
item,
)
if type_.bot_entity_descriptor.full_name
else f'{type_.bot_entity_descriptor.name}: {str(item.id)}'
}",
"value": str(item.id),
}
for item in entity_items
]
keyboard_builder = InlineKeyboardBuilder()
for item in items:
keyboard_builder.row(
InlineKeyboardButton(text = item["text"],
callback_data = ContextData(
command = CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM if is_list else 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 = f"{page}&{item['value']}" if is_list else item["value"],
save_state = True).pack()))
add_pagination_controls(keyboard_builder = keyboard_builder,
callback_data = callback_data,
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)
InlineKeyboardButton(
text=item["text"],
callback_data=ContextData(
command=(
CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM
if is_list
else 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,
form_params=callback_data.form_params,
data=f"{page}&{item['value']}" if is_list else item["value"],
).pack(),
)
)
if form_list and form_list.pagination:
add_pagination_controls(
keyboard_builder=keyboard_builder,
callback_data=callback_data,
total_pages=total_pages,
command=CallbackCommand.ENTITY_PICKER_PAGE,
page=page,
)
if (
issubclass(type_, BotEntity)
and form_list.filtering
and form_list.filtering_fields
):
add_filter_controls(
keyboard_builder=keyboard_builder,
entity_descriptor=type_.bot_entity_descriptor,
filter=entity_filter,
)
if is_list:
keyboard_builder.row(
InlineKeyboardButton(text = await Settings.get(Settings.APP_STRINGS_DONE_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()))
InlineKeyboardButton(
text=await Settings.get(Settings.APP_STRINGS_DONE_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,
form_params=callback_data.form_params,
).pack(),
)
)
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 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)
await send_message(text = edit_prompt, reply_markup = keyboard_builder.as_markup())
await send_message(text=edit_prompt, reply_markup=keyboard_builder.as_markup())
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_PAGE))
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM))
async def entity_picker_callback(query: CallbackQuery,
callback_data: ContextData,
db_session: AsyncSession,
app: "QBotApp",
state: FSMContext,
**kwargs):
@router.callback_query(
ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_PAGE)
)
@router.callback_query(
ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM)
)
async def entity_picker_callback(
query: CallbackQuery,
callback_data: ContextData,
db_session: AsyncSession,
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)
field_descriptor = get_field_descriptor(app=app, callback_data=callback_data)
current_value = await deserialize(session = db_session, type_ = field_descriptor.type_, value = state_data["current_value"])
# current_value = await deserialize(session = db_session, type_ = field_descriptor.type_, value = state_data["current_value"])
edit_prompt = state_data["edit_prompt"]
value = await deserialize(session = db_session, type_ = field_descriptor.type_, value = state_data["value"])
value = await deserialize(
session=db_session, type_=field_descriptor.type_, value=state_data["value"]
)
if callback_data.command == CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM:
page, id_value = callback_data.data.split("&")
page = int(page)
type_ = field_descriptor.type_base
type_ = field_descriptor.type_base
if issubclass(type_, BotEnum):
item = type_(id_value)
if item in value:
@@ -208,7 +331,7 @@ async def entity_picker_callback(query: CallbackQuery,
else:
value.append(item)
else:
item = await type_.get(session = db_session, id = int(id_value))
item = await type_.get(session=db_session, id=int(id_value))
if item in value:
value.remove(item)
else:
@@ -221,16 +344,16 @@ async def entity_picker_callback(query: CallbackQuery,
page = int(callback_data.data)
else:
raise ValueError("Unsupported command")
await render_entity_picker(field_descriptor = field_descriptor,
message = query,
callback_data = callback_data,
current_value = value,
edit_prompt = edit_prompt,
db_session = db_session,
app = app,
state = state,
page = page,
**kwargs)
await render_entity_picker(
field_descriptor=field_descriptor,
message=query,
callback_data=callback_data,
current_value=value,
edit_prompt=edit_prompt,
db_session=db_session,
app=app,
state=state,
page=page,
**kwargs,
)

View File

@@ -0,0 +1,156 @@
from typing import TYPE_CHECKING
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....model import EntityPermission
from ....model.settings import Settings
from ....model.user import UserBase
from ....utils.main import (
check_entity_permission,
get_field_descriptor,
)
from ....utils.serialization import deserialize, serialize
from ..context import ContextData, CallbackCommand, CommandContext
from ....auth import authorize_command
from ..navigation import (
get_navigation_context,
save_navigation_context,
)
from ..forms.entity_form import entity_item
from .common import show_editor
from ..menu.parameters import parameters_menu
from .string import router as string_editor_router
from .date import router as date_picker_router
from .boolean import router as bool_editor_router
from .entity import router as entity_picker_router
if TYPE_CHECKING:
from ....main import QBotApp
logger = getLogger(__name__)
router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.FIELD_EDITOR))
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"]
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)
kwargs["state_data"] = state_data
entity_descriptor = None
if callback_data.context == CommandContext.SETTING_EDIT:
field_descriptor = get_field_descriptor(app, callback_data)
if field_descriptor.type_ is bool:
if await authorize_command(user=user, callback_data=callback_data):
await Settings.set_param(
field_descriptor, not await Settings.get(field_descriptor)
)
else:
return await message.answer(
text=(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
)
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, all_locales=True)
else:
field_descriptor = get_field_descriptor(app, callback_data)
entity_descriptor = field_descriptor.entity_descriptor
current_value = None
if (
field_descriptor.type_base is 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 check_entity_permission(
entity=entity, user=user, permission=EntityPermission.UPDATE
):
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 check_entity_permission(
entity=entity, user=user, permission=EntityPermission.READ
):
if entity:
form_name = (
callback_data.form_params.split("&")[0]
if callback_data.form_params
else "default"
)
form = entity_descriptor.forms.get(
form_name, entity_descriptor.default_form
)
entity_data = {
key: serialize(
getattr(entity, key),
entity_descriptor.fields_descriptors[key],
)
for key in (
form.edit_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, current_value=current_value, **kwargs)
router.include_routers(
string_editor_router,
date_picker_router,
bool_editor_router,
entity_picker_router,
)

View File

@@ -0,0 +1,284 @@
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
from sqlmodel.ext.asyncio.session import AsyncSession
from typing import TYPE_CHECKING
from decimal import Decimal
import json
from ..context import ContextData, CallbackCommand, CommandContext
from ...command_context_filter import CallbackCommandFilter
from ....model import EntityPermission
from ....model.user import UserBase
from ....model.settings import Settings
from ....model.descriptors import EntityFieldDescriptor
from ....model.language import LanguageBase
from ....auth import authorize_command
from ....utils.main import (
get_user_permissions,
check_entity_permission,
clear_state,
get_entity_descriptor,
get_field_descriptor,
)
from ....utils.serialization import deserialize
from ..common.routing import route_callback
from .common import show_editor
if TYPE_CHECKING:
from ....main import QBotApp
router = Router()
@router.message(CallbackCommandFilter(CallbackCommand.FIELD_EDITOR_CALLBACK))
@router.callback_query(
ContextData.filter(F.command == CallbackCommand.FIELD_EDITOR_CALLBACK)
)
async def field_editor_callback(message: Message | CallbackQuery, **kwargs):
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:
callback_data = ContextData.unpack(context_data)
value = message.text
field_descriptor = get_field_descriptor(app, callback_data)
type_base = field_descriptor.type_base
if type_base is str and field_descriptor.localizable:
locale_index = int(state_data.get("locale_index"))
value = state_data.get("value")
if value:
value = json.loads(value)
else:
value = {}
value[list(LanguageBase.all_members.keys())[locale_index]] = message.text
value = json.dumps(value, ensure_ascii=False)
if locale_index < len(LanguageBase.all_members.values()) - 1:
current_value = state_data.get("current_value")
state_data.update({"value": value})
entity_descriptor = get_entity_descriptor(app, callback_data)
kwargs.update({"callback_data": callback_data})
return await show_editor(
message=message,
locale_index=locale_index + 1,
field_descriptor=field_descriptor,
entity_descriptor=entity_descriptor,
current_value=current_value,
value=value,
**kwargs,
)
# else:
# value = state_data.get("value")
# if value:
# value = json.loads(value)
# else:
# value = {}
# value[list(LanguageBase.all_members.keys())[locale_index]] = (
# message.text
# )
# value = json.dumps(value, ensure_ascii=False)
elif type_base in [int, float, Decimal]:
try:
_ = type_base(value) # @IgnoreException
except Exception:
return await message.answer(
text=(await Settings.get(Settings.APP_STRINGS_INVALID_INPUT))
)
else:
callback_data: ContextData = kwargs["callback_data"]
if 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)
kwargs.update(
{
"callback_data": callback_data,
}
)
await process_field_edit_callback(
message=message, value=value, field_descriptor=field_descriptor, **kwargs
)
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_data: dict = kwargs["state_data"]
value = kwargs["value"]
field_descriptor: EntityFieldDescriptor = kwargs["field_descriptor"]
if callback_data.context == CommandContext.SETTING_EDIT:
if callback_data.data != "cancel":
if await authorize_command(user=user, callback_data=callback_data):
value = await deserialize(
session=db_session, type_=field_descriptor.type_, value=value
)
await Settings.set_param(field_descriptor, value)
else:
return await message.answer(
text=(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
)
return await route_callback(message=message, back=True, **kwargs)
elif callback_data.context in [
CommandContext.ENTITY_CREATE,
CommandContext.ENTITY_EDIT,
CommandContext.ENTITY_FIELD_EDIT,
]:
app: "QBotApp" = kwargs["app"]
entity_descriptor = get_entity_descriptor(app, callback_data)
form_name = (
callback_data.form_params.split("&")[0]
if callback_data.form_params
else "default"
)
form = entity_descriptor.forms.get(form_name, entity_descriptor.default_form)
field_sequence = form.edit_field_sequence
current_index = (
field_sequence.index(callback_data.field_name)
if callback_data.context
in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT]
else 0
)
entity_data = state_data.get("entity_data", {})
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
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
]
kwargs.update({"field_descriptor": next_field_descriptor})
callback_data.field_name = next_field_name
state_entity_val = entity_data.get(next_field_descriptor.field_name)
current_value = (
await deserialize(
session=db_session,
type_=next_field_descriptor.type_,
value=state_entity_val,
)
if state_entity_val
else None
)
await show_editor(
message=message,
entity_descriptor=entity_descriptor,
current_value=current_value,
**kwargs,
)
else:
entity_type = entity_descriptor.type_
entity_data[field_descriptor.field_name] = value
# What if user has several roles and each role has its own ownership field? Should we allow creation even
# if user has no CREATE_ALL permission
# for role in user.roles:
# if role in entity_descriptor.ownership_fields and not EntityPermission.CREATE_ALL in user_permissions:
# entity_data[entity_descriptor.ownership_fields[role]] = user.id
deser_entity_data = {
key: await deserialize(
session=db_session,
type_=entity_descriptor.fields_descriptors[key].type_,
value=value,
)
for key, value in entity_data.items()
}
if callback_data.context == CommandContext.ENTITY_CREATE:
user_permissions = get_user_permissions(user, entity_descriptor)
if (
EntityPermission.CREATE not in user_permissions
and EntityPermission.CREATE_ALL not in user_permissions
):
return await message.answer(
text=(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
)
new_entity = await entity_type.create(
session=db_session,
obj_in=entity_type(**deser_entity_data),
commit=True,
)
form_name = (
callback_data.form_params.split("&")[0]
if callback_data.form_params
else "default"
)
form_list = entity_descriptor.lists.get(
form_name or "default", entity_descriptor.default_list
)
state_data["navigation_context"] = ContextData(
command=CallbackCommand.ENTITY_ITEM,
entity_name=entity_descriptor.name,
form_params=form_list.item_form,
entity_id=str(new_entity.id),
).pack()
state_data.update(state_data)
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)
if not entity:
return await message.answer(
text=(await Settings.get(Settings.APP_STRINGS_NOT_FOUND))
)
if not check_entity_permission(
entity=entity, user=user, permission=EntityPermission.UPDATE
):
return await message.answer(
text=(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
)
for key, value in deser_entity_data.items():
setattr(entity, key, value)
await db_session.commit()
clear_state(state_data=state_data)
await route_callback(message=message, back=True, **kwargs)

View File

@@ -1,98 +1,97 @@
from types import UnionType
from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, CopyTextButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from typing import Any, get_args, get_origin
from typing import Any
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ....model.descriptors import EntityFieldDescriptor
from ....model.language import LanguageBase
from ....model.settings import Settings
from ....utils import serialize
from ....utils.main import get_send_message, get_local_text
from ....utils.serialization import serialize
from ..context import ContextData, CallbackCommand
from ..common import get_send_message, get_local_text
from .common import wrap_editor
from .wrapper import wrap_editor
logger = getLogger(__name__)
router = Router()
async def string_editor(message: Message | CallbackQuery,
field_descriptor: EntityFieldDescriptor,
callback_data: ContextData,
current_value: Any,
edit_prompt: str,
state: FSMContext,
locale_index: int = 0,
**kwargs):
async def string_editor(
message: Message | CallbackQuery,
field_descriptor: EntityFieldDescriptor,
callback_data: ContextData,
current_value: Any,
edit_prompt: str,
state: FSMContext,
locale_index: int = 0,
**kwargs,
):
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]
context_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,
form_params=callback_data.form_params,
)
if field_descriptor.type_base == str and field_descriptor.localizable:
if field_descriptor.type_base is str and field_descriptor.localizable:
current_locale = list(LanguageBase.all_members.values())[locale_index]
context_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)
_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) if current_value else None
state_data.update({
"context_data": context_data.pack(),
"edit_prompt": edit_prompt,
"locale_index": str(locale_index),
"current_value": current_value})
else:
context_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)
_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) if current_value else None
)
state_data.update(
{
"context_data": context_data.pack(),
"edit_prompt": edit_prompt,
"locale_index": str(locale_index),
"current_value": current_value,
}
)
else:
_current_value = serialize(current_value, field_descriptor)
state_data.update({
"context_data": context_data.pack()})
if _current_value:
state_data.update({"context_data": context_data.pack()})
_current_value_caption = f"{_current_value[:30]}..." if len(_current_value) > 30 else _current_value
keyboard_builder.row(InlineKeyboardButton(text = _current_value_caption,
copy_text = CopyTextButton(text = _current_value)))
if _current_value:
_current_value_caption = (
f"{_current_value[:30]}..." if len(_current_value) > 30 else _current_value
)
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,
callback_data = callback_data,
state_data = 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)
await send_message(text = _edit_prompt, reply_markup = keyboard_builder.as_markup())
# async def context_command_fiter(*args, **kwargs):
# print(args, kwargs)
# return True
send_message = get_send_message(message)
await send_message(text=_edit_prompt, reply_markup=keyboard_builder.as_markup())

View File

@@ -0,0 +1,93 @@
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from ....model.settings import Settings
from ....model.descriptors import EntityFieldDescriptor
from ..context import ContextData, CallbackCommand, CommandContext
from ..navigation import get_navigation_context, pop_navigation_context
async def wrap_editor(
keyboard_builder: InlineKeyboardBuilder,
field_descriptor: EntityFieldDescriptor,
callback_data: ContextData,
state_data: dict,
):
if callback_data.context in [
CommandContext.ENTITY_CREATE,
CommandContext.ENTITY_EDIT,
CommandContext.ENTITY_FIELD_EDIT,
]:
form_name = (
callback_data.form_params.split("&")[0]
if callback_data.form_params
else "default"
)
form = field_descriptor.entity_descriptor.forms.get(
form_name, field_descriptor.entity_descriptor.default_form
)
btns = []
field_index = (
form.edit_field_sequence.index(field_descriptor.name)
if callback_data.context
in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT]
else 0
)
stack, context = get_navigation_context(state_data=state_data)
context = pop_navigation_context(stack)
if field_index > 0:
btns.append(
InlineKeyboardButton(
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data=ContextData(
command=CallbackCommand.FIELD_EDITOR,
context=callback_data.context,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
form_params=callback_data.form_params,
field_name=form.edit_field_sequence[field_index - 1],
).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,
form_params=callback_data.form_params,
field_name=callback_data.field_name,
data="skip",
).pack(),
)
)
keyboard_builder.row(*btns)
keyboard_builder.row(
InlineKeyboardButton(
text=(await Settings.get(Settings.APP_STRINGS_CANCEL_BTN)),
callback_data=context.pack(),
)
)
elif callback_data.context == CommandContext.SETTING_EDIT:
keyboard_builder.row(
InlineKeyboardButton(
text=(await Settings.get(Settings.APP_STRINGS_CANCEL_BTN)),
callback_data=ContextData(
command=CallbackCommand.FIELD_EDITOR_CALLBACK,
context=callback_data.context,
field_name=callback_data.field_name,
form_params=callback_data.form_params,
data="cancel",
).pack(),
)
)

View File

@@ -1,24 +1,28 @@
from typing import get_args, get_origin, TYPE_CHECKING
from typing import TYPE_CHECKING
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.i18n import I18n
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....model.bot_entity import BotEntity
from ....model.bot_enum import BotEnum
from ....model.owned_bot_entity import OwnedBotEntity
from ....model.descriptors import FieldEditButton, CommandButton
from ....model.settings import Settings
from ....model.user import UserBase
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ....model import EntityPermission
from ....utils import serialize, deserialize, get_user_permissions
from ....utils.main import (
check_entity_permission,
get_send_message,
clear_state,
get_value_repr,
get_callable_str,
get_entity_descriptor,
)
from ..context import ContextData, CallbackCommand, CommandContext
from ..common import get_send_message, get_local_text, get_entity_descriptor, get_callable_str, get_value_repr
from ..navigation import (
pop_navigation_context,
save_navigation_context,
)
if TYPE_CHECKING:
from ....main import QBotApp
@@ -30,204 +34,212 @@ router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_ITEM))
async def entity_item_callback(query: CallbackQuery, **kwargs):
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, navigation_stack = stack, **kwargs)
clear_state(state_data=state_data)
stack = save_navigation_context(callback_data=callback_data, state_data=state_data)
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",
navigation_stack: list[ContextData],
**kwargs):
async def entity_item(
query: CallbackQuery,
callback_data: ContextData,
db_session: AsyncSession,
user: UserBase,
app: "QBotApp",
navigation_stack: list[ContextData],
**kwargs,
):
entity_descriptor = get_entity_descriptor(app, callback_data)
user_permissions = get_user_permissions(user, entity_descriptor)
entity_type: BotEntity = entity_descriptor.type_
# user_permissions = get_user_permissions(user, entity_descriptor)
entity_type = entity_descriptor.type_
keyboard_builder = InlineKeyboardBuilder()
entity_item = await entity_type.get(session = db_session, id = callback_data.entity_id)
entity_item = await entity_type.get(session=db_session, id=callback_data.entity_id)
if not entity_item:
return await query.answer(text = (await Settings.get(Settings.APP_STRINGS_NOT_FOUND)))
return await query.answer(
text=(await Settings.get(Settings.APP_STRINGS_NOT_FOUND))
)
is_owned = issubclass(entity_type, OwnedBotEntity)
# is_owned = issubclass(entity_type, OwnedBotEntity)
if (EntityPermission.READ not in user_permissions and
EntityPermission.READ_ALL not in user_permissions):
if not check_entity_permission(
entity=entity_item, user=user, permission=EntityPermission.READ
):
return await query.answer(
text=(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
)
return await query.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
can_edit = check_entity_permission(
entity=entity_item, user=user, permission=EntityPermission.UPDATE
)
if (is_owned and
EntityPermission.READ_ALL not in user_permissions and
entity_item.user_id != user.id):
return await query.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
can_edit = (EntityPermission.UPDATE_ALL in user_permissions or
(EntityPermission.UPDATE in user_permissions and not is_owned) or
(EntityPermission.UPDATE in user_permissions and is_owned and
entity_item.user_id == user.id))
can_delete = (EntityPermission.DELETE_ALL in user_permissions or
(EntityPermission.DELETE in user_permissions and not is_owned) or
(EntityPermission.DELETE in user_permissions and is_owned and
entity_item.user_id == user.id))
form = entity_descriptor.forms.get(
callback_data.form_params or "default", entity_descriptor.default_form
)
if can_edit:
for edit_buttons_row in entity_descriptor.edit_buttons:
for edit_buttons_row in form.form_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}"
for button in edit_buttons_row:
if isinstance(button, FieldEditButton):
field_name = button.field_name
btn_caption = button.caption
if field_name in entity_descriptor.fields_descriptors:
field_descriptor = entity_descriptor.fields_descriptors[
field_name
]
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:
btn_text = (f"✏️ {get_callable_str(field_descriptor.caption, field_descriptor, entity_item, field_value)}"
if field_descriptor.caption else f"✏️ {field_name}")
if field_descriptor.type_base is 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(),
)
)
elif isinstance(button, CommandButton):
btn_caption = button.caption
if btn_caption:
btn_text = get_callable_str(
btn_caption, entity_descriptor, entity_item
)
else:
btn_text = button.command
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()))
text=btn_text,
callback_data=(
button.context_data.pack()
if button.context_data
else ContextData(
command=CallbackCommand.USER_COMMAND,
user_command=button.command,
data=str(entity_item.id),
).pack()
),
)
)
if btn_row:
keyboard_builder.row(*btn_row)
edit_delete_row = []
if can_edit and entity_descriptor.edit_button_visible:
if can_edit and form.show_edit_button:
edit_delete_row.append(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_EDIT_BTN)),
callback_data = ContextData(
command = CallbackCommand.FIELD_EDITOR,
context = CommandContext.ENTITY_EDIT,
entity_name = entity_descriptor.name,
entity_id = str(entity_item.id),
field_name = entity_descriptor.field_sequence[0]).pack()))
if can_delete:
text=(await Settings.get(Settings.APP_STRINGS_EDIT_BTN)),
callback_data=ContextData(
command=CallbackCommand.FIELD_EDITOR,
context=CommandContext.ENTITY_EDIT,
entity_name=entity_descriptor.name,
entity_id=str(entity_item.id),
form_params=callback_data.form_params,
field_name=form.edit_field_sequence[0],
).pack(),
)
)
if (
check_entity_permission(
entity=entity_item, user=user, permission=EntityPermission.DELETE
)
and form.show_delete_button
):
edit_delete_row.append(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_DELETE_BTN)),
callback_data = ContextData(
command = CallbackCommand.ENTITY_DELETE,
entity_name = entity_descriptor.name,
entity_id = str(entity_item.id)).pack()))
text=(await Settings.get(Settings.APP_STRINGS_DELETE_BTN)),
callback_data=ContextData(
command=CallbackCommand.ENTITY_DELETE,
entity_name=entity_descriptor.name,
form_params=callback_data.form_params,
entity_id=str(entity_item.id),
).pack(),
)
)
if edit_delete_row:
keyboard_builder.row(*edit_delete_row)
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
if form.item_repr:
item_text = form.item_repr(entity_descriptor, entity_item)
else:
entity_caption = (
get_callable_str(
entity_descriptor.full_name, entity_descriptor, entity_item
)
if entity_descriptor.full_name
else entity_descriptor.name
)
item_text = f"<b><u><i>{entity_caption or entity_descriptor.name}:</i></u></b> <b>{entity_item_name}</b>"
entity_item_repr = (
get_callable_str(
entity_descriptor.item_repr, entity_descriptor, entity_item
)
if entity_descriptor.item_repr
else str(entity_item.id)
)
item_text = f"<b><u><i>{entity_caption or entity_descriptor.name}:</i></u></b> <b>{entity_item_repr}</b>"
for field_descriptor in entity_descriptor.fields_descriptors.values():
if field_descriptor.is_visible:
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,
)
item_text += f"\n{field_caption or field_descriptor.name}:{f' <b>{value}</b>' if value else ''}"
for field_descriptor in entity_descriptor.fields_descriptors.values():
if field_descriptor.name == "name" or not field_descriptor.is_visible:
continue
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)
item_text += f"\n{field_caption or field_descriptor.name}:{f" <b>{value}</b>" if value else ""}"
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
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())
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_DELETE))
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"]
entity_descriptor = get_entity_descriptor(app = app, callback_data = callback_data)
user_permissions = get_user_permissions(user, entity_descriptor)
entity = await entity_descriptor.type_.get(session = db_session, id = int(callback_data.entity_id))
if not (EntityPermission.DELETE_ALL in user_permissions or
(EntityPermission.DELETE in user_permissions and not issubclass(entity_descriptor.type_, OwnedBotEntity)) or
(EntityPermission.DELETE in user_permissions and issubclass(entity_descriptor.type_, OwnedBotEntity) and
entity.user_id == user.id)):
return await query.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
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)
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"]
entity = await entity_descriptor.type_.get(session = db_session, id = int(callback_data.entity_id))
return await query.message.edit_text(
text = (await Settings.get(Settings.APP_STRINGS_CONFIRM_DELETE_P_NAME)).format(
name = get_value_repr(
value = getattr(entity, field_descriptor.name),
field_descriptor = field_descriptor,
locale = user.lang)),
reply_markup = InlineKeyboardBuilder().row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_YES_BTN)),
callback_data = ContextData(
command = CallbackCommand.ENTITY_DELETE,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
data = "yes").pack()),
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_NO_BTN)),
callback_data = ContextData(
command = CallbackCommand.ENTITY_ITEM,
entity_name = callback_data.entity_name,
entity_id = callback_data.entity_id,
data = "no").pack())).as_markup())
from ..navigation import pop_navigation_context, save_navigation_context, clear_state, route_callback
await send_message(text=item_text, reply_markup=keyboard_builder.as_markup())

View File

@@ -0,0 +1,96 @@
from aiogram import Router, F
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlmodel.ext.asyncio.session import AsyncSession
from typing import TYPE_CHECKING
from ..context import ContextData, CallbackCommand
from ....model.user import UserBase
from ....model.settings import Settings
from ....model import EntityPermission
from ....utils.main import (
check_entity_permission,
get_value_repr,
get_entity_descriptor,
)
from ..common.routing import route_callback
if TYPE_CHECKING:
from ....main import QBotApp
router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_DELETE))
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"]
entity_descriptor = get_entity_descriptor(app=app, callback_data=callback_data)
entity = await entity_descriptor.type_.get(
session=db_session, id=int(callback_data.entity_id)
)
if not check_entity_permission(
entity=entity, user=user, permission=EntityPermission.DELETE
):
return await query.answer(
text=(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
)
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)
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"]
entity = await entity_descriptor.type_.get(
session=db_session, id=int(callback_data.entity_id)
)
return await query.message.edit_text(
text=(
await Settings.get(Settings.APP_STRINGS_CONFIRM_DELETE_P_NAME)
).format(
name=get_value_repr(
value=getattr(entity, field_descriptor.name),
field_descriptor=field_descriptor,
locale=user.lang,
)
),
reply_markup=InlineKeyboardBuilder()
.row(
InlineKeyboardButton(
text=(await Settings.get(Settings.APP_STRINGS_YES_BTN)),
callback_data=ContextData(
command=CallbackCommand.ENTITY_DELETE,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
form_params=callback_data.form_params,
data="yes",
).pack(),
),
InlineKeyboardButton(
text=(await Settings.get(Settings.APP_STRINGS_NO_BTN)),
callback_data=ContextData(
command=CallbackCommand.ENTITY_ITEM,
entity_name=callback_data.entity_name,
entity_id=callback_data.entity_id,
form_params=callback_data.form_params,
data="no",
).pack(),
),
)
.as_markup(),
)

View File

@@ -1,26 +1,29 @@
from typing import get_args, get_origin, TYPE_CHECKING
from typing import TYPE_CHECKING
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.i18n import I18n
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
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.descriptors import EntityDescriptor, Filter
from ....model import EntityPermission
from ....utils import serialize, deserialize, get_user_permissions
from ....utils.main import (
get_user_permissions,
get_send_message,
clear_state,
get_entity_descriptor,
get_callable_str,
)
from ....utils.serialization import deserialize
from ..context import ContextData, CallbackCommand, CommandContext
from ..common import (add_pagination_controls, get_local_text, get_entity_descriptor,
get_callable_str, get_send_message, add_filter_controls)
from ..common.pagination import add_pagination_controls
from ..common.filtering import add_filter_controls
from ..navigation import pop_navigation_context, save_navigation_context
if TYPE_CHECKING:
from ....main import QBotApp
@@ -32,135 +35,226 @@ router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_LIST))
async def entity_list_callback(query: CallbackQuery, **kwargs):
callback_data: ContextData = kwargs["callback_data"]
if callback_data.data == "skip":
return
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)
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)
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)
return max(items_count // page_size + (1 if items_count % page_size else 0), 1)
async def entity_list(message: CallbackQuery | Message,
callback_data: ContextData,
db_session: AsyncSession,
user: UserBase,
app: "QBotApp",
navigation_stack: list[ContextData],
**kwargs):
async def _prepare_static_filter(
db_session: AsyncSession,
entity_descriptor: EntityDescriptor,
static_filters: list[Filter],
params: list[str],
) -> list[Filter]:
return (
[
Filter(
field_name=f.field_name,
operator=f.operator,
value_type="const",
value=(
f.value
if f.value_type == "const"
else await deserialize(
session=db_session,
type_=entity_descriptor.fields_descriptors[
f.field_name
].type_base,
value=params[f.param_index],
)
),
)
for f in static_filters
]
if static_filters
else None
)
async def entity_list(
message: CallbackQuery | Message,
callback_data: ContextData,
db_session: AsyncSession,
user: UserBase,
app: "QBotApp",
navigation_stack: list[ContextData],
**kwargs,
):
page = int(callback_data.data or "1")
entity_descriptor = get_entity_descriptor(app, callback_data)
user_permissions = get_user_permissions(user, entity_descriptor)
entity_type = entity_descriptor.type_
form_params = (
callback_data.form_params.split("&") if callback_data.form_params else []
)
form_name = form_params.pop(0) if form_params else "default"
form_list = entity_descriptor.lists.get(
form_name or "default", entity_descriptor.default_list
)
form_item = entity_descriptor.forms.get(
form_list.item_form or "default", entity_descriptor.default_form
)
keyboard_builder = InlineKeyboardBuilder()
if EntityPermission.CREATE in user_permissions or EntityPermission.CREATE_ALL in user_permissions:
if (
EntityPermission.CREATE in user_permissions
or EntityPermission.CREATE_ALL in user_permissions
) and form_list.show_add_new_button:
keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_ADD_BTN)),
callback_data = ContextData(
command = CallbackCommand.FIELD_EDITOR,
context = CommandContext.ENTITY_CREATE,
entity_name = entity_descriptor.name,
field_name = entity_descriptor.field_sequence[0],
save_state = True).pack()))
text=(await Settings.get(Settings.APP_STRINGS_ADD_BTN)),
callback_data=ContextData(
command=CallbackCommand.FIELD_EDITOR,
context=CommandContext.ENTITY_CREATE,
entity_name=entity_descriptor.name,
field_name=form_item.edit_field_sequence[0],
form_params=form_list.item_form,
).pack(),
)
)
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)
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, 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, filter = entity_filter,
skip = page_size * (page - 1), limit = page_size)
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, filter = entity_filter,
skip = page_size * (page - 1), limit = page_size)
else:
items = list[BotEntity]()
total_pages = 1
page = 1
items_count = 0
if form_list.filtering:
entity_filter = await ViewSetting.get_filter(
session=db_session,
user_id=user.id,
entity_name=entity_descriptor.class_name,
)
else:
raise ValueError(f"Unsupported entity type: {entity_type}")
entity_filter = None
list_all = (
EntityPermission.LIST_ALL in user_permissions
or EntityPermission.READ_ALL in user_permissions
)
if (
list_all
or EntityPermission.LIST in user_permissions
or EntityPermission.READ in user_permissions
):
if form_list.pagination:
page_size = await Settings.get(Settings.PAGE_SIZE)
items_count = await entity_type.get_count(
session=db_session,
static_filter=await _prepare_static_filter(
db_session=db_session,
entity_descriptor=entity_descriptor,
static_filters=form_list.static_filters,
params=form_params,
),
filter=entity_filter,
filter_fields=form_list.filtering_fields,
user=user if not list_all else None,
)
total_pages = calc_total_pages(items_count, page_size)
page = min(page, total_pages)
skip = page_size * (page - 1)
limit = page_size
else:
skip = 0
limit = None
items = await entity_type.get_multi(
session=db_session,
order_by=form_list.order_by,
static_filter=await _prepare_static_filter(
db_session=db_session,
entity_descriptor=entity_descriptor,
static_filters=form_list.static_filters,
params=form_params,
),
filter=entity_filter,
filter_fields=form_list.filtering_fields,
user=user if not list_all else None,
skip=skip,
limit=limit,
)
else:
items = list[BotEntity]()
items_count = 0
total_pages = 1
page = 1
for item in items:
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)
if form_list.item_repr:
caption = form_list.item_repr(entity_descriptor, item)
elif entity_descriptor.item_repr:
caption = entity_descriptor.item_repr(entity_descriptor, item)
elif entity_descriptor.full_name:
caption = f"{
get_callable_str(
callable_str=entity_descriptor.full_name,
descriptor=entity_descriptor,
entity=item,
)
}: {item.id}"
else:
caption = item.name
caption = f"{entity_descriptor.name}: {item.id}"
keyboard_builder.row(
InlineKeyboardButton(
text = caption,
callback_data = ContextData(
command = CallbackCommand.ENTITY_ITEM,
entity_name = entity_descriptor.name,
entity_id = str(item.id)).pack()))
add_pagination_controls(keyboard_builder = keyboard_builder,
callback_data = callback_data,
total_pages = total_pages,
command = CallbackCommand.ENTITY_LIST,
page = page)
add_filter_controls(keyboard_builder = keyboard_builder,
entity_descriptor = entity_descriptor,
filter = entity_filter)
text=caption,
callback_data=ContextData(
command=CallbackCommand.ENTITY_ITEM,
entity_name=entity_descriptor.name,
form_params=form_list.item_form,
entity_id=str(item.id),
).pack(),
)
)
if form_list.pagination:
add_pagination_controls(
keyboard_builder=keyboard_builder,
callback_data=callback_data,
total_pages=total_pages,
command=CallbackCommand.ENTITY_LIST,
page=page,
)
if form_list.filtering and form_list.filtering_fields:
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(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
if entity_descriptor.caption:
entity_text = get_callable_str(entity_descriptor.caption_plural, entity_descriptor)
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data=context.pack(),
)
)
if form_list.caption:
entity_text = get_callable_str(form_list.caption, entity_descriptor)
else:
entity_text = entity_descriptor.name
if entity_descriptor.description:
entity_desciption = get_callable_str(entity_descriptor.description, entity_descriptor)
else:
entity_desciption = None
if entity_descriptor.full_name_plural:
entity_text = get_callable_str(
entity_descriptor.full_name_plural, entity_descriptor
)
else:
entity_text = entity_descriptor.name
if entity_descriptor.description:
entity_text = f"{entity_text} {get_callable_str(entity_descriptor.description, entity_descriptor)}"
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
@@ -168,8 +262,4 @@ async def entity_list(message: CallbackQuery | Message,
send_message = get_send_message(message)
await send_message(text = f"{entity_text}{f"\n{entity_desciption}" if entity_desciption else ""}",
reply_markup = keyboard_builder.as_markup())
from ..navigation import pop_navigation_context, save_navigation_context, clear_state
await send_message(text=entity_text, reply_markup=keyboard_builder.as_markup())

View File

@@ -4,13 +4,12 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from babel.support import LazyProxy
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
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 ....utils.main import get_send_message
from ....model.descriptors import EntityCaptionCallable
from ..navigation import save_navigation_context, pop_navigation_context
if TYPE_CHECKING:
from ....main import QBotApp
@@ -20,55 +19,63 @@ logger = getLogger(__name__)
router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_ENTITIES))
@router.callback_query(
ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_ENTITIES)
)
async def menu_entry_entities(message: CallbackQuery, **kwargs):
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)
stack = save_navigation_context(callback_data=callback_data, state_data=state_data)
await entities_menu(message = message, navigation_stack = stack, **kwargs)
await entities_menu(message=message, navigation_stack=stack, **kwargs)
async def entities_menu(message: Message | CallbackQuery,
app: "QBotApp",
state: FSMContext,
navigation_stack: list[ContextData],
**kwargs):
async def entities_menu(
message: Message | CallbackQuery,
app: "QBotApp",
state: FSMContext,
navigation_stack: list[ContextData],
**kwargs,
):
keyboard_builder = InlineKeyboardBuilder()
entity_metadata = app.entity_metadata
for entity in entity_metadata.entity_descriptors.values():
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}"
if entity.full_name_plural.__class__ == EntityCaptionCallable:
caption = entity.full_name_plural(entity) or entity.name
elif entity.full_name_plural.__class__ == LazyProxy:
caption = f"{f'{entity.icon} ' if entity.icon else ''}{entity.full_name_plural.value or entity.name}"
else:
caption = f"{f"{entity.icon} " if entity.icon else ""}{entity.caption_plural or entity.name}"
caption = f"{f'{entity.icon} ' if entity.icon else ''}{entity.full_name_plural or entity.name}"
keyboard_builder.row(
InlineKeyboardButton(
text = caption,
callback_data = ContextData(command = CallbackCommand.ENTITY_LIST, entity_name = entity.name).pack()))
text=caption,
callback_data=ContextData(
command=CallbackCommand.ENTITY_LIST, entity_name=entity.name
).pack(),
)
)
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
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)
await send_message(text = (await Settings.get(Settings.APP_STRINGS_REFERENCES)), reply_markup = keyboard_builder.as_markup())
from ..navigation import save_navigation_context, pop_navigation_context
await send_message(
text=(await Settings.get(Settings.APP_STRINGS_REFERENCES)),
reply_markup=keyboard_builder.as_markup(),
)

View File

@@ -1,64 +1,85 @@
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.types import (
Message,
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
)
from aiogram.fsm.context import FSMContext
from aiogram.utils.i18n import I18n
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ..navigation import pop_navigation_context, save_navigation_context
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 ..common import get_send_message
from ..common.routing import route_callback
from ....utils.main import get_send_message
logger = getLogger(__name__)
router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_LANGUAGE))
@router.callback_query(
ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_LANGUAGE)
)
async def menu_entry_language(message: CallbackQuery, **kwargs):
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)
stack = save_navigation_context(callback_data=callback_data, state_data=state_data)
await language_menu(message, navigation_stack = stack, **kwargs)
await language_menu(message, navigation_stack=stack, **kwargs)
async def language_menu(message: Message | CallbackQuery,
navigation_stack: list[ContextData],
user: UserBase,
**kwargs):
async def language_menu(
message: Message | CallbackQuery,
navigation_stack: list[ContextData],
user: UserBase,
**kwargs,
):
send_message = get_send_message(message)
inline_keyboard = [
[InlineKeyboardButton(text = locale.localized(user.lang),
callback_data = ContextData(command = CallbackCommand.SET_LANGUAGE,
data = str(locale)).pack())]
for locale in LanguageBase.all_members.values()]
[
InlineKeyboardButton(
text=locale.localized(user.lang),
callback_data=ContextData(
command=CallbackCommand.SET_LANGUAGE, data=str(locale)
).pack(),
)
]
for locale in LanguageBase.all_members.values()
]
context = pop_navigation_context(navigation_stack)
if context:
inline_keyboard.append([InlineKeyboardButton(text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack())])
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))
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, **kwargs):
user: UserBase = kwargs["user"]
callback_data: ContextData = kwargs["callback_data"]
db_session: AsyncSession = kwargs["db_session"]
@@ -73,6 +94,3 @@ async def set_language(message: CallbackQuery, **kwargs):
i18n: I18n = kwargs["i18n"]
with i18n.use_locale(user.lang):
await route_callback(message, **kwargs)
from ..navigation import pop_navigation_context, save_navigation_context

View File

@@ -1,93 +1,88 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
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 ....model.settings import Settings
from ....model.user import UserBase
from ..context import ContextData, CallbackCommand
from ..common import get_send_message
from ....utils.main import get_send_message
from ..navigation import save_navigation_context, pop_navigation_context
import qbot.bot.handlers.menu.entities as entities
import qbot.bot.handlers.menu.settings as settings
import qbot.bot.handlers.menu.parameters as parameters
import qbot.bot.handlers.menu.language as language
import qbot.bot.handlers.editors.main as editor
import qbot.bot.handlers.editors.main_callbacks as editor_callbacks
import qbot.bot.handlers.forms.entity_list as entity_list
import qbot.bot.handlers.forms.entity_form as entity_form
import qbot.bot.handlers.forms.entity_form_callbacks as entity_form_callbacks
import qbot.bot.handlers.common.filtering_callbacks as filtering_callbacks
import qbot.bot.handlers.user_handlers as user_handlers
logger = getLogger(__name__)
router = Router()
# @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 main_menu(message, **kwargs)
# @router.callback_query(CallbackData.filter(F.command == CallbackCommand.MENU_ENTRY))
# async def menu_entry(query: CallbackQuery, callback_data: CallbackData, user: UserBase, db_session: AsyncSession, app: QBotApp):
# pass
@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_MAIN))
async def menu_entry_main(message: CallbackQuery, **kwargs):
async def menu_entry_main(message: CallbackQuery, **kwargs):
stack = await save_navigation_context(
callback_data=kwargs["callback_data"], state=kwargs["state"]
)
stack = await save_navigation_context(callback_data = kwargs["callback_data"], state = kwargs["state"])
await main_menu(message, navigation_stack = stack, **kwargs)
await main_menu(message, navigation_stack=stack, **kwargs)
async def main_menu(message: Message | CallbackQuery, navigation_stack: list[ContextData], **kwargs):
async def main_menu(
message: Message | CallbackQuery, navigation_stack: list[ContextData], **kwargs
):
keyboard_builder = InlineKeyboardBuilder()
keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_REFERENCES_BTN)),
callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_ENTITIES).pack()))
keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_SETTINGS_BTN)),
callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_SETTINGS).pack()))
text=(await Settings.get(Settings.APP_STRINGS_REFERENCES_BTN)),
callback_data=ContextData(
command=CallbackCommand.MENU_ENTRY_ENTITIES
).pack(),
)
)
keyboard_builder.row(
InlineKeyboardButton(
text=(await Settings.get(Settings.APP_STRINGS_SETTINGS_BTN)),
callback_data=ContextData(
command=CallbackCommand.MENU_ENTRY_SETTINGS
).pack(),
)
)
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data=context.pack(),
)
)
send_message = get_send_message(message)
await send_message(text = (await Settings.get(Settings.APP_STRINGS_MAIN_NENU)),
reply_markup = keyboard_builder.as_markup())
await send_message(
text=(await Settings.get(Settings.APP_STRINGS_MAIN_NENU)),
reply_markup=keyboard_builder.as_markup(),
)
from .entities import router as entities_router
from .settings import router as settings_router
from .parameters import router as parameters_router
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,
settings_router,
parameters_router,
language_router,
editors_router,
entity_list_router,
entity_form_router,
common_router,
user_handlers_router
entities.router,
settings.router,
parameters.router,
language.router,
editor.router,
editor_callbacks.router,
entity_list.router,
entity_form.router,
entity_form_callbacks.router,
filtering_callbacks.router,
user_handlers.router,
)
from ..navigation import save_navigation_context, pop_navigation_context, clear_state

View File

@@ -1,79 +1,94 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.i18n import I18n
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....model.settings import Settings
from ....model.user import UserBase
from ..context import ContextData, CallbackCommand, CommandContext
from ....utils.main import (
get_send_message,
clear_state,
get_value_repr,
get_callable_str,
)
from ..navigation import save_navigation_context, pop_navigation_context
from ....auth import authorize_command
logger = getLogger(__name__)
router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_PARAMETERS))
@router.callback_query(
ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_PARAMETERS)
)
async def menu_entry_parameters(message: CallbackQuery, **kwargs):
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)
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)
await parameters_menu(message=message, navigation_stack=stack, **kwargs)
async def parameters_menu(message: Message | CallbackQuery,
user: UserBase,
callback_data: ContextData,
navigation_stack: list[ContextData],
**kwargs):
if not await authorize_command(user = user, callback_data = callback_data):
async def parameters_menu(
message: Message | CallbackQuery,
user: UserBase,
callback_data: ContextData,
navigation_stack: list[ContextData],
**kwargs,
):
if not await authorize_command(user=user, callback_data=callback_data):
await message.answer(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
settings = await Settings.get_params()
keyboard_builder = InlineKeyboardBuilder()
for key, value in settings.items():
if not key.is_visible:
continue
if key.caption_value:
caption = get_callable_str(callable_str = key.caption_value, descriptor = key, entity = None, value = value)
caption = get_callable_str(
callable_str=key.caption_value, descriptor=key, entity=None, value=value
)
else:
if key.caption:
caption = get_callable_str(callable_str = key.caption, descriptor = key, entity = None, value = value)
caption = get_callable_str(
callable_str=key.caption, descriptor=key, entity=None, value=value
)
else:
caption = key.name
if key.type_ == bool:
caption = f"{"【✔︎】" if value else "【 】"} {caption}"
else:
caption = f"{caption}: {get_value_repr(value = value, field_descriptor = key, locale = user.lang)}"
caption = key.name
if key.type_ is bool:
caption = f"{'【✔︎】' if value else '【 】'} {caption}"
else:
caption = f"{caption}: {get_value_repr(value=value, field_descriptor=key, locale=user.lang)}"
keyboard_builder.row(
InlineKeyboardButton(
text=caption,
callback_data=ContextData(
command=CallbackCommand.FIELD_EDITOR,
context=CommandContext.SETTING_EDIT,
field_name=key.name,
).pack(),
)
)
keyboard_builder.row(InlineKeyboardButton(text = caption,
callback_data = ContextData(
command = CallbackCommand.FIELD_EDITOR,
context = CommandContext.SETTING_EDIT,
field_name = key.name).pack()))
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(InlineKeyboardButton(text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
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"]
@@ -81,8 +96,7 @@ async def parameters_menu(message: Message | CallbackQuery,
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 ..common import get_send_message, get_value_repr, get_callable_str, authorize_command
await send_message(
text=(await Settings.get(Settings.APP_STRINGS_PARAMETERS)),
reply_markup=keyboard_builder.as_markup(),
)

View File

@@ -1,68 +1,80 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
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 ....model.settings import Settings
from ....model.user import UserBase
from ....utils.main import get_send_message
from ..context import ContextData, CallbackCommand
from ..common import get_send_message, authorize_command
from ....auth import authorize_command
from ..navigation import save_navigation_context, pop_navigation_context
logger = getLogger(__name__)
router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_SETTINGS))
@router.callback_query(
ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_SETTINGS)
)
async def menu_entry_settings(message: CallbackQuery, **kwargs):
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)
stack = save_navigation_context(callback_data=callback_data, state_data=state_data)
await settings_menu(message, navigation_stack = stack, **kwargs)
await settings_menu(message, navigation_stack=stack, **kwargs)
async def settings_menu(message: Message | CallbackQuery,
user: UserBase,
navigation_stack: list[ContextData],
**kwargs):
async def settings_menu(
message: Message | CallbackQuery,
user: UserBase,
navigation_stack: list[ContextData],
**kwargs,
):
keyboard_builder = InlineKeyboardBuilder()
if await authorize_command(user = user, callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_PARAMETERS)):
if await authorize_command(
user=user,
callback_data=ContextData(command=CallbackCommand.MENU_ENTRY_PARAMETERS),
):
keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_PARAMETERS_BTN)),
callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_PARAMETERS).pack()))
text=(await Settings.get(Settings.APP_STRINGS_PARAMETERS_BTN)),
callback_data=ContextData(
command=CallbackCommand.MENU_ENTRY_PARAMETERS
).pack(),
)
)
keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_LANGUAGE_BTN)),
callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_LANGUAGE).pack()))
text=(await Settings.get(Settings.APP_STRINGS_LANGUAGE_BTN)),
callback_data=ContextData(
command=CallbackCommand.MENU_ENTRY_LANGUAGE
).pack(),
)
)
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(
InlineKeyboardButton(
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data = context.pack()))
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())
from ..navigation import pop_navigation_context, get_navigation_context
await send_message(
text=(await Settings.get(Settings.APP_STRINGS_SETTINGS)),
reply_markup=keyboard_builder.as_markup(),
)

View File

@@ -1,21 +1,25 @@
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
from .context import ContextData, CallbackCommand
def save_navigation_context(callback_data: ContextData, state_data: dict) -> list[ContextData]:
stack = [ContextData.unpack(item) for item in state_data.get("navigation_stack", [])]
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
callback_data.back = False
if stack:
stack.pop()
else:
if (stack and navigation_context and
navigation_context.command == callback_data.command and
navigation_context.command != CallbackCommand.USER_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)
@@ -31,65 +35,14 @@ def pop_navigation_context(stack: list[ContextData]) -> ContextData | None:
data = stack[-1]
data.back = True
return data
def get_navigation_context(state_data: dict) -> tuple[list[ContextData], ContextData | None]:
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 state_data.get("navigation_stack", [])],
context)
def clear_state(state_data: dict, clear_nav: bool = False):
if clear_nav:
state_data.clear()
else:
stack = state_data.get("navigation_stack")
context = state_data.get("navigation_context")
state_data.clear()
if stack:
state_data["navigation_stack"] = stack
if context:
state_data["navigation_context"] = context
async def route_callback(message: Message | CallbackQuery, back: bool = True, **kwargs):
state_data = kwargs["state_data"]
stack, context = get_navigation_context(state_data)
if back:
context = pop_navigation_context(stack)
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:
await main_menu(message, **kwargs)
elif context.command == CallbackCommand.MENU_ENTRY_SETTINGS:
await settings_menu(message, **kwargs)
elif context.command == CallbackCommand.MENU_ENTRY_PARAMETERS:
await parameters_menu(message, **kwargs)
elif context.command == CallbackCommand.MENU_ENTRY_LANGUAGE:
await language_menu(message, **kwargs)
elif context.command == CallbackCommand.MENU_ENTRY_ENTITIES:
await entities_menu(message, **kwargs)
elif context.command == CallbackCommand.ENTITY_LIST:
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}")
else:
raise ValueError("No navigation context")
from .menu.main import main_menu
from .menu.settings import settings_menu
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 .editors import field_editor
return (
[ContextData.unpack(item) for item in state_data.get("navigation_stack", [])],
context,
)

View File

@@ -8,7 +8,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
from ...main import QBotApp
from ...model.settings import Settings
from ...model.language import LanguageBase
from .navigation import clear_state
from ...utils.main import clear_state
logger = getLogger(__name__)
@@ -16,40 +16,56 @@ router = Router()
@router.message(CommandStart())
async def start(message: Message, db_session: AsyncSession, app: QBotApp, state: FSMContext):
async def start(
message: Message, db_session: AsyncSession, app: QBotApp, state: FSMContext
):
state_data = await state.get_data()
clear_state(state_data = state_data, clear_nav = True)
clear_state(state_data=state_data, clear_nav=True)
User = app.user_class
user = await User.get(session = db_session, id = message.from_user.id)
user = await User.get(session=db_session, id=message.from_user.id)
if not user:
msg_text = (await Settings.get(Settings.APP_STRINGS_WELCOME_P_NAME)).format(name = message.from_user.full_name)
msg_text = (await Settings.get(Settings.APP_STRINGS_WELCOME_P_NAME)).format(
name=message.from_user.full_name
)
try:
if message.from_user.language_code in [item.value for item in LanguageBase.all_members.values()]:
if message.from_user.language_code in [
item.value for item in LanguageBase.all_members.values()
]:
lang = LanguageBase(message.from_user.language_code)
user = await User.create(session = db_session,
obj_in = User(
id = message.from_user.id,
name = message.from_user.full_name,
lang = lang,
is_active = True),
commit = True)
user = await User.create(
session=db_session,
obj_in=User(
id=message.from_user.id,
name=message.from_user.full_name,
lang=lang,
is_active=True,
),
commit=True,
)
except Exception as e:
logger.error("Error creating user", exc_info = True)
message.answer((await Settings.get(Settings.APP_STRINGS_INTERNAL_ERROR_P_ERROR)).format(error = str(e)))
logger.error("Error creating user", exc_info=True)
message.answer(
(
await Settings.get(Settings.APP_STRINGS_INTERNAL_ERROR_P_ERROR)
).format(error=str(e))
)
return
else:
if user.is_active:
msg_text = (await Settings.get(Settings.APP_STRINGS_GREETING_P_NAME)).format(name = user.name)
msg_text = (
await Settings.get(Settings.APP_STRINGS_GREETING_P_NAME)
).format(name=user.name)
else:
msg_text = (await Settings.get(Settings.APP_STRINGS_USER_BLOCKED_P_NAME)).format(name = user.name)
msg_text = (
await Settings.get(Settings.APP_STRINGS_USER_BLOCKED_P_NAME)
).format(name=user.name)
await message.answer(msg_text)

View File

@@ -1,14 +1,16 @@
from dataclasses import dataclass, field
from typing import Any, Callable, TYPE_CHECKING
from typing import 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
from ....utils.main import get_send_message, clear_state
from ....model.descriptors import CommandCallbackContext
from ..navigation import (
save_navigation_context,
get_navigation_context,
pop_navigation_context,
)
if TYPE_CHECKING:
@@ -18,51 +20,22 @@ if TYPE_CHECKING:
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)
callback_data = ContextData(
command=CallbackCommand.USER_COMMAND, user_command=str_command
)
await command_handler(message = message, callback_data = callback_data, **kwargs)
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)
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")
@@ -70,45 +43,48 @@ async def command_handler(message: Message | CallbackQuery, **kwargs):
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)
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)
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)
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)
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()))
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data=back_callback_data.pack(),
)
)
send_message = get_send_message(message)
@@ -116,10 +92,11 @@ async def command_handler(message: Message | CallbackQuery, **kwargs):
message = message.message
if callback_context.message_text:
await send_message(text = callback_context.message_text,
reply_markup = callback_context.keyboard_builder.as_markup())
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
await message.edit_reply_markup(
reply_markup=callback_context.keyboard_builder.as_markup()
)