add options functionality for input editors
All checks were successful
Build Docs / changes (push) Successful in 25s
Build Docs / build-docs (push) Has been skipped
Build Docs / deploy-docs (push) Has been skipped

This commit is contained in:
Alexander Kalinovsky
2025-06-10 23:31:31 +03:00
parent 923b0e6cc9
commit 4ac80e0105
12 changed files with 178 additions and 80 deletions

View File

@@ -119,6 +119,7 @@ async def show_editor(message: Message | CallbackQuery, **kwargs):
)
).format(name=caption_str)
kwargs["entity_data"] = entity_data
kwargs["edit_prompt"] = edit_prompt
if value_type not in [int, float, Decimal, str]:

View File

@@ -103,6 +103,8 @@ async def render_entity_picker(
message=message,
)
entity = None
if issubclass(type_, BotEnum):
items_count = len(type_.all_members)
total_pages = calc_total_pages(items_count, page_size)
@@ -112,7 +114,6 @@ async def render_entity_picker(
page_size * (page - 1) : page_size * page
]
elif callable(field_descriptor.options):
entity = None
if callback_data.entity_id:
entity = await field_descriptor.entity_descriptor.type_.get(
session=db_session, id=callback_data.entity_id
@@ -126,13 +127,25 @@ async def render_entity_picker(
enum_items = list(type_.all_members.values())[
page_size * (page - 1) : page_size * page
]
items = [
items = []
for it_row in enum_items:
if not isinstance(it_row, list):
it_row = [it_row]
item_row = []
for item in it_row:
if isinstance(item, tuple):
item_value, item_text = item
else:
item_value = item
item_text = item.localized(user.lang)
item_row.append(
{
"text": f"{'' if not is_list else '【✔︎】 ' if item in (current_value or []) else ' '}{item.localized(user.lang)}",
"value": item.value,
"text": f"{'' if not is_list else '[✓] ' if item_value in (current_value or []) else '[ ] '}{item_text}",
"value": item_value.value,
}
for item in enum_items
]
)
items.append(item_row)
elif issubclass(type_, BotEntity):
ep_form = "default"
ep_form_params = []
@@ -224,13 +237,14 @@ async def render_entity_picker(
entity_items = list[BotEntity]()
items = [
[
{
"text": f"{
''
if not is_list
else '【✔︎】 '
else '[✓] '
if item in (current_value or [])
else ' '
else '[ ] '
}{
await get_callable_str(
callable_str=type_.bot_entity_descriptor.item_repr,
@@ -248,13 +262,16 @@ async def render_entity_picker(
}",
"value": str(item.id),
}
]
for item in entity_items
]
keyboard_builder = InlineKeyboardBuilder()
for item in items:
keyboard_builder.row(
for item_row in items:
btn_row = []
for item in item_row:
btn_row.append(
InlineKeyboardButton(
text=item["text"],
callback_data=ContextData(
@@ -273,6 +290,7 @@ async def render_entity_picker(
).pack(),
)
)
keyboard_builder.row(*btn_row)
if form_list and form_list.pagination:
add_pagination_controls(
@@ -321,6 +339,7 @@ async def render_entity_picker(
state_data=state_data,
user=user,
context=context,
entity=entity,
)
await state.set_data(state_data)

View File

@@ -261,7 +261,9 @@ async def field_editor(message: Message | CallbackQuery, **kwargs):
async def delete_message_callback(query: CallbackQuery, **kwargs):
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
clear_state(state_data=state_data)
context_data: ContextData = kwargs["callback_data"]
clear_nav = context_data.data == "clear_nav"
clear_state(state_data=state_data, clear_nav=clear_nav)
await state.set_data(state_data)
await query.message.delete()

View File

@@ -67,18 +67,23 @@ async def _validate_value(
)
async def field_editor_callback(message: Message | CallbackQuery, **kwargs):
app: "QBotApp" = kwargs["app"]
state: FSMContext = kwargs["state"]
callback_data: ContextData = kwargs.get("callback_data", None)
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)
if not field_descriptor.options_custom_value:
return
value = message.text
type_base = field_descriptor.type_base
if type_base in [int, float, Decimal]:
@@ -132,21 +137,23 @@ async def field_editor_callback(message: Message | CallbackQuery, **kwargs):
current_value = state_data.get("current_value")
state_data.update({"value": value})
entity_descriptor = get_entity_descriptor(app, callback_data)
# entity_descriptor = field_descriptor.entity_descriptor
# 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,
# entity_descriptor=entity_descriptor,
current_value=current_value,
value=value,
**kwargs,
)
else:
callback_data: ContextData = kwargs["callback_data"]
field_descriptor = get_field_descriptor(app, callback_data)
if callback_data.data:
if callback_data.data == "skip":
value = None
@@ -154,7 +161,6 @@ async def field_editor_callback(message: Message | CallbackQuery, **kwargs):
value = callback_data.data
else:
value = state_data.get("value")
field_descriptor = get_field_descriptor(app, callback_data)
kwargs.update(
{

View File

@@ -74,7 +74,11 @@ async def string_editor(
state_data.update({"context_data": context_data.pack()})
if _current_value:
if (
_current_value
and field_descriptor.show_current_value_button
and field_descriptor.options_custom_value
):
_current_value_caption = (
f"{_current_value[:30]}..." if len(_current_value) > 30 else _current_value
)
@@ -102,6 +106,7 @@ async def string_editor(
state_data=state_data,
user=user,
context=context,
entity=kwargs.get("entity_data"),
)
await state.set_data(state_data)

View File

@@ -1,12 +1,17 @@
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from inspect import iscoroutinefunction
from typing import Any
from ....model.bot_entity import BotEntity
from ....model.bot_enum import BotEnum
from ....model.settings import Settings
from ....model.descriptors import BotContext, EntityForm, FieldDescriptor
from ....model.user import UserBase
from ..context import ContextData, CallbackCommand, CommandContext
from ....utils.navigation import get_navigation_context, pop_navigation_context
from ....utils.main import build_field_sequence
from ....utils.main import build_field_sequence, get_value_repr
from ....utils.serialization import serialize
async def wrap_editor(
@@ -16,6 +21,7 @@ async def wrap_editor(
state_data: dict,
user: UserBase,
context: BotContext,
entity: BotEntity | Any = None,
):
if callback_data.context in [
CommandContext.ENTITY_CREATE,
@@ -23,9 +29,9 @@ async def wrap_editor(
CommandContext.ENTITY_FIELD_EDIT,
CommandContext.COMMAND_FORM,
]:
btns = []
show_back = True
show_cancel = True
if callback_data.context == CommandContext.COMMAND_FORM:
field_sequence = list(field_descriptor.command.param_form.keys())
field_index = field_sequence.index(callback_data.field_name)
@@ -57,8 +63,55 @@ async def wrap_editor(
else 0
)
stack, context = get_navigation_context(state_data=state_data)
context = pop_navigation_context(stack)
stack, navigation_context = get_navigation_context(state_data=state_data)
navigation_context = pop_navigation_context(stack)
if not issubclass(field_descriptor.type_base, BotEnum):
options = []
if field_descriptor.options:
if isinstance(field_descriptor.options, list):
options = field_descriptor.options
elif callable(field_descriptor.options):
if iscoroutinefunction(field_descriptor.options):
options = await field_descriptor.options(entity, context)
else:
options = field_descriptor.options(entity, context)
for option_row in options:
btns_row = []
for option in option_row:
if isinstance(option, tuple):
value = option[0]
caption = option[1]
else:
value = option
caption = await get_value_repr(
value=value,
field_descriptor=field_descriptor,
context=context,
)
btns_row.append(
InlineKeyboardButton(
text=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,
user_command=callback_data.user_command,
data=serialize(
value=value,
field_descriptor=field_descriptor,
),
).pack(),
)
)
keyboard_builder.row(*btns_row)
btns = []
if field_index > 0 and show_back:
btns.append(
@@ -102,9 +155,11 @@ async def wrap_editor(
keyboard_builder.row(
InlineKeyboardButton(
text=(await Settings.get(Settings.APP_STRINGS_CANCEL_BTN)),
callback_data=context.pack()
if context
else ContextData(command=CallbackCommand.DELETE_MESSAGE).pack(),
callback_data=navigation_context.pack()
if navigation_context
else ContextData(
command=CallbackCommand.DELETE_MESSAGE, data="clear_nav"
).pack(),
)
)

View File

@@ -144,7 +144,7 @@ async def entity_item(
)
else:
if field_descriptor.type_base is bool:
btn_text = f"{'【✔︎】 ' if field_value else ' '}{
btn_text = f"{'[✓] ' if field_value else '[ ] '}{
await get_callable_str(
callable_str=field_descriptor.caption,
context=context,

View File

@@ -240,12 +240,12 @@ async def entity_list(
filtering_fields=form_list.filtering_fields,
)
context = pop_navigation_context(navigation_stack)
if context:
navigation_context = pop_navigation_context(navigation_stack)
if navigation_context:
keyboard_builder.row(
InlineKeyboardButton(
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
callback_data=context.pack(),
callback_data=navigation_context.pack(),
)
)

View File

@@ -81,7 +81,7 @@ async def parameters_menu(
caption = key.name
if key.type_ is bool:
caption = f"{'【✔︎】' if value else ' '} {caption}"
caption = f"{'[✓]' if value else '[ ]'} {caption}"
else:
caption = f"{caption}: {await get_value_repr(value=value, field_descriptor=key, context=context, locale=user.lang)}"

View File

@@ -103,7 +103,9 @@ class QBotApp(Generic[UserType, ConfigType], FastAPI):
self.bot = Bot(
token=self.config.TELEGRAM_BOT_TOKEN,
session=session,
default=DefaultBotProperties(parse_mode="HTML"),
default=DefaultBotProperties(
parse_mode="HTML", link_preview_is_disabled=True
),
)
dp = Dispatcher(storage=DbStorage())

View File

@@ -125,7 +125,13 @@ class _BaseFieldDescriptor[T: "BotEntity"]:
ep_parent_field: str | None = None
ep_child_field: str | None = None
dt_type: Literal["date", "datetime"] = "date"
options: list[Any] | Callable[[T, "BotContext"], list[Any]] | None = None
options: (
list[list[Union[Any, tuple[Any, str]]]]
| Callable[[T, "BotContext"], list[list[Union[Any, tuple[Any, str]]]]]
| None
) = None
options_custom_value: bool = True
show_current_value_button: bool = True
show_skip_in_editor: Literal[False, "Auto"] = "Auto"
default: Any = None
default_factory: Callable[[], Any] | None = None

View File

@@ -19,6 +19,7 @@ from ..model.descriptors import (
EntityDescriptor,
EntityPermission,
_BaseFieldDescriptor,
_BaseEntityDescriptor,
Filter,
)
@@ -76,11 +77,11 @@ def check_entity_permission(
if perm_mapping[permission] in permissions:
return True
ownership_filds = entity_descriptor.ownership_fields
ownership_fields = entity_descriptor.ownership_fields
for role in user.roles:
if role in ownership_filds:
if getattr(entity, ownership_filds[role]) == user.id:
if role in ownership_fields:
if getattr(entity, ownership_fields[role]) == user.id:
return True
else:
if permission in permissions:
@@ -148,7 +149,7 @@ async def get_value_repr(
type_ = field_descriptor.type_base
if isinstance(value, bool):
return "【✔︎】" if value else "【 】"
return "[✓]" if value else "[ ]"
elif field_descriptor.is_list:
if issubclass(type_, BotEntity):
return f"[{
@@ -204,8 +205,9 @@ async def get_callable_str(
return await callable_str(descriptor, entity, context)
else:
param = args[next(iter(args))]
if not isinstance(param.annotation, str) and issubclass(
param.annotation, _BaseFieldDescriptor
if not isinstance(param.annotation, str) and (
issubclass(param.annotation, _BaseFieldDescriptor)
or issubclass(param.annotation, _BaseEntityDescriptor)
):
return await callable_str(descriptor, context)
else: