enhanced strings and elements visibility delegates
All checks were successful
Build Docs / changes (push) Successful in 27s
Build Docs / build-docs (push) Has been skipped
Build Docs / deploy-docs (push) Has been skipped

This commit is contained in:
Alexander Kalinovsky
2025-04-01 00:19:51 +07:00
parent 6a7355996c
commit e10d7ff7bf
15 changed files with 458 additions and 206 deletions

View File

@@ -15,7 +15,7 @@ from .model.descriptors import (
Filter as Filter, Filter as Filter,
EntityPermission as EntityPermission, EntityPermission as EntityPermission,
CommandCallbackContext as CommandCallbackContext, CommandCallbackContext as CommandCallbackContext,
EntityEventContext as EntityEventContext, BotContext as BotContext,
CommandButton as CommandButton, CommandButton as CommandButton,
FieldEditButton as FieldEditButton, FieldEditButton as FieldEditButton,
InlineButton as InlineButton, InlineButton as InlineButton,

View File

@@ -30,14 +30,12 @@ async def telegram_webhook(
logger.error("Invalid request", exc_info=True) logger.error("Invalid request", exc_info=True)
return Response(status_code=400) return Response(status_code=400)
try: try:
state_kw = request.state._state # TODO: avoid accessing private attribute
await app.dp.feed_webhook_update( await app.dp.feed_webhook_update(
app.bot, app.bot,
update, update,
db_session=db_session, db_session=db_session,
app=app, app=app,
**(state_kw or {}), app_state=request.state,
) )
except Exception: except Exception:
logger.error("Error processing update", exc_info=True) logger.error("Error processing update", exc_info=True)

View File

@@ -1,7 +1,7 @@
from aiogram.types import InlineKeyboardButton from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from ....model.descriptors import EntityDescriptor from ....model.descriptors import BotContext, EntityDescriptor
from ....utils.main import get_callable_str from ....utils.main import get_callable_str
from ..context import ContextData, CallbackCommand from ..context import ContextData, CallbackCommand
@@ -9,6 +9,7 @@ from ..context import ContextData, CallbackCommand
async def add_filter_controls( async def add_filter_controls(
keyboard_builder: InlineKeyboardBuilder, keyboard_builder: InlineKeyboardBuilder,
entity_descriptor: EntityDescriptor, entity_descriptor: EntityDescriptor,
context: BotContext,
filter: str = None, filter: str = None,
filtering_fields: list[str] = None, filtering_fields: list[str] = None,
page: int = 1, page: int = 1,
@@ -16,8 +17,9 @@ async def add_filter_controls(
caption = ", ".join( caption = ", ".join(
[ [
await get_callable_str( await get_callable_str(
entity_descriptor.fields_descriptors[field_name].caption, callable_str=entity_descriptor.fields_descriptors[field_name].caption,
entity_descriptor, context=context,
descriptor=entity_descriptor.fields_descriptors[field_name],
) )
if entity_descriptor.fields_descriptors[field_name].caption if entity_descriptor.fields_descriptors[field_name].caption
else field_name else field_name

View File

@@ -2,9 +2,12 @@ from aiogram.types import Message, CallbackQuery
from decimal import Decimal from decimal import Decimal
from datetime import datetime, time from datetime import datetime, time
from quickbot.main import QBotApp
from quickbot.utils.serialization import deserialize
from ....model.bot_entity import BotEntity from ....model.bot_entity import BotEntity
from ....model.bot_enum import BotEnum from ....model.bot_enum import BotEnum
from ....model.descriptors import FieldDescriptor from ....model.descriptors import BotContext, FieldDescriptor
from ....model.settings import Settings from ....model.settings import Settings
from ....model.user import UserBase from ....model.user import UserBase
from ....utils.main import get_callable_str, get_value_repr from ....utils.main import get_callable_str, get_value_repr
@@ -21,38 +24,87 @@ async def show_editor(message: Message | CallbackQuery, **kwargs):
user: UserBase = kwargs["user"] user: UserBase = kwargs["user"]
callback_data: ContextData = kwargs.get("callback_data", None) callback_data: ContextData = kwargs.get("callback_data", None)
state_data: dict = kwargs["state_data"] state_data: dict = kwargs["state_data"]
db_session = kwargs["db_session"]
app: QBotApp = kwargs["app"]
value_type = field_descriptor.type_base value_type = field_descriptor.type_base
entity_data_dict: dict = state_data.get("entity_data")
entity_data = None
if callback_data.context == CommandContext.COMMAND_FORM:
cmd = app.bot_commands.get(callback_data.user_command.split("&")[0])
entity_data = (
{
key: await deserialize(
session=kwargs["db_session"],
type_=cmd.param_form[key].type_,
value=value,
)
for key, value in entity_data_dict.items()
}
if entity_data_dict and cmd.param_form
else None
)
elif callback_data.context == CommandContext.ENTITY_CREATE:
entity_data = (
{
key: await deserialize(
session=kwargs["db_session"],
type_=field_descriptor.entity_descriptor.fields_descriptors[
key
].type_,
value=value,
)
for key, value in entity_data_dict.items()
}
if entity_data_dict
else None
)
else:
entity_id = callback_data.entity_id
if entity_id:
entity_data = await field_descriptor.entity_descriptor.type_.get(
session=db_session, id=entity_id
)
context = BotContext(
db_session=db_session, app=app, app_state=kwargs["app_state"], message=message
)
if field_descriptor.edit_prompt: if field_descriptor.edit_prompt:
edit_prompt = await get_callable_str( edit_prompt = await get_callable_str(
field_descriptor.edit_prompt, callable_str=field_descriptor.edit_prompt,
field_descriptor, context=context,
callback_data descriptor=field_descriptor,
if callback_data.context == CommandContext.COMMAND_FORM entity=entity_data,
else None,
current_value,
) )
else: else:
if field_descriptor.caption: if field_descriptor.caption:
caption_str = await get_callable_str( caption_str = await get_callable_str(
field_descriptor.caption, field_descriptor.caption,
field_descriptor, context=context,
callback_data descriptor=field_descriptor,
if callback_data.context == CommandContext.COMMAND_FORM
else None,
current_value,
) )
else: else:
caption_str = field_descriptor.name caption_str = field_descriptor.name
if callback_data.context == CommandContext.ENTITY_EDIT: if callback_data.context == CommandContext.ENTITY_EDIT:
db_session = kwargs["db_session"]
app = kwargs["app"]
edit_prompt = ( edit_prompt = (
await Settings.get( await Settings.get(
Settings.APP_STRINGS_FIELD_EDIT_PROMPT_TEMPLATE_P_NAME_VALUE Settings.APP_STRINGS_FIELD_EDIT_PROMPT_TEMPLATE_P_NAME_VALUE
) )
).format( ).format(
name=caption_str, name=caption_str,
value=await get_value_repr(current_value, field_descriptor, user.lang), value=await get_value_repr(
value=current_value,
field_descriptor=field_descriptor,
context=context,
locale=user.lang,
),
) )
else: else:
edit_prompt = ( edit_prompt = (

View File

@@ -14,7 +14,7 @@ from ....model.bot_enum import BotEnum
from ....model.settings import Settings from ....model.settings import Settings
from ....model.user import UserBase from ....model.user import UserBase
from ....model.view_setting import ViewSetting from ....model.view_setting import ViewSetting
from ....model.descriptors import FieldDescriptor, Filter from ....model.descriptors import BotContext, EntityList, FieldDescriptor, Filter
from ....model import EntityPermission from ....model import EntityPermission
from ....utils.main import ( from ....utils.main import (
get_user_permissions, get_user_permissions,
@@ -108,9 +108,8 @@ async def render_entity_picker(
for item in enum_items for item in enum_items
] ]
elif issubclass(type_, BotEntity): elif issubclass(type_, BotEntity):
form_name = field_descriptor.ep_form or "default" form_list: EntityList = type_.bot_entity_descriptor.lists.get(
form_list = type_.bot_entity_descriptor.lists.get( field_descriptor.ep_form, type_.bot_entity_descriptor.default_list
form_name, type_.bot_entity_descriptor.default_list
) )
permissions = get_user_permissions(user, type_.bot_entity_descriptor) permissions = get_user_permissions(user, type_.bot_entity_descriptor)
if form_list.filtering: if form_list.filtering:
@@ -196,6 +195,13 @@ async def render_entity_picker(
page = 1 page = 1
entity_items = list[BotEntity]() entity_items = list[BotEntity]()
context = BotContext(
db_session=db_session,
app=kwargs["app"],
app_state=kwargs["app_state"],
message=message,
)
items = [ items = [
{ {
"text": f"{ "text": f"{
@@ -205,14 +211,16 @@ async def render_entity_picker(
if item in (current_value or []) if item in (current_value or [])
else '【 】 ' else '【 】 '
}{ }{
type_.bot_entity_descriptor.item_repr( await get_callable_str(
type_.bot_entity_descriptor, item callable_str=type_.bot_entity_descriptor.item_repr,
context=context,
entity=item,
) )
if type_.bot_entity_descriptor.item_repr if type_.bot_entity_descriptor.item_repr
else await get_callable_str( else await get_callable_str(
type_.bot_entity_descriptor.full_name, callable_str=type_.bot_entity_descriptor.full_name,
type_.bot_entity_descriptor, context=context,
item, descriptor=type_.bot_entity_descriptor,
) )
if type_.bot_entity_descriptor.full_name if type_.bot_entity_descriptor.full_name
else f'{type_.bot_entity_descriptor.name}: {str(item.id)}' else f'{type_.bot_entity_descriptor.name}: {str(item.id)}'
@@ -262,6 +270,7 @@ async def render_entity_picker(
await add_filter_controls( await add_filter_controls(
keyboard_builder=keyboard_builder, keyboard_builder=keyboard_builder,
entity_descriptor=type_.bot_entity_descriptor, entity_descriptor=type_.bot_entity_descriptor,
context=context,
filter=entity_filter, filter=entity_filter,
filtering_fields=form_list.filtering_fields, filtering_fields=form_list.filtering_fields,
) )

View File

@@ -5,6 +5,8 @@ from aiogram.types import Message, CallbackQuery
from logging import getLogger from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from quickbot.model.descriptors import EntityForm
from ....model import EntityPermission from ....model import EntityPermission
from ....model.settings import Settings from ....model.settings import Settings
from ....model.user import UserBase from ....model.user import UserBase
@@ -127,14 +129,17 @@ async def field_editor(message: Message | CallbackQuery, **kwargs):
form_name = ( form_name = (
callback_data.form_params.split("&")[0] callback_data.form_params.split("&")[0]
if callback_data.form_params if callback_data.form_params
else "default" else None
) )
form = entity_descriptor.forms.get( form: EntityForm = entity_descriptor.forms.get(
form_name, entity_descriptor.default_form form_name, entity_descriptor.default_form
) )
entity_data = { entity_data = {
key: serialize( key: serialize(
getattr(entity, key), getattr(
entity,
entity_descriptor.fields_descriptors[key].field_name,
),
entity_descriptor.fields_descriptors[key], entity_descriptor.fields_descriptors[key],
) )
for key in ( for key in (

View File

@@ -13,7 +13,12 @@ from ..user_handlers.main import cammand_handler
from ....model import EntityPermission from ....model import EntityPermission
from ....model.user import UserBase from ....model.user import UserBase
from ....model.settings import Settings from ....model.settings import Settings
from ....model.descriptors import EntityEventContext, FieldDescriptor from ....model.descriptors import (
BotContext,
EntityForm,
EntityList,
FieldDescriptor,
)
from ....model.language import LanguageBase from ....model.language import LanguageBase
from ....auth import authorize_command from ....auth import authorize_command
from ....utils.main import ( from ....utils.main import (
@@ -84,16 +89,6 @@ async def field_editor_callback(message: Message | CallbackQuery, **kwargs):
value=value, value=value,
**kwargs, **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]: elif type_base in [int, float, Decimal]:
try: try:
@@ -163,9 +158,9 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
form_name = ( form_name = (
callback_data.form_params.split("&")[0] callback_data.form_params.split("&")[0]
if callback_data.form_params if callback_data.form_params
else "default" else None
) )
form = entity_descriptor.forms.get( form: EntityForm = entity_descriptor.forms.get(
form_name, entity_descriptor.default_form form_name, entity_descriptor.default_form
) )
@@ -176,6 +171,7 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
entity_descriptor=entity_descriptor, entity_descriptor=entity_descriptor,
user=user, user=user,
callback_data=callback_data, callback_data=callback_data,
state_data=state_data,
) )
current_index = ( current_index = (
@@ -188,7 +184,7 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
entity_data = state_data.get("entity_data", {}) entity_data = state_data.get("entity_data", {})
if callback_data.context == CommandContext.ENTITY_CREATE and not entity_data: if callback_data.context == CommandContext.ENTITY_CREATE:
stack = state_data.get("navigation_stack", []) stack = state_data.get("navigation_stack", [])
prev_callback_data = ContextData.unpack(stack[-1]) if stack else None prev_callback_data = ContextData.unpack(stack[-1]) if stack else None
if ( if (
@@ -199,15 +195,15 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
prev_form_name = ( prev_form_name = (
prev_callback_data.form_params.split("&")[0] prev_callback_data.form_params.split("&")[0]
if prev_callback_data.form_params if prev_callback_data.form_params
else "default" else None
) )
prev_form_params = ( prev_form_params = (
prev_callback_data.form_params.split("&")[1:] prev_callback_data.form_params.split("&")[1:]
if prev_callback_data.form_params if prev_callback_data.form_params
else [] else []
) )
prev_form_list = entity_descriptor.lists.get( prev_form_list: EntityList = entity_descriptor.lists.get(
prev_form_name or "default", entity_descriptor.default_list prev_form_name, entity_descriptor.default_list
) )
if prev_form_list.static_filters: if prev_form_list.static_filters:
@@ -256,7 +252,7 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
) )
else: else:
entity_data[field_descriptor.field_name] = value entity_data[field_descriptor.name] = value
# What if user has several roles and each role has its own ownership field? Should we allow creation even # 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 # if user has no CREATE_ALL permission
@@ -283,6 +279,13 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
for key, value in entity_data.items() for key, value in entity_data.items()
} }
context = BotContext(
db_session=db_session,
app=app,
app_state=kwargs["app_state"],
message=message,
)
if callback_data.context == CommandContext.ENTITY_CREATE: if callback_data.context == CommandContext.ENTITY_CREATE:
entity_type = entity_descriptor.type_ entity_type = entity_descriptor.type_
user_permissions = get_user_permissions(user, entity_descriptor) user_permissions = get_user_permissions(user, entity_descriptor)
@@ -304,25 +307,21 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
if iscoroutinefunction(entity_descriptor.on_created): if iscoroutinefunction(entity_descriptor.on_created):
await entity_descriptor.on_created( await entity_descriptor.on_created(
new_entity, new_entity,
EntityEventContext( context,
db_session=db_session, app=app, message=message
),
) )
else: else:
entity_descriptor.on_created( entity_descriptor.on_created(
new_entity, new_entity,
EntityEventContext( context,
db_session=db_session, app=app, message=message
),
) )
form_name = ( form_name = (
callback_data.form_params.split("&")[0] callback_data.form_params.split("&")[0]
if callback_data.form_params if callback_data.form_params
else "default" else None
) )
form_list = entity_descriptor.lists.get( form_list = entity_descriptor.lists.get(
form_name or "default", entity_descriptor.default_list form_name, entity_descriptor.default_list
) )
state_data["navigation_context"] = ContextData( state_data["navigation_context"] = ContextData(
@@ -357,7 +356,11 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
) )
for key, value in deser_entity_data.items(): for key, value in deser_entity_data.items():
setattr(entity, key, value) setattr(
entity,
entity.bot_entity_descriptor.fields_descriptors[key].field_name,
value,
)
await db_session.commit() await db_session.commit()
await db_session.refresh(entity) await db_session.refresh(entity)
@@ -366,16 +369,12 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
if iscoroutinefunction(entity_descriptor.on_updated): if iscoroutinefunction(entity_descriptor.on_updated):
await entity_descriptor.on_updated( await entity_descriptor.on_updated(
entity, entity,
EntityEventContext( context,
db_session=db_session, app=app, message=message
),
) )
else: else:
entity_descriptor.on_updated( entity_descriptor.on_updated(
entity, entity,
EntityEventContext( context,
db_session=db_session, app=app, message=message
),
) )
elif callback_data.context == CommandContext.COMMAND_FORM: elif callback_data.context == CommandContext.COMMAND_FORM:
@@ -397,5 +396,4 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
clear_state(state_data=state_data) clear_state(state_data=state_data)
# TODO: Try back=False and check if it works to navigate to newly created entity
await route_callback(message=message, back=True, **kwargs) await route_callback(message=message, back=True, **kwargs)

View File

@@ -2,7 +2,7 @@ from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from ....model.settings import Settings from ....model.settings import Settings
from ....model.descriptors import FieldDescriptor from ....model.descriptors import EntityForm, FieldDescriptor
from ....model.user import UserBase from ....model.user import UserBase
from ..context import ContextData, CallbackCommand, CommandContext from ..context import ContextData, CallbackCommand, CommandContext
from ....utils.navigation import get_navigation_context, pop_navigation_context from ....utils.navigation import get_navigation_context, pop_navigation_context
@@ -34,9 +34,9 @@ async def wrap_editor(
form_name = ( form_name = (
callback_data.form_params.split("&")[0] callback_data.form_params.split("&")[0]
if callback_data.form_params if callback_data.form_params
else "default" else None
) )
form = field_descriptor.entity_descriptor.forms.get( form: EntityForm = field_descriptor.entity_descriptor.forms.get(
form_name, field_descriptor.entity_descriptor.default_form form_name, field_descriptor.entity_descriptor.default_form
) )
if form.edit_field_sequence: if form.edit_field_sequence:
@@ -46,6 +46,7 @@ async def wrap_editor(
entity_descriptor=field_descriptor.entity_descriptor, entity_descriptor=field_descriptor.entity_descriptor,
user=user, user=user,
callback_data=callback_data, callback_data=callback_data,
state_data=state_data,
) )
field_index = ( field_index = (
field_sequence.index(field_descriptor.name) field_sequence.index(field_descriptor.name)

View File

@@ -8,10 +8,11 @@ from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from ....model.descriptors import ( from ....model.descriptors import (
EntityForm,
FieldEditButton, FieldEditButton,
CommandButton, CommandButton,
InlineButton, InlineButton,
EntityEventContext, BotContext,
) )
from ....model.settings import Settings from ....model.settings import Settings
from ....model.user import UserBase from ....model.user import UserBase
@@ -92,12 +93,15 @@ async def entity_item(
entity=entity_item, user=user, permission=EntityPermission.UPDATE entity=entity_item, user=user, permission=EntityPermission.UPDATE
) )
form = entity_descriptor.forms.get( form: EntityForm = entity_descriptor.forms.get(
callback_data.form_params or "default", entity_descriptor.default_form callback_data.form_params, entity_descriptor.default_form
)
context = BotContext(
db_session=db_session, app=app, app_state=kwargs["app_state"], message=query
) )
if form.form_buttons: if form.form_buttons:
context = EntityEventContext(db_session=db_session, app=app, message=query)
for edit_buttons_row in form.form_buttons: for edit_buttons_row in form.form_buttons:
btn_row = [] btn_row = []
for button in edit_buttons_row: for button in edit_buttons_row:
@@ -114,16 +118,17 @@ async def entity_item(
field_value = getattr(entity_item, field_descriptor.field_name) field_value = getattr(entity_item, field_descriptor.field_name)
if btn_caption: if btn_caption:
btn_text = await get_callable_str( btn_text = await get_callable_str(
btn_caption, field_descriptor, entity_item, field_value callable_str=btn_caption,
context=context,
entity=entity_item,
) )
else: else:
if field_descriptor.type_base is bool: 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( await get_callable_str(
field_descriptor.caption, callable_str=field_descriptor.caption,
field_descriptor, context=context,
entity_item, descriptor=field_descriptor,
field_value,
) )
if field_descriptor.caption if field_descriptor.caption
else field_name else field_name
@@ -135,10 +140,9 @@ async def entity_item(
else '✏️' else '✏️'
} { } {
await get_callable_str( await get_callable_str(
field_descriptor.caption, callable_str=field_descriptor.caption,
field_descriptor, context=context,
entity_item, descriptor=field_descriptor,
field_value,
) )
if field_descriptor.caption if field_descriptor.caption
else field_name else field_name
@@ -160,7 +164,9 @@ async def entity_item(
btn_caption = button.caption btn_caption = button.caption
btn_text = await get_callable_str( btn_text = await get_callable_str(
btn_caption, entity_descriptor, entity_item callable_str=btn_caption,
context=context,
entity=entity_item,
) )
if isinstance(button.command, ContextData): if isinstance(button.command, ContextData):
@@ -206,6 +212,7 @@ async def entity_item(
entity_descriptor=entity_descriptor, entity_descriptor=entity_descriptor,
user=user, user=user,
callback_data=callback_data, callback_data=callback_data,
state_data=state_data,
) )
edit_delete_row.append( edit_delete_row.append(
InlineKeyboardButton( InlineKeyboardButton(
@@ -243,11 +250,17 @@ async def entity_item(
keyboard_builder.row(*edit_delete_row) keyboard_builder.row(*edit_delete_row)
if form.item_repr: if form.item_repr:
item_text = form.item_repr(entity_descriptor, entity_item) item_text = await get_callable_str(
callable_str=form.item_repr,
context=context,
entity=entity_item,
)
else: else:
entity_caption = ( entity_caption = (
await get_callable_str( await get_callable_str(
entity_descriptor.full_name, entity_descriptor, entity_item callable_str=entity_descriptor.full_name,
context=context,
descriptor=entity_descriptor,
) )
if entity_descriptor.full_name if entity_descriptor.full_name
else entity_descriptor.name else entity_descriptor.name
@@ -255,7 +268,9 @@ async def entity_item(
entity_item_repr = ( entity_item_repr = (
await get_callable_str( await get_callable_str(
entity_descriptor.item_repr, entity_descriptor, entity_item callable_str=entity_descriptor.item_repr,
context=context,
entity=entity_item,
) )
if entity_descriptor.item_repr if entity_descriptor.item_repr
else str(entity_item.id) else str(entity_item.id)
@@ -267,11 +282,23 @@ async def entity_item(
for field_descriptor in entity_descriptor.fields_descriptors.values(): for field_descriptor in entity_descriptor.fields_descriptors.values():
if ( if (
field_descriptor.is_visible is not None isinstance(field_descriptor.is_visible, bool)
and not field_descriptor.is_visible and not field_descriptor.is_visible
): ):
continue continue
if callable(field_descriptor.is_visible):
if iscoroutinefunction(field_descriptor.is_visible):
field_visible = await field_descriptor.is_visible(
field_descriptor, entity_item, context
)
else:
field_visible = field_descriptor.is_visible(
field_descriptor, entity_item, context
)
if not field_visible:
continue
skip = False skip = False
for own_field in entity_descriptor.ownership_fields.items(): for own_field in entity_descriptor.ownership_fields.items():
@@ -287,20 +314,27 @@ async def entity_item(
if skip: if skip:
continue continue
field_caption = await get_callable_str( field_caption = (
field_descriptor.caption, field_descriptor, entity_item await get_callable_str(
callable_str=field_descriptor.caption,
context=context,
descriptor=field_descriptor,
)
if field_descriptor.caption
else field_descriptor.field_name
) )
if field_descriptor.caption_value: if field_descriptor.caption_value:
value = await get_callable_str( value = await get_callable_str(
field_descriptor.caption_value, callable_str=field_descriptor.caption_value,
field_descriptor, context=context,
entity_item, descriptor=field_descriptor,
getattr(entity_item, field_descriptor.field_name), entity=entity_item,
) )
else: else:
value = await get_value_repr( value = await get_value_repr(
value=getattr(entity_item, field_descriptor.field_name), value=getattr(entity_item, field_descriptor.field_name),
field_descriptor=field_descriptor, field_descriptor=field_descriptor,
context=context,
locale=user.lang, locale=user.lang,
) )
item_text += f"\n{field_caption or field_descriptor.name}:{f' <b>{value}</b>' if value else ''}" item_text += f"\n{field_caption or field_descriptor.name}:{f' <b>{value}</b>' if value else ''}"

View File

@@ -6,7 +6,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from quickbot.model.descriptors import EntityEventContext from quickbot.model.descriptors import BotContext
from ..context import ContextData, CallbackCommand from ..context import ContextData, CallbackCommand
from ....model.user import UserBase from ....model.user import UserBase
@@ -49,6 +49,10 @@ async def entity_delete_callback(query: CallbackQuery, **kwargs):
text=(await Settings.get(Settings.APP_STRINGS_FORBIDDEN)) text=(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
) )
context = BotContext(
db_session=db_session, app=app, app_state=kwargs["app_state"], message=query
)
if callback_data.data == "yes": if callback_data.data == "yes":
entity = await entity_descriptor.type_.remove( entity = await entity_descriptor.type_.remove(
session=db_session, id=int(callback_data.entity_id), commit=True session=db_session, id=int(callback_data.entity_id), commit=True
@@ -58,12 +62,12 @@ async def entity_delete_callback(query: CallbackQuery, **kwargs):
if iscoroutinefunction(entity_descriptor.on_created): if iscoroutinefunction(entity_descriptor.on_created):
await entity_descriptor.on_deleted( await entity_descriptor.on_deleted(
entity, entity,
EntityEventContext(db_session=db_session, app=app, message=query), context,
) )
else: else:
entity_descriptor.on_deleted( entity_descriptor.on_deleted(
entity, entity,
EntityEventContext(db_session=db_session, app=app, message=query), context,
) )
await route_callback(message=query, **kwargs) await route_callback(message=query, **kwargs)
@@ -76,7 +80,12 @@ async def entity_delete_callback(query: CallbackQuery, **kwargs):
return await query.message.edit_text( return await query.message.edit_text(
text=( text=(
await Settings.get(Settings.APP_STRINGS_CONFIRM_DELETE_P_NAME) await Settings.get(Settings.APP_STRINGS_CONFIRM_DELETE_P_NAME)
).format(name=await get_entity_item_repr(entity=entity)), ).format(
name=await get_entity_item_repr(
entity=entity,
context=context,
)
),
reply_markup=InlineKeyboardBuilder() reply_markup=InlineKeyboardBuilder()
.row( .row(
InlineKeyboardButton( InlineKeyboardButton(

View File

@@ -10,7 +10,13 @@ from ....model.bot_entity import BotEntity
from ....model.settings import Settings from ....model.settings import Settings
from ....model.user import UserBase from ....model.user import UserBase
from ....model.view_setting import ViewSetting from ....model.view_setting import ViewSetting
from ....model.descriptors import EntityDescriptor, Filter from ....model.descriptors import (
BotContext,
EntityDescriptor,
EntityForm,
EntityList,
Filter,
)
from ....model import EntityPermission from ....model import EntityPermission
from ....utils.main import ( from ....utils.main import (
get_user_permissions, get_user_permissions,
@@ -103,12 +109,12 @@ async def entity_list(
form_params = ( form_params = (
callback_data.form_params.split("&") if callback_data.form_params else [] callback_data.form_params.split("&") if callback_data.form_params else []
) )
form_name = form_params.pop(0) if form_params else "default" form_name = form_params.pop(0) if form_params else None
form_list = entity_descriptor.lists.get( form_list: EntityList = entity_descriptor.lists.get(
form_name or "default", entity_descriptor.default_list form_name, entity_descriptor.default_list
) )
form_item = entity_descriptor.forms.get( form_item: EntityForm = entity_descriptor.forms.get(
form_list.item_form or "default", entity_descriptor.default_form form_list.item_form, entity_descriptor.default_form
) )
keyboard_builder = InlineKeyboardBuilder() keyboard_builder = InlineKeyboardBuilder()
@@ -124,6 +130,7 @@ async def entity_list(
entity_descriptor=entity_descriptor, entity_descriptor=entity_descriptor,
user=user, user=user,
callback_data=callback_data, callback_data=callback_data,
state_data=kwargs["state_data"],
) )
keyboard_builder.row( keyboard_builder.row(
InlineKeyboardButton( InlineKeyboardButton(
@@ -199,20 +206,39 @@ async def entity_list(
total_pages = 1 total_pages = 1
page = 1 page = 1
context = BotContext(
db_session=db_session,
app=app,
app_state=kwargs["app_state"],
message=message,
)
for item in items: for item in items:
caption = None
if form_list.item_repr: if form_list.item_repr:
caption = form_list.item_repr(entity_descriptor, item) caption = await get_callable_str(
callable_str=form_list.item_repr,
context=context,
entity=item,
)
elif entity_descriptor.item_repr: elif entity_descriptor.item_repr:
caption = entity_descriptor.item_repr(entity_descriptor, item) caption = await get_callable_str(
callable_str=entity_descriptor.item_repr,
context=context,
entity=item,
)
elif entity_descriptor.full_name: elif entity_descriptor.full_name:
caption = f"{ caption = f"{
await get_callable_str( await get_callable_str(
callable_str=entity_descriptor.full_name, callable_str=entity_descriptor.full_name,
context=context,
descriptor=entity_descriptor, descriptor=entity_descriptor,
entity=item, entity=item,
) )
}: {item.id}" }: {item.id}"
else:
if not caption:
caption = f"{entity_descriptor.name}: {item.id}" caption = f"{entity_descriptor.name}: {item.id}"
keyboard_builder.row( keyboard_builder.row(
@@ -240,6 +266,7 @@ async def entity_list(
await add_filter_controls( await add_filter_controls(
keyboard_builder=keyboard_builder, keyboard_builder=keyboard_builder,
entity_descriptor=entity_descriptor, entity_descriptor=entity_descriptor,
context=context,
filter=entity_filter, filter=entity_filter,
filtering_fields=form_list.filtering_fields, filtering_fields=form_list.filtering_fields,
) )
@@ -254,17 +281,29 @@ async def entity_list(
) )
if form_list.caption: if form_list.caption:
entity_text = await get_callable_str(form_list.caption, entity_descriptor) entity_text = await get_callable_str(
callable_str=form_list.caption,
context=context,
descriptor=entity_descriptor,
)
else: else:
if entity_descriptor.full_name_plural: if entity_descriptor.full_name_plural:
entity_text = await get_callable_str( entity_text = await get_callable_str(
entity_descriptor.full_name_plural, entity_descriptor callable_str=entity_descriptor.full_name_plural,
context=context,
descriptor=entity_descriptor,
) )
else: else:
entity_text = entity_descriptor.name entity_text = entity_descriptor.name
if entity_descriptor.description: if entity_descriptor.description:
entity_text = f"{entity_text} {await get_callable_str(entity_descriptor.description, entity_descriptor)}" entity_text = f"{entity_text} {
await get_callable_str(
callable_str=entity_descriptor.description,
context=context,
descriptor=entity_descriptor,
)
}"
state: FSMContext = kwargs["state"] state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"] state_data = kwargs["state_data"]

View File

@@ -2,13 +2,13 @@ from aiogram import Router, F
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from babel.support import LazyProxy
from logging import getLogger from logging import getLogger
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from quickbot.model.descriptors import BotContext
from ....model.settings import Settings from ....model.settings import Settings
from ..context import ContextData, CallbackCommand from ..context import ContextData, CallbackCommand
from ....utils.main import get_send_message from ....utils.main import get_send_message, get_callable_str
from ....model.descriptors import EntityCaptionCallable
from ....utils.navigation import save_navigation_context, pop_navigation_context from ....utils.navigation import save_navigation_context, pop_navigation_context
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -46,12 +46,21 @@ async def entities_menu(
for entity in entity_metadata.entity_descriptors.values(): for entity in entity_metadata.entity_descriptors.values():
if entity.show_in_entities_menu: if entity.show_in_entities_menu:
if entity.full_name_plural.__class__ == EntityCaptionCallable: if entity.full_name_plural:
caption = entity.full_name_plural(entity) or entity.name caption = await get_callable_str(
elif entity.full_name_plural.__class__ == LazyProxy: callable_str=entity.full_name_plural,
caption = f"{f'{entity.icon} ' if entity.icon else ''}{entity.full_name_plural.value or entity.name}" context=BotContext(
db_session=kwargs["db_session"],
app=app,
app_state=kwargs["app_state"],
message=message,
),
descriptor=entity,
)
else: else:
caption = f"{f'{entity.icon} ' if entity.icon else ''}{entity.full_name_plural or entity.name}" caption = entity.name
caption = f"{f'{entity.icon} ' if entity.icon else ''}{caption}"
keyboard_builder.row( keyboard_builder.row(
InlineKeyboardButton( InlineKeyboardButton(

View File

@@ -4,6 +4,8 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger from logging import getLogger
from quickbot.model.descriptors import BotContext
from ....model.settings import Settings from ....model.settings import Settings
from ....model.user import UserBase from ....model.user import UserBase
from ..context import ContextData, CallbackCommand, CommandContext from ..context import ContextData, CallbackCommand, CommandContext
@@ -53,14 +55,26 @@ async def parameters_menu(
if not key.is_visible: if not key.is_visible:
continue continue
context = BotContext(
db_session=kwargs["db_session"],
app=kwargs["app"],
app_state=kwargs["app_state"],
message=message,
)
if key.caption_value: if key.caption_value:
caption = await get_callable_str( caption = await get_callable_str(
callable_str=key.caption_value, descriptor=key, entity=None, value=value callable_str=key.caption_value,
context=context,
descriptor=key,
entity={key.field_name: value},
) )
else: else:
if key.caption: if key.caption:
caption = await get_callable_str( caption = await get_callable_str(
callable_str=key.caption, descriptor=key, entity=None, value=value callable_str=key.caption,
context=context,
descriptor=key,
) )
else: else:
caption = key.name caption = key.name
@@ -68,7 +82,7 @@ async def parameters_menu(
if key.type_ is bool: if key.type_ is bool:
caption = f"{'【✔︎】' if value else '【 】'} {caption}" caption = f"{'【✔︎】' if value else '【 】'} {caption}"
else: else:
caption = f"{caption}: {await get_value_repr(value=value, field_descriptor=key, locale=user.lang)}" caption = f"{caption}: {await get_value_repr(value=value, field_descriptor=key, context=context, locale=user.lang)}"
keyboard_builder.row( keyboard_builder.row(
InlineKeyboardButton( InlineKeyboardButton(

View File

@@ -5,6 +5,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from typing import Any, Callable, TYPE_CHECKING, Literal, Union from typing import Any, Callable, TYPE_CHECKING, Literal, Union
from babel.support import LazyProxy from babel.support import LazyProxy
from dataclasses import dataclass, field from dataclasses import dataclass, field
from fastapi.datastructures import State
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from .role import RoleBase from .role import RoleBase
@@ -16,29 +17,32 @@ if TYPE_CHECKING:
from ..main import QBotApp from ..main import QBotApp
from .user import UserBase from .user import UserBase
EntityCaptionCallable = Callable[["EntityDescriptor"], str] # EntityCaptionCallable = Callable[["EntityDescriptor"], str]
EntityItemCaptionCallable = Callable[["EntityDescriptor", Any], str] # EntityItemCaptionCallable = Callable[["EntityDescriptor", Any], str]
EntityFieldCaptionCallable = Callable[["FieldDescriptor", Any, Any], str] # EntityFieldCaptionCallable = Callable[["FieldDescriptor", Any, Any], str]
@dataclass @dataclass
class FieldEditButton: class FieldEditButton:
field_name: str field_name: str
caption: str | LazyProxy | EntityFieldCaptionCallable | None = None caption: str | LazyProxy | Callable[["BotEntity", "BotContext"], str] | None = None
visibility: Callable[[Any], bool] | None = None visibility: Callable[["BotEntity", "BotContext"], bool] | None = None
@dataclass @dataclass
class CommandButton: class CommandButton:
command: ContextData | Callable[[ContextData, Any], ContextData] | str command: ContextData | Callable[[ContextData, Any], ContextData] | str
caption: str | LazyProxy | EntityItemCaptionCallable caption: str | LazyProxy | Callable[["BotEntity", "BotContext"], str] | None = None
visibility: Callable[[Any], bool] | None = None visibility: Callable[["BotEntity", "BotContext"], bool] | None = None
@dataclass @dataclass
class InlineButton: class InlineButton:
inline_button: InlineKeyboardButton | Callable[[Any], InlineKeyboardButton] inline_button: (
visibility: Callable[[Any], bool] | None = None InlineKeyboardButton
| Callable[["BotEntity", "BotContext"], InlineKeyboardButton]
)
visibility: Callable[["BotEntity", "BotContext"], bool] | None = None
@dataclass @dataclass
@@ -66,8 +70,10 @@ class Filter:
@dataclass @dataclass
class EntityList: class EntityList:
caption: str | LazyProxy | EntityCaptionCallable | None = None caption: (
item_repr: EntityItemCaptionCallable | None = None str | LazyProxy | Callable[["EntityDescriptor", "BotContext"], str] | None
) = None
item_repr: Callable[["BotEntity", "BotContext"], str] | None = None
show_add_new_button: bool = True show_add_new_button: bool = True
item_form: str | None = None item_form: str | None = None
pagination: bool = True pagination: bool = True
@@ -79,7 +85,7 @@ class EntityList:
@dataclass @dataclass
class EntityForm: class EntityForm:
item_repr: EntityItemCaptionCallable | None = None item_repr: Callable[["BotEntity", "BotContext"], str] | None = None
edit_field_sequence: list[str] = None edit_field_sequence: list[str] = None
form_buttons: list[list[FieldEditButton | CommandButton | InlineButton]] = None form_buttons: list[list[FieldEditButton | CommandButton | InlineButton]] = None
show_edit_button: bool = True show_edit_button: bool = True
@@ -89,11 +95,24 @@ class EntityForm:
@dataclass(kw_only=True) @dataclass(kw_only=True)
class _BaseFieldDescriptor: class _BaseFieldDescriptor:
icon: str = None icon: str = None
caption: str | LazyProxy | EntityFieldCaptionCallable | None = None caption: (
description: str | LazyProxy | EntityFieldCaptionCallable | None = None str | LazyProxy | Callable[["FieldDescriptor", "BotContext"], str] | None
edit_prompt: str | LazyProxy | EntityFieldCaptionCallable | None = None ) = None
caption_value: EntityFieldCaptionCallable | None = None description: (
is_visible: bool | None = None str | LazyProxy | Callable[["FieldDescriptor", "BotContext"], str] | None
) = None
edit_prompt: (
str
| LazyProxy
| Callable[["FieldDescriptor", Union["BotEntity", Any], "BotContext"], str]
| None
) = None
caption_value: (
Callable[["FieldDescriptor", Union["BotEntity", Any], "BotContext"], str] | None
) = None
is_visible: (
bool | Callable[["FieldDescriptor", "BotEntity", "BotContext"], bool] | None
) = None
localizable: bool = False localizable: bool = False
bool_false_value: str | LazyProxy = "no" bool_false_value: str | LazyProxy = "no"
bool_true_value: str | LazyProxy = "yes" bool_true_value: str | LazyProxy = "yes"
@@ -140,10 +159,16 @@ class FieldDescriptor(_BaseFieldDescriptor):
@dataclass(kw_only=True) @dataclass(kw_only=True)
class _BaseEntityDescriptor: class _BaseEntityDescriptor:
icon: str = "📘" icon: str = "📘"
full_name: str | LazyProxy | EntityCaptionCallable | None = None full_name: (
full_name_plural: str | LazyProxy | EntityCaptionCallable | None = None str | LazyProxy | Callable[["EntityDescriptor", "BotContext"], str] | None
description: str | LazyProxy | EntityCaptionCallable | None = None ) = None
item_repr: EntityItemCaptionCallable | None = None full_name_plural: (
str | LazyProxy | Callable[["EntityDescriptor", "BotContext"], str] | None
) = None
description: (
str | LazyProxy | Callable[["EntityDescriptor", "BotContext"], str] | None
) = None
item_repr: Callable[["BotEntity", "BotContext"], str] | None = None
default_list: EntityList = field(default_factory=EntityList) default_list: EntityList = field(default_factory=EntityList)
default_form: EntityForm = field(default_factory=EntityForm) default_form: EntityForm = field(default_factory=EntityForm)
lists: dict[str, EntityList] = field(default_factory=dict[str, EntityList]) lists: dict[str, EntityList] = field(default_factory=dict[str, EntityList])
@@ -164,9 +189,9 @@ class _BaseEntityDescriptor:
EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER], EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER],
} }
) )
on_created: Callable[["BotEntity", "EntityEventContext"], None] | None = None on_created: Callable[["BotEntity", "BotContext"], None] | None = None
on_deleted: Callable[["BotEntity", "EntityEventContext"], None] | None = None on_deleted: Callable[["BotEntity", "BotContext"], None] | None = None
on_updated: Callable[["BotEntity", "EntityEventContext"], None] | None = None on_updated: Callable[["BotEntity", "BotContext"], None] | None = None
@dataclass(kw_only=True) @dataclass(kw_only=True)
@@ -202,9 +227,10 @@ class CommandCallbackContext[UT: UserBase]:
@dataclass(kw_only=True) @dataclass(kw_only=True)
class EntityEventContext: class BotContext:
db_session: AsyncSession db_session: AsyncSession
app: "QBotApp" app: "QBotApp"
app_state: State
message: Message | CallbackQuery | None = None message: Message | CallbackQuery | None = None

View File

@@ -2,7 +2,7 @@ from babel.support import LazyProxy
from inspect import iscoroutinefunction, signature from inspect import iscoroutinefunction, signature
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from aiogram.utils.i18n import I18n from aiogram.utils.i18n import I18n
from typing import Any, TYPE_CHECKING from typing import Any, TYPE_CHECKING, Callable
import ujson as json import ujson as json
from ..model.bot_entity import BotEntity from ..model.bot_entity import BotEntity
@@ -10,15 +10,14 @@ from ..model.bot_enum import BotEnum
from ..model.settings import Settings from ..model.settings import Settings
from ..model.descriptors import ( from ..model.descriptors import (
BotContext,
EntityList,
FieldDescriptor, FieldDescriptor,
EntityDescriptor, EntityDescriptor,
EntityItemCaptionCallable,
EntityFieldCaptionCallable,
EntityPermission, EntityPermission,
EntityCaptionCallable,
) )
from ..bot.handlers.context import ContextData, CommandContext from ..bot.handlers.context import CallbackCommand, ContextData, CommandContext
if TYPE_CHECKING: if TYPE_CHECKING:
from ..model.user import UserBase from ..model.user import UserBase
@@ -106,24 +105,38 @@ def clear_state(state_data: dict, clear_nav: bool = False):
async def get_entity_item_repr( async def get_entity_item_repr(
entity: BotEntity, item_repr: EntityItemCaptionCallable | None = None entity: BotEntity,
context: BotContext,
item_repr: Callable[[BotEntity, BotContext], str] | None = None,
) -> str: ) -> str:
descr = entity.bot_entity_descriptor descr = entity.bot_entity_descriptor
if not item_repr:
item_repr = descr.item_repr
if item_repr: if item_repr:
return item_repr(descr, entity) if iscoroutinefunction(item_repr):
return ( return await item_repr(entity, context)
descr.item_repr(descr, entity) else:
if descr.item_repr return item_repr(entity, context)
else f"{
await get_callable_str(descr.full_name, descr, entity) return f"{
if descr.full_name await get_callable_str(
else descr.name callable_str=descr.full_name,
}: {str(entity.id)}" context=context,
) descriptor=descr,
entity=entity,
)
if descr.full_name
else descr.name
}: {str(entity.id)}"
async def get_value_repr( async def get_value_repr(
value: Any, field_descriptor: FieldDescriptor, locale: str | None = None value: Any,
field_descriptor: FieldDescriptor,
context: BotContext,
locale: str | None = None,
) -> str: ) -> str:
if value is None: if value is None:
return "" return ""
@@ -133,9 +146,14 @@ async def get_value_repr(
return "【✔︎】" if value else "【 】" return "【✔︎】" if value else "【 】"
elif field_descriptor.is_list: elif field_descriptor.is_list:
if issubclass(type_, BotEntity): if issubclass(type_, BotEntity):
return ( return f"[{
f"[{', '.join([await get_entity_item_repr(item) for item in value])}]" ', '.join(
) [
await get_entity_item_repr(entity=item, context=context)
for item in value
]
)
}]"
elif issubclass(type_, BotEnum): elif issubclass(type_, BotEnum):
return f"[{', '.join(item.localized(locale) for item in value)}]" return f"[{', '.join(item.localized(locale) for item in value)}]"
elif type_ is str: elif type_ is str:
@@ -143,7 +161,7 @@ async def get_value_repr(
else: else:
return f"[{', '.join([str(item) for item in value])}]" return f"[{', '.join([str(item) for item in value])}]"
elif issubclass(type_, BotEntity): elif issubclass(type_, BotEntity):
return await get_entity_item_repr(value) return await get_entity_item_repr(entity=value, context=context)
elif issubclass(type_, BotEnum): elif issubclass(type_, BotEnum):
return value.localized(locale) return value.localized(locale)
elif isinstance(value, str): elif isinstance(value, str):
@@ -162,13 +180,13 @@ async def get_callable_str(
callable_str: ( callable_str: (
str str
| LazyProxy | LazyProxy
| EntityCaptionCallable | Callable[[EntityDescriptor, BotContext], str]
| EntityItemCaptionCallable | Callable[[BotEntity, BotContext], str]
| EntityFieldCaptionCallable | Callable[[FieldDescriptor, BotEntity, BotContext], str]
), ),
descriptor: FieldDescriptor | EntityDescriptor, context: BotContext,
entity: Any = None, descriptor: FieldDescriptor | EntityDescriptor | None = None,
value: Any = None, entity: BotEntity | Any = None,
) -> str: ) -> str:
if isinstance(callable_str, str): if isinstance(callable_str, str):
return callable_str return callable_str
@@ -177,19 +195,22 @@ async def get_callable_str(
elif callable(callable_str): elif callable(callable_str):
args = signature(callable_str).parameters args = signature(callable_str).parameters
if iscoroutinefunction(callable_str): if iscoroutinefunction(callable_str):
if len(args) == 1: if len(args) == 3:
return await callable_str(descriptor) return await callable_str(descriptor, entity, context)
elif len(args) == 2: else:
return await callable_str(descriptor, entity) if issubclass(args[0].annotation, BotEntity):
elif len(args) == 3: return await callable_str(entity, context)
return await callable_str(descriptor, entity, value) else:
return await callable_str(descriptor, context)
else: else:
if len(args) == 1: if len(args) == 3:
return callable_str(descriptor) return callable_str(descriptor, entity, context)
elif len(args) == 2: else:
return callable_str(descriptor, entity) return callable_str(entity or descriptor, context)
elif len(args) == 3: else:
return callable_str(descriptor, entity, value) raise ValueError(
f"Invalid callable type: {type(callable_str)}. Expected str, LazyProxy or callable."
)
def get_entity_descriptor( def get_entity_descriptor(
@@ -225,37 +246,72 @@ def get_field_descriptor(
def build_field_sequence( def build_field_sequence(
entity_descriptor: EntityDescriptor, user: "UserBase", callback_data: ContextData entity_descriptor: EntityDescriptor,
user: "UserBase",
callback_data: ContextData,
state_data: dict,
): ):
prev_form_list = None
stack = state_data.get("navigation_stack", [])
prev_callback_data = ContextData.unpack(stack[-1]) if stack else None
if (
prev_callback_data
and prev_callback_data.command == CallbackCommand.ENTITY_LIST
and prev_callback_data.entity_name == entity_descriptor.name
):
prev_form_name = (
prev_callback_data.form_params.split("&")[0]
if prev_callback_data.form_params
else None
)
prev_form_list: EntityList = entity_descriptor.lists.get(
prev_form_name, entity_descriptor.default_list
)
field_sequence = list[str]() field_sequence = list[str]()
# exclude ownership fields from edit if user has no CREATE_ALL/UPDATE_ALL permission # exclude ownership fields from edit if user has no CREATE_ALL/UPDATE_ALL permission
user_permissions = get_user_permissions(user, entity_descriptor) user_permissions = get_user_permissions(user, entity_descriptor)
for fd in entity_descriptor.fields_descriptors.values(): for fd in entity_descriptor.fields_descriptors.values():
if not ( skip = False
if (
fd.is_optional fd.is_optional
or fd.field_name == "id" or fd.field_name == "id"
or fd.field_name[:-3] == "_id" or (
fd.field_name[-3:] == "_id"
and fd.field_name[:-3] in entity_descriptor.fields_descriptors
)
or fd.default is not None or fd.default is not None
or fd.default_factory is not None
): ):
skip = False skip = True
for own_field in entity_descriptor.ownership_fields.items(): for own_field in entity_descriptor.ownership_fields.items():
if ( if (
own_field[1].rstrip("_id") == fd.field_name.rstrip("_id") own_field[1].rstrip("_id") == fd.field_name.rstrip("_id")
and own_field[0] in user.roles and own_field[0] in user.roles
and ( and (
( (
EntityPermission.CREATE_ALL not in user_permissions EntityPermission.CREATE_ALL not in user_permissions
and callback_data.context == CommandContext.ENTITY_CREATE and callback_data.context == CommandContext.ENTITY_CREATE
)
or (
EntityPermission.UPDATE_ALL not in user_permissions
and callback_data.context == CommandContext.ENTITY_EDIT
)
) )
): or (
skip = True EntityPermission.UPDATE_ALL not in user_permissions
break and callback_data.context == CommandContext.ENTITY_EDIT
if not skip: )
field_sequence.append(fd.field_name) )
):
skip = True
break
if (
prev_form_list
and prev_form_list.static_filters
and fd.field_name.rstrip("_id")
in [f.field_name.rstrip("_id") for f in prev_form_list.static_filters]
):
skip = True
if not skip:
field_sequence.append(fd.field_name)
return field_sequence return field_sequence