add ruff format, ruff check, time_picker, project structure and imports reorganized
This commit is contained in:
12
__init__.py
12
__init__.py
@@ -1 +1,13 @@
|
|||||||
from .main import QBotApp as QBotApp, Config as Config
|
from .main import QBotApp as QBotApp, Config as Config
|
||||||
|
from .router import Router as Router
|
||||||
|
from .model.bot_entity import BotEntity as BotEntity
|
||||||
|
from .model.bot_enum import BotEnum as BotEnum, EnumMember as EnumMember
|
||||||
|
from .model.descriptors import (
|
||||||
|
Entity as Entity,
|
||||||
|
EntityField as EntityField,
|
||||||
|
EntityForm as EntityForm,
|
||||||
|
EntityList as EntityList,
|
||||||
|
EntityPermission as EntityPermission,
|
||||||
|
Command as Command,
|
||||||
|
CommandCallbackContext as CommandCallbackContext,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from fastapi import APIRouter, Depends, Request, Response
|
from fastapi import APIRouter, Depends, Request, Response
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from ..main import QBotApp
|
from ..main import QBotApp
|
||||||
|
|
||||||
|
|
||||||
from ..db import get_db
|
from ..db import get_db
|
||||||
from aiogram.types import Update
|
from aiogram.types import Update
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
@@ -14,24 +15,25 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/webhook")
|
@router.post("/webhook")
|
||||||
async def telegram_webhook(db_session: Annotated[AsyncSession, Depends(get_db)],
|
async def telegram_webhook(
|
||||||
request: Request):
|
db_session: Annotated[AsyncSession, Depends(get_db)], request: Request
|
||||||
|
):
|
||||||
logger.debug("Webhook request %s", await request.json())
|
logger.debug("Webhook request %s", await request.json())
|
||||||
app = request.app #type: QBotApp
|
app: QBotApp = request.app
|
||||||
|
|
||||||
request_token = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
|
request_token = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
|
||||||
if request_token != app.bot_auth_token:
|
if request_token != app.bot_auth_token:
|
||||||
logger.warning("Unauthorized request %s", request)
|
logger.warning("Unauthorized request %s", request)
|
||||||
return Response(status_code = 403)
|
return Response(status_code=403)
|
||||||
try:
|
try:
|
||||||
update = Update(**await request.json())
|
update = Update(**await request.json())
|
||||||
except:
|
except Exception:
|
||||||
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:
|
||||||
await app.dp.feed_webhook_update(app.bot, update, db_session = db_session, app = app)
|
await app.dp.feed_webhook_update(
|
||||||
except:
|
app.bot, update, db_session=db_session, app=app
|
||||||
logger.error("Error processing update", exc_info = True)
|
)
|
||||||
return Response(status_code = 200)
|
except Exception:
|
||||||
|
logger.error("Error processing update", exc_info=True)
|
||||||
|
return Response(status_code=200)
|
||||||
|
|||||||
14
auth/__init__.py
Normal file
14
auth/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from ..model.settings import Settings
|
||||||
|
from ..model.user import UserBase
|
||||||
|
from ..bot.handlers.context import ContextData, CallbackCommand, CommandContext
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -8,7 +8,6 @@ logger = getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class CallbackCommandFilter(Filter):
|
class CallbackCommandFilter(Filter):
|
||||||
|
|
||||||
def __init__(self, command: CallbackCommand):
|
def __init__(self, command: CallbackCommand):
|
||||||
self.command = command
|
self.command = command
|
||||||
|
|
||||||
@@ -19,11 +18,9 @@ class CallbackCommandFilter(Filter):
|
|||||||
if context_data:
|
if context_data:
|
||||||
try:
|
try:
|
||||||
context_data = ContextData.unpack(context_data)
|
context_data = ContextData.unpack(context_data)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Error unpacking context data", exc_info = True)
|
logger.error("Error unpacking context data", exc_info=True)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return context_data.command == self.command
|
return context_data.command == self.command
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
30
bot/handlers/common/filtering.py
Normal file
30
bot/handlers/common/filtering.py
Normal 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(),
|
||||||
|
)
|
||||||
|
)
|
||||||
168
bot/handlers/common/filtering_callbacks.py
Normal file
168
bot/handlers/common/filtering_callbacks.py
Normal 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)
|
||||||
114
bot/handlers/common/pagination.py
Normal file
114
bot/handlers/common/pagination.py
Normal 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)
|
||||||
48
bot/handlers/common/routing.py
Normal file
48
bot/handlers/common/routing.py
Normal 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")
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
from aiogram.filters.callback_data import CallbackData as BaseCallbackData
|
from aiogram.filters.callback_data import CallbackData as BaseCallbackData
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
class CallbackCommand(StrEnum):
|
|
||||||
|
|
||||||
|
class CallbackCommand(StrEnum):
|
||||||
FIELD_EDITOR = "fe"
|
FIELD_EDITOR = "fe"
|
||||||
FIELD_EDITOR_CALLBACK = "fc"
|
FIELD_EDITOR_CALLBACK = "fc"
|
||||||
ENTITY_LIST = "el"
|
ENTITY_LIST = "el"
|
||||||
@@ -16,25 +16,28 @@ class CallbackCommand(StrEnum):
|
|||||||
SET_LANGUAGE = "ls"
|
SET_LANGUAGE = "ls"
|
||||||
DATE_PICKER_MONTH = "dm"
|
DATE_PICKER_MONTH = "dm"
|
||||||
DATE_PICKER_YEAR = "dy"
|
DATE_PICKER_YEAR = "dy"
|
||||||
#STRING_EDITOR_LOCALE = "sl"
|
TIME_PICKER = "tp"
|
||||||
|
# STRING_EDITOR_LOCALE = "sl"
|
||||||
ENTITY_PICKER_PAGE = "ep"
|
ENTITY_PICKER_PAGE = "ep"
|
||||||
ENTITY_PICKER_TOGGLE_ITEM = "et"
|
ENTITY_PICKER_TOGGLE_ITEM = "et"
|
||||||
VIEW_FILTER_EDIT = "vf"
|
VIEW_FILTER_EDIT = "vf"
|
||||||
USER_COMMAND = "uc"
|
USER_COMMAND = "uc"
|
||||||
|
|
||||||
class CommandContext(StrEnum):
|
|
||||||
|
|
||||||
|
class CommandContext(StrEnum):
|
||||||
SETTING_EDIT = "se"
|
SETTING_EDIT = "se"
|
||||||
ENTITY_CREATE = "ec"
|
ENTITY_CREATE = "ec"
|
||||||
ENTITY_EDIT = "ee"
|
ENTITY_EDIT = "ee"
|
||||||
ENTITY_FIELD_EDIT = "ef"
|
ENTITY_FIELD_EDIT = "ef"
|
||||||
|
|
||||||
class ContextData(BaseCallbackData, prefix = "cd"):
|
|
||||||
|
class ContextData(BaseCallbackData, prefix="cd"):
|
||||||
command: CallbackCommand
|
command: CallbackCommand
|
||||||
context: CommandContext | None = None
|
context: CommandContext | None = None
|
||||||
entity_name: str | None = None
|
entity_name: str | None = None
|
||||||
entity_id: int | None = None
|
entity_id: int | None = None
|
||||||
field_name: str | None = None
|
field_name: str | None = None
|
||||||
|
form_params: str | None = None
|
||||||
user_command: str | None = None
|
user_command: str | None = None
|
||||||
data: str | None = None
|
data: str | None = None
|
||||||
back: bool = False
|
back: bool = False
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -5,22 +5,23 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|||||||
from babel.support import LazyProxy
|
from babel.support import LazyProxy
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
|
from ....model.descriptors import EntityFieldDescriptor
|
||||||
from ..context import ContextData, CallbackCommand
|
from ..context import ContextData, CallbackCommand
|
||||||
from ..common import get_send_message
|
from ....utils.main import get_send_message
|
||||||
from .common import wrap_editor
|
from .wrapper import wrap_editor
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
async def bool_editor(message: Message | CallbackQuery,
|
async def bool_editor(
|
||||||
edit_prompt: str,
|
message: Message | CallbackQuery,
|
||||||
field_descriptor: EntityFieldDescriptor,
|
edit_prompt: str,
|
||||||
callback_data: ContextData,
|
field_descriptor: EntityFieldDescriptor,
|
||||||
**kwargs):
|
callback_data: ContextData,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
keyboard_builder = InlineKeyboardBuilder()
|
keyboard_builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
if isinstance(field_descriptor.bool_true_value, LazyProxy):
|
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
|
false_caption = field_descriptor.bool_false_value
|
||||||
|
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(text = true_caption,
|
InlineKeyboardButton(
|
||||||
callback_data = ContextData(
|
text=true_caption,
|
||||||
command = CallbackCommand.FIELD_EDITOR_CALLBACK,
|
callback_data=ContextData(
|
||||||
context = callback_data.context,
|
command=CallbackCommand.FIELD_EDITOR_CALLBACK,
|
||||||
entity_name = callback_data.entity_name,
|
context=callback_data.context,
|
||||||
entity_id = callback_data.entity_id,
|
entity_name=callback_data.entity_name,
|
||||||
field_name = callback_data.field_name,
|
entity_id=callback_data.entity_id,
|
||||||
data = str(True),
|
field_name=callback_data.field_name,
|
||||||
save_state = True).pack()),
|
form_params=callback_data.form_params,
|
||||||
InlineKeyboardButton(text = false_caption,
|
data=str(True),
|
||||||
callback_data = ContextData(
|
).pack(),
|
||||||
command = CallbackCommand.FIELD_EDITOR_CALLBACK,
|
),
|
||||||
context = callback_data.context,
|
InlineKeyboardButton(
|
||||||
entity_name = callback_data.entity_name,
|
text=false_caption,
|
||||||
entity_id = callback_data.entity_id,
|
callback_data=ContextData(
|
||||||
field_name = callback_data.field_name,
|
command=CallbackCommand.FIELD_EDITOR_CALLBACK,
|
||||||
data = str(False),
|
context=callback_data.context,
|
||||||
save_state = True).pack())
|
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"]
|
state_data = kwargs["state_data"]
|
||||||
|
|
||||||
await wrap_editor(keyboard_builder = keyboard_builder,
|
await wrap_editor(
|
||||||
field_descriptor = field_descriptor,
|
keyboard_builder=keyboard_builder,
|
||||||
callback_data = callback_data,
|
field_descriptor=field_descriptor,
|
||||||
state_data = state_data)
|
callback_data=callback_data,
|
||||||
|
state_data=state_data,
|
||||||
|
)
|
||||||
|
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
await state.set_data(state_data)
|
await state.set_data(state_data)
|
||||||
|
|
||||||
send_message = get_send_message(message)
|
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())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +1,75 @@
|
|||||||
from types import NoneType, UnionType
|
from aiogram.types import Message, CallbackQuery
|
||||||
from typing import get_args, get_origin
|
from decimal import Decimal
|
||||||
from aiogram.fsm.context import FSMContext
|
from datetime import datetime, time
|
||||||
from aiogram.types import InlineKeyboardButton
|
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|
||||||
|
|
||||||
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 ....model.settings import Settings
|
||||||
from ..context import ContextData, CallbackCommand, CommandContext
|
from ....model.user import UserBase
|
||||||
from ..navigation import get_navigation_context, pop_navigation_context
|
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,
|
async def show_editor(message: Message | CallbackQuery, **kwargs):
|
||||||
field_descriptor: EntityFieldDescriptor,
|
field_descriptor: EntityFieldDescriptor = kwargs["field_descriptor"]
|
||||||
callback_data: ContextData,
|
current_value = kwargs["current_value"]
|
||||||
state_data: dict):
|
user: UserBase = kwargs["user"]
|
||||||
|
callback_data: ContextData = kwargs.get("callback_data", None)
|
||||||
|
state_data: dict = kwargs["state_data"]
|
||||||
|
|
||||||
if callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT, CommandContext.ENTITY_FIELD_EDIT]:
|
value_type = field_descriptor.type_base
|
||||||
|
|
||||||
btns = []
|
if field_descriptor.edit_prompt:
|
||||||
entity_descriptor = field_descriptor.entity_descriptor
|
edit_prompt = get_callable_str(
|
||||||
field_index = (entity_descriptor.field_sequence.index(field_descriptor.name)
|
field_descriptor.edit_prompt, field_descriptor, None, current_value
|
||||||
if callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT]
|
)
|
||||||
else 0)
|
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)
|
||||||
|
|
||||||
stack, context = get_navigation_context(state_data = state_data)
|
kwargs["edit_prompt"] = edit_prompt
|
||||||
context = pop_navigation_context(stack)
|
|
||||||
|
|
||||||
if field_index > 0:
|
if value_type not in [int, float, Decimal, str]:
|
||||||
btns.append(InlineKeyboardButton(
|
state_data.update({"context_data": callback_data.pack()})
|
||||||
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:
|
if value_type is bool:
|
||||||
btns.append(InlineKeyboardButton(
|
await bool_editor(message=message, **kwargs)
|
||||||
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)
|
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)
|
||||||
|
|
||||||
keyboard_builder.row(InlineKeyboardButton(
|
elif value_type is time:
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_CANCEL_BTN)),
|
await time_picker(message=message, **kwargs)
|
||||||
callback_data = context.pack()))
|
|
||||||
|
|
||||||
elif callback_data.context == CommandContext.SETTING_EDIT:
|
elif issubclass(value_type, BotEntity) or issubclass(value_type, BotEnum):
|
||||||
|
await entity_picker(message=message, **kwargs)
|
||||||
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()))
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported field type: {value_type}")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||||
@@ -6,10 +6,11 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import TYPE_CHECKING
|
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 ..context import ContextData, CallbackCommand
|
||||||
from ..common import get_send_message, get_field_descriptor, get_entity_descriptor
|
from ....utils.main import get_send_message, get_field_descriptor
|
||||||
from .common import wrap_editor
|
from .wrapper import wrap_editor
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ....main import QBotApp
|
from ....main import QBotApp
|
||||||
@@ -19,154 +20,343 @@ logger = getLogger(__name__)
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
async def date_picker(message: Message | CallbackQuery,
|
@router.callback_query(ContextData.filter(F.command == CallbackCommand.TIME_PICKER))
|
||||||
field_descriptor: EntityFieldDescriptor,
|
async def time_picker_callback(
|
||||||
callback_data: ContextData,
|
query: CallbackQuery, callback_data: ContextData, app: "QBotApp", **kwargs
|
||||||
current_value: datetime,
|
):
|
||||||
state: FSMContext,
|
if not callback_data.data:
|
||||||
edit_prompt: str | None = None,
|
return
|
||||||
**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):
|
|
||||||
|
|
||||||
field_descriptor = get_field_descriptor(app, callback_data)
|
field_descriptor = get_field_descriptor(app, callback_data)
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
state_data = await state.get_data()
|
state_data = await state.get_data()
|
||||||
kwargs["state_data"] = state_data
|
kwargs["state_data"] = state_data
|
||||||
|
|
||||||
await date_picker(query.message,
|
await time_picker(
|
||||||
field_descriptor = field_descriptor,
|
query.message,
|
||||||
callback_data = callback_data,
|
field_descriptor=field_descriptor,
|
||||||
current_value = datetime.strptime(callback_data.data, "%Y-%m-%d"),
|
callback_data=callback_data,
|
||||||
**kwargs)
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,25 +1,32 @@
|
|||||||
from types import UnionType
|
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
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 sqlmodel import column
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
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.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.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 EntityFieldDescriptor, EntityDescriptor
|
from ....model.descriptors import EntityFieldDescriptor, Filter
|
||||||
from ....model import EntityPermission
|
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 ..context import ContextData, CallbackCommand
|
||||||
from ..common import (get_send_message, get_local_text, get_field_descriptor,
|
from ..common.pagination import add_pagination_controls
|
||||||
get_entity_descriptor, add_pagination_controls, add_filter_controls)
|
from ..common.filtering import add_filter_controls
|
||||||
from .common import wrap_editor
|
from .wrapper import wrap_editor
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ....main import QBotApp
|
from ....main import QBotApp
|
||||||
@@ -28,59 +35,55 @@ logger = getLogger(__name__)
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
async def entity_picker(message: Message | CallbackQuery,
|
async def entity_picker(
|
||||||
field_descriptor: EntityFieldDescriptor,
|
message: Message | CallbackQuery,
|
||||||
edit_prompt: str,
|
field_descriptor: EntityFieldDescriptor,
|
||||||
current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
|
edit_prompt: str,
|
||||||
**kwargs):
|
current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
state_data: dict = kwargs["state_data"]
|
state_data: dict = kwargs["state_data"]
|
||||||
|
|
||||||
state_data.update({"current_value": serialize(current_value, field_descriptor),
|
state_data.update(
|
||||||
"value": serialize(current_value, field_descriptor),
|
{
|
||||||
"edit_prompt": edit_prompt})
|
"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,
|
await render_entity_picker(
|
||||||
message = message,
|
field_descriptor=field_descriptor,
|
||||||
current_value = current_value,
|
message=message,
|
||||||
edit_prompt = edit_prompt,
|
current_value=current_value,
|
||||||
**kwargs)
|
edit_prompt=edit_prompt,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def calc_total_pages(items_count: int, page_size: int) -> int:
|
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 render_entity_picker(*,
|
async def render_entity_picker(
|
||||||
field_descriptor: EntityFieldDescriptor,
|
*,
|
||||||
message: Message | CallbackQuery,
|
field_descriptor: EntityFieldDescriptor,
|
||||||
callback_data: ContextData,
|
message: Message | CallbackQuery,
|
||||||
user: UserBase,
|
callback_data: ContextData,
|
||||||
db_session: AsyncSession,
|
user: UserBase,
|
||||||
state: FSMContext,
|
db_session: AsyncSession,
|
||||||
current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
|
state: FSMContext,
|
||||||
edit_prompt: str,
|
current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
|
||||||
page: int = 1,
|
edit_prompt: str,
|
||||||
**kwargs):
|
page: int = 1,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
if callback_data.command in [CallbackCommand.ENTITY_PICKER_PAGE, CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM]:
|
if callback_data.command in [
|
||||||
|
CallbackCommand.ENTITY_PICKER_PAGE,
|
||||||
|
CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM,
|
||||||
|
]:
|
||||||
page = int(callback_data.data.split("&")[0])
|
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
|
type_ = field_descriptor.type_base
|
||||||
is_list = field_descriptor.is_list
|
is_list = field_descriptor.is_list
|
||||||
|
|
||||||
@@ -88,114 +91,234 @@ async def render_entity_picker(*,
|
|||||||
raise ValueError("Unsupported type")
|
raise ValueError("Unsupported type")
|
||||||
|
|
||||||
page_size = await Settings.get(Settings.PAGE_SIZE)
|
page_size = await Settings.get(Settings.PAGE_SIZE)
|
||||||
|
form_list = None
|
||||||
|
|
||||||
if issubclass(type_, BotEnum):
|
if issubclass(type_, BotEnum):
|
||||||
items_count = len(type_.all_members)
|
items_count = len(type_.all_members)
|
||||||
total_pages = calc_total_pages(items_count, page_size)
|
total_pages = calc_total_pages(items_count, page_size)
|
||||||
page = min(page, total_pages)
|
page = min(page, total_pages)
|
||||||
enum_items = list(type_.all_members.values())[page_size * (page - 1):page_size * page]
|
enum_items = list(type_.all_members.values())[
|
||||||
items = [{"text": f"{"" if not is_list else "【✔︎】 " if item in (current_value or []) else "【 】 "}{item.localized(user.lang)}",
|
page_size * (page - 1) : page_size * page
|
||||||
"value": item.value} for item in enum_items]
|
]
|
||||||
else:
|
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)
|
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 form_list.filtering:
|
||||||
if (EntityPermission.LIST_ALL in permissions or
|
entity_filter = await ViewSetting.get_filter(
|
||||||
(EntityPermission.LIST in permissions and
|
session=db_session,
|
||||||
not issubclass(type_, OwnedBotEntity))):
|
user_id=user.id,
|
||||||
items_count = await type_.get_count(session = db_session, filter = entity_filter)
|
entity_name=type_.bot_entity_descriptor.class_name,
|
||||||
total_pages = calc_total_pages(items_count, page_size)
|
)
|
||||||
page = min(page, total_pages)
|
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(
|
entity_items = await type_.get_multi(
|
||||||
session = db_session, order_by = type_.name, filter = entity_filter,
|
session=db_session,
|
||||||
skip = page_size * (page - 1), limit = page_size)
|
order_by=form_list.order_by,
|
||||||
elif (EntityPermission.LIST in permissions and
|
static_filter=(
|
||||||
issubclass(type_, OwnedBotEntity)):
|
[
|
||||||
items_count = await type_.get_count_by_user(session = db_session, user_id = user.id, filter = entity_filter)
|
Filter(
|
||||||
total_pages = calc_total_pages(items_count, page_size)
|
field_name=f.field_name,
|
||||||
page = min(page, total_pages)
|
operator=f.operator,
|
||||||
entity_items = await type_.get_multi_by_user(
|
value_type="const",
|
||||||
session = db_session, user_id = user.id, order_by = type_.name, filter = entity_filter,
|
value=f.value,
|
||||||
skip = page_size * (page - 1), limit = page_size)
|
)
|
||||||
|
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:
|
else:
|
||||||
items_count = 0
|
items_count = 0
|
||||||
total_pages = 1
|
total_pages = 1
|
||||||
page = 1
|
page = 1
|
||||||
entity_items = list[BotEntity]()
|
entity_items = list[BotEntity]()
|
||||||
|
|
||||||
items = [{"text": f"{"" if not is_list else "【✔︎】 " if item in (current_value or []) else "【 】 "}{
|
items = [
|
||||||
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}",
|
"text": f"{
|
||||||
"value": str(item.id)} for item in entity_items]
|
''
|
||||||
|
if not is_list
|
||||||
# total_pages = items_count // page_size + (1 if items_count % page_size else 0)
|
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()
|
keyboard_builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(text = item["text"],
|
InlineKeyboardButton(
|
||||||
callback_data = ContextData(
|
text=item["text"],
|
||||||
command = CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM if is_list else CallbackCommand.FIELD_EDITOR_CALLBACK,
|
callback_data=ContextData(
|
||||||
context = callback_data.context,
|
command=(
|
||||||
entity_name = callback_data.entity_name,
|
CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM
|
||||||
entity_id = callback_data.entity_id,
|
if is_list
|
||||||
field_name = callback_data.field_name,
|
else CallbackCommand.FIELD_EDITOR_CALLBACK
|
||||||
data = f"{page}&{item['value']}" if is_list else item["value"],
|
),
|
||||||
save_state = True).pack()))
|
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(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
add_pagination_controls(keyboard_builder = keyboard_builder,
|
if form_list and form_list.pagination:
|
||||||
callback_data = callback_data,
|
add_pagination_controls(
|
||||||
total_pages = total_pages,
|
keyboard_builder=keyboard_builder,
|
||||||
command = CallbackCommand.ENTITY_PICKER_PAGE,
|
callback_data=callback_data,
|
||||||
page = page)
|
total_pages=total_pages,
|
||||||
|
command=CallbackCommand.ENTITY_PICKER_PAGE,
|
||||||
|
page=page,
|
||||||
|
)
|
||||||
|
|
||||||
if issubclass(type_, BotEntity):
|
if (
|
||||||
add_filter_controls(keyboard_builder = keyboard_builder,
|
issubclass(type_, BotEntity)
|
||||||
entity_descriptor = type_.bot_entity_descriptor,
|
and form_list.filtering
|
||||||
filter = entity_filter)
|
and form_list.filtering_fields
|
||||||
|
):
|
||||||
|
add_filter_controls(
|
||||||
|
keyboard_builder=keyboard_builder,
|
||||||
|
entity_descriptor=type_.bot_entity_descriptor,
|
||||||
|
filter=entity_filter,
|
||||||
|
)
|
||||||
|
|
||||||
if is_list:
|
if is_list:
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(text = await Settings.get(Settings.APP_STRINGS_DONE_BTN),
|
InlineKeyboardButton(
|
||||||
callback_data = ContextData(
|
text=await Settings.get(Settings.APP_STRINGS_DONE_BTN),
|
||||||
command = CallbackCommand.FIELD_EDITOR_CALLBACK,
|
callback_data=ContextData(
|
||||||
context = callback_data.context,
|
command=CallbackCommand.FIELD_EDITOR_CALLBACK,
|
||||||
entity_name = callback_data.entity_name,
|
context=callback_data.context,
|
||||||
entity_id = callback_data.entity_id,
|
entity_name=callback_data.entity_name,
|
||||||
field_name = callback_data.field_name,
|
entity_id=callback_data.entity_id,
|
||||||
save_state = True).pack()))
|
field_name=callback_data.field_name,
|
||||||
|
form_params=callback_data.form_params,
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
state_data = kwargs["state_data"]
|
state_data = kwargs["state_data"]
|
||||||
|
|
||||||
await wrap_editor(keyboard_builder = keyboard_builder,
|
await wrap_editor(
|
||||||
field_descriptor = field_descriptor,
|
keyboard_builder=keyboard_builder,
|
||||||
callback_data = callback_data,
|
field_descriptor=field_descriptor,
|
||||||
state_data = state_data)
|
callback_data=callback_data,
|
||||||
|
state_data=state_data,
|
||||||
|
)
|
||||||
|
|
||||||
await state.set_data(state_data)
|
await state.set_data(state_data)
|
||||||
|
|
||||||
send_message = get_send_message(message)
|
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(
|
||||||
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM))
|
ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_PAGE)
|
||||||
async def entity_picker_callback(query: CallbackQuery,
|
)
|
||||||
callback_data: ContextData,
|
@router.callback_query(
|
||||||
db_session: AsyncSession,
|
ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM)
|
||||||
app: "QBotApp",
|
)
|
||||||
state: FSMContext,
|
async def entity_picker_callback(
|
||||||
**kwargs):
|
query: CallbackQuery,
|
||||||
|
callback_data: ContextData,
|
||||||
|
db_session: AsyncSession,
|
||||||
|
app: "QBotApp",
|
||||||
|
state: FSMContext,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
state_data = await state.get_data()
|
state_data = await state.get_data()
|
||||||
kwargs["state_data"] = state_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"]
|
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:
|
if callback_data.command == CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM:
|
||||||
page, id_value = callback_data.data.split("&")
|
page, id_value = callback_data.data.split("&")
|
||||||
@@ -208,7 +331,7 @@ async def entity_picker_callback(query: CallbackQuery,
|
|||||||
else:
|
else:
|
||||||
value.append(item)
|
value.append(item)
|
||||||
else:
|
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:
|
if item in value:
|
||||||
value.remove(item)
|
value.remove(item)
|
||||||
else:
|
else:
|
||||||
@@ -222,15 +345,15 @@ async def entity_picker_callback(query: CallbackQuery,
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Unsupported command")
|
raise ValueError("Unsupported command")
|
||||||
|
|
||||||
await render_entity_picker(field_descriptor = field_descriptor,
|
await render_entity_picker(
|
||||||
message = query,
|
field_descriptor=field_descriptor,
|
||||||
callback_data = callback_data,
|
message=query,
|
||||||
current_value = value,
|
callback_data=callback_data,
|
||||||
edit_prompt = edit_prompt,
|
current_value=value,
|
||||||
db_session = db_session,
|
edit_prompt=edit_prompt,
|
||||||
app = app,
|
db_session=db_session,
|
||||||
state = state,
|
app=app,
|
||||||
page = page,
|
state=state,
|
||||||
**kwargs)
|
page=page,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|||||||
156
bot/handlers/editors/main.py
Normal file
156
bot/handlers/editors/main.py
Normal 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,
|
||||||
|
)
|
||||||
284
bot/handlers/editors/main_callbacks.py
Normal file
284
bot/handlers/editors/main_callbacks.py
Normal 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)
|
||||||
@@ -1,98 +1,97 @@
|
|||||||
from types import UnionType
|
|
||||||
from aiogram import Router
|
from aiogram import Router
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, CopyTextButton
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, CopyTextButton
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
from logging import getLogger
|
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.language import LanguageBase
|
||||||
from ....model.settings import Settings
|
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 ..context import ContextData, CallbackCommand
|
||||||
from ..common import get_send_message, get_local_text
|
from .wrapper import wrap_editor
|
||||||
from .common import wrap_editor
|
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
async def string_editor(message: Message | CallbackQuery,
|
async def string_editor(
|
||||||
field_descriptor: EntityFieldDescriptor,
|
message: Message | CallbackQuery,
|
||||||
callback_data: ContextData,
|
field_descriptor: EntityFieldDescriptor,
|
||||||
current_value: Any,
|
callback_data: ContextData,
|
||||||
edit_prompt: str,
|
current_value: Any,
|
||||||
state: FSMContext,
|
edit_prompt: str,
|
||||||
locale_index: int = 0,
|
state: FSMContext,
|
||||||
**kwargs):
|
locale_index: int = 0,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
keyboard_builder = InlineKeyboardBuilder()
|
keyboard_builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
state_data: dict = kwargs["state_data"]
|
state_data: dict = kwargs["state_data"]
|
||||||
|
|
||||||
_edit_prompt = edit_prompt
|
_edit_prompt = edit_prompt
|
||||||
|
|
||||||
# type_ = field_descriptor.type_
|
context_data = ContextData(
|
||||||
# type_origin = get_origin(type_)
|
command=CallbackCommand.FIELD_EDITOR_CALLBACK,
|
||||||
# if type_origin == UnionType:
|
context=callback_data.context,
|
||||||
# type_ = get_args(type_)[0]
|
entity_name=callback_data.entity_name,
|
||||||
|
entity_id=callback_data.entity_id,
|
||||||
if field_descriptor.type_base == str and field_descriptor.localizable:
|
field_name=callback_data.field_name,
|
||||||
|
form_params=callback_data.form_params,
|
||||||
|
)
|
||||||
|
|
||||||
|
if field_descriptor.type_base is str and field_descriptor.localizable:
|
||||||
current_locale = list(LanguageBase.all_members.values())[locale_index]
|
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(
|
_edit_prompt = f"{edit_prompt}\n{
|
||||||
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
|
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({
|
state_data.update(
|
||||||
"context_data": context_data.pack(),
|
{
|
||||||
"edit_prompt": edit_prompt,
|
"context_data": context_data.pack(),
|
||||||
"locale_index": str(locale_index),
|
"edit_prompt": edit_prompt,
|
||||||
"current_value": current_value})
|
"locale_index": str(locale_index),
|
||||||
|
"current_value": current_value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
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)
|
|
||||||
|
|
||||||
_current_value = serialize(current_value, field_descriptor)
|
_current_value = serialize(current_value, field_descriptor)
|
||||||
|
|
||||||
state_data.update({
|
state_data.update({"context_data": context_data.pack()})
|
||||||
"context_data": context_data.pack()})
|
|
||||||
|
|
||||||
if _current_value:
|
if _current_value:
|
||||||
|
_current_value_caption = (
|
||||||
_current_value_caption = f"{_current_value[:30]}..." if len(_current_value) > 30 else _current_value
|
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)))
|
keyboard_builder.row(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=_current_value_caption,
|
||||||
|
copy_text=CopyTextButton(text=_current_value),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
state_data = kwargs["state_data"]
|
state_data = kwargs["state_data"]
|
||||||
|
|
||||||
await wrap_editor(keyboard_builder = keyboard_builder,
|
await wrap_editor(
|
||||||
field_descriptor = field_descriptor,
|
keyboard_builder=keyboard_builder,
|
||||||
callback_data = callback_data,
|
field_descriptor=field_descriptor,
|
||||||
state_data = state_data)
|
callback_data=callback_data,
|
||||||
|
state_data=state_data,
|
||||||
|
)
|
||||||
|
|
||||||
await state.set_data(state_data)
|
await state.set_data(state_data)
|
||||||
|
|
||||||
send_message = get_send_message(message)
|
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())
|
||||||
|
|
||||||
# async def context_command_fiter(*args, **kwargs):
|
|
||||||
# print(args, kwargs)
|
|
||||||
# return True
|
|
||||||
|
|
||||||
|
|||||||
93
bot/handlers/editors/wrapper.py
Normal file
93
bot/handlers/editors/wrapper.py
Normal 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(),
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -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 import Router, F
|
||||||
from aiogram.filters import Command
|
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.fsm.state import StatesGroup, State
|
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
||||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
|
||||||
from aiogram.utils.i18n import I18n
|
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from ....model.bot_entity import BotEntity
|
from ....model.descriptors import FieldEditButton, CommandButton
|
||||||
from ....model.bot_enum import BotEnum
|
|
||||||
from ....model.owned_bot_entity import OwnedBotEntity
|
|
||||||
from ....model.settings import Settings
|
from ....model.settings import Settings
|
||||||
from ....model.user import UserBase
|
from ....model.user import UserBase
|
||||||
from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
|
|
||||||
from ....model import EntityPermission
|
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 ..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:
|
if TYPE_CHECKING:
|
||||||
from ....main import QBotApp
|
from ....main import QBotApp
|
||||||
@@ -30,136 +34,207 @@ router = Router()
|
|||||||
|
|
||||||
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_ITEM))
|
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_ITEM))
|
||||||
async def entity_item_callback(query: CallbackQuery, **kwargs):
|
async def entity_item_callback(query: CallbackQuery, **kwargs):
|
||||||
|
|
||||||
callback_data: ContextData = kwargs["callback_data"]
|
callback_data: ContextData = kwargs["callback_data"]
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
state_data = await state.get_data()
|
state_data = await state.get_data()
|
||||||
kwargs["state_data"] = state_data
|
kwargs["state_data"] = state_data
|
||||||
|
|
||||||
clear_state(state_data = state_data)
|
clear_state(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 entity_item(query = query, navigation_stack = stack, **kwargs)
|
await entity_item(query=query, navigation_stack=stack, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def entity_item(query: CallbackQuery,
|
async def entity_item(
|
||||||
callback_data: ContextData,
|
query: CallbackQuery,
|
||||||
db_session: AsyncSession,
|
callback_data: ContextData,
|
||||||
user: UserBase,
|
db_session: AsyncSession,
|
||||||
app: "QBotApp",
|
user: UserBase,
|
||||||
navigation_stack: list[ContextData],
|
app: "QBotApp",
|
||||||
**kwargs):
|
navigation_stack: list[ContextData],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
entity_descriptor = get_entity_descriptor(app, callback_data)
|
entity_descriptor = get_entity_descriptor(app, callback_data)
|
||||||
user_permissions = get_user_permissions(user, entity_descriptor)
|
# user_permissions = get_user_permissions(user, entity_descriptor)
|
||||||
entity_type: BotEntity = entity_descriptor.type_
|
entity_type = entity_descriptor.type_
|
||||||
|
|
||||||
keyboard_builder = InlineKeyboardBuilder()
|
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:
|
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
|
if not check_entity_permission(
|
||||||
EntityPermission.READ_ALL not in user_permissions):
|
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
|
form = entity_descriptor.forms.get(
|
||||||
EntityPermission.READ_ALL not in user_permissions and
|
callback_data.form_params or "default", entity_descriptor.default_form
|
||||||
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))
|
|
||||||
|
|
||||||
if can_edit:
|
if can_edit:
|
||||||
for edit_buttons_row in entity_descriptor.edit_buttons:
|
for edit_buttons_row in form.form_buttons:
|
||||||
btn_row = []
|
btn_row = []
|
||||||
for field_name in edit_buttons_row:
|
for button in edit_buttons_row:
|
||||||
field_name, btn_caption = field_name if isinstance(field_name, tuple) else (field_name, None)
|
if isinstance(button, FieldEditButton):
|
||||||
if field_name in entity_descriptor.fields_descriptors:
|
field_name = button.field_name
|
||||||
field_descriptor = entity_descriptor.fields_descriptors[field_name]
|
btn_caption = button.caption
|
||||||
# if field_descriptor.is_list and issubclass(field_descriptor.type_base, BotEntity):
|
if field_name in entity_descriptor.fields_descriptors:
|
||||||
# await field_descriptor.type_base.
|
field_descriptor = entity_descriptor.fields_descriptors[
|
||||||
field_value = getattr(entity_item, field_descriptor.field_name)
|
field_name
|
||||||
if btn_caption:
|
]
|
||||||
btn_text = get_callable_str(btn_caption, field_descriptor, entity_item, field_value)
|
field_value = getattr(entity_item, field_descriptor.field_name)
|
||||||
else:
|
if btn_caption:
|
||||||
if field_descriptor.type_base == bool:
|
btn_text = get_callable_str(
|
||||||
btn_text = f"{"【✔︎】 " if field_value else "【 】 "}{
|
btn_caption, field_descriptor, entity_item, field_value
|
||||||
get_callable_str(field_descriptor.caption, field_descriptor, entity_item, field_value) if field_descriptor.caption
|
)
|
||||||
else field_name}"
|
|
||||||
else:
|
else:
|
||||||
btn_text = (f"✏️ {get_callable_str(field_descriptor.caption, field_descriptor, entity_item, field_value)}"
|
if field_descriptor.type_base is bool:
|
||||||
if field_descriptor.caption else f"✏️ {field_name}")
|
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(
|
btn_row.append(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = btn_text,
|
text=btn_text,
|
||||||
callback_data = ContextData(
|
callback_data=(
|
||||||
command = CallbackCommand.FIELD_EDITOR,
|
button.context_data.pack()
|
||||||
context = CommandContext.ENTITY_FIELD_EDIT,
|
if button.context_data
|
||||||
entity_name = entity_descriptor.name,
|
else ContextData(
|
||||||
entity_id = str(entity_item.id),
|
command=CallbackCommand.USER_COMMAND,
|
||||||
field_name = field_name).pack()))
|
user_command=button.command,
|
||||||
|
data=str(entity_item.id),
|
||||||
|
).pack()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if btn_row:
|
if btn_row:
|
||||||
keyboard_builder.row(*btn_row)
|
keyboard_builder.row(*btn_row)
|
||||||
|
|
||||||
edit_delete_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(
|
edit_delete_row.append(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_EDIT_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_EDIT_BTN)),
|
||||||
callback_data = ContextData(
|
callback_data=ContextData(
|
||||||
command = CallbackCommand.FIELD_EDITOR,
|
command=CallbackCommand.FIELD_EDITOR,
|
||||||
context = CommandContext.ENTITY_EDIT,
|
context=CommandContext.ENTITY_EDIT,
|
||||||
entity_name = entity_descriptor.name,
|
entity_name=entity_descriptor.name,
|
||||||
entity_id = str(entity_item.id),
|
entity_id=str(entity_item.id),
|
||||||
field_name = entity_descriptor.field_sequence[0]).pack()))
|
form_params=callback_data.form_params,
|
||||||
|
field_name=form.edit_field_sequence[0],
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if can_delete:
|
if (
|
||||||
|
check_entity_permission(
|
||||||
|
entity=entity_item, user=user, permission=EntityPermission.DELETE
|
||||||
|
)
|
||||||
|
and form.show_delete_button
|
||||||
|
):
|
||||||
edit_delete_row.append(
|
edit_delete_row.append(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_DELETE_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_DELETE_BTN)),
|
||||||
callback_data = ContextData(
|
callback_data=ContextData(
|
||||||
command = CallbackCommand.ENTITY_DELETE,
|
command=CallbackCommand.ENTITY_DELETE,
|
||||||
entity_name = entity_descriptor.name,
|
entity_name=entity_descriptor.name,
|
||||||
entity_id = str(entity_item.id)).pack()))
|
form_params=callback_data.form_params,
|
||||||
|
entity_id=str(entity_item.id),
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if edit_delete_row:
|
if edit_delete_row:
|
||||||
keyboard_builder.row(*edit_delete_row)
|
keyboard_builder.row(*edit_delete_row)
|
||||||
|
|
||||||
entity_caption = get_callable_str(entity_descriptor.caption, entity_descriptor, entity_item)
|
if form.item_repr:
|
||||||
entity_item_name = get_local_text(entity_item.name, user.lang) if entity_descriptor.fields_descriptors["name"].localizable else entity_item.name
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
for field_descriptor in entity_descriptor.fields_descriptors.values():
|
item_text = f"<b><u><i>{entity_caption or entity_descriptor.name}:</i></u></b> <b>{entity_item_repr}</b>"
|
||||||
if field_descriptor.name == "name" or not field_descriptor.is_visible:
|
|
||||||
continue
|
for field_descriptor in entity_descriptor.fields_descriptors.values():
|
||||||
field_caption = get_callable_str(field_descriptor.caption, field_descriptor, entity_item)
|
if field_descriptor.is_visible:
|
||||||
value = get_value_repr(value = getattr(entity_item, field_descriptor.name),
|
field_caption = get_callable_str(
|
||||||
field_descriptor = field_descriptor,
|
field_descriptor.caption, field_descriptor, entity_item
|
||||||
locale = user.lang)
|
)
|
||||||
item_text += f"\n{field_caption or field_descriptor.name}:{f" <b>{value}</b>" if value else ""}"
|
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)
|
context = pop_navigation_context(navigation_stack)
|
||||||
if context:
|
if context:
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
||||||
callback_data = context.pack()))
|
callback_data=context.pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
state_data = kwargs["state_data"]
|
state_data = kwargs["state_data"]
|
||||||
@@ -167,67 +242,4 @@ async def entity_item(query: CallbackQuery,
|
|||||||
|
|
||||||
send_message = get_send_message(query)
|
send_message = get_send_message(query)
|
||||||
|
|
||||||
await send_message(text = item_text, reply_markup = keyboard_builder.as_markup())
|
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
|
|
||||||
|
|||||||
96
bot/handlers/forms/entity_form_callbacks.py
Normal file
96
bot/handlers/forms/entity_form_callbacks.py
Normal 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(),
|
||||||
|
)
|
||||||
@@ -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 import Router, F
|
||||||
from aiogram.filters import Command
|
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.fsm.state import StatesGroup, State
|
|
||||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||||
from aiogram.utils.i18n import I18n
|
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from ....model.bot_entity import BotEntity
|
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.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 EntityFieldDescriptor, EntityDescriptor
|
from ....model.descriptors import EntityDescriptor, Filter
|
||||||
from ....model import EntityPermission
|
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 ..context import ContextData, CallbackCommand, CommandContext
|
||||||
from ..common import (add_pagination_controls, get_local_text, get_entity_descriptor,
|
from ..common.pagination import add_pagination_controls
|
||||||
get_callable_str, get_send_message, add_filter_controls)
|
from ..common.filtering import add_filter_controls
|
||||||
|
from ..navigation import pop_navigation_context, save_navigation_context
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ....main import QBotApp
|
from ....main import QBotApp
|
||||||
@@ -32,7 +35,6 @@ router = Router()
|
|||||||
|
|
||||||
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_LIST))
|
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_LIST))
|
||||||
async def entity_list_callback(query: CallbackQuery, **kwargs):
|
async def entity_list_callback(query: CallbackQuery, **kwargs):
|
||||||
|
|
||||||
callback_data: ContextData = kwargs["callback_data"]
|
callback_data: ContextData = kwargs["callback_data"]
|
||||||
|
|
||||||
if callback_data.data == "skip":
|
if callback_data.data == "skip":
|
||||||
@@ -42,125 +44,217 @@ async def entity_list_callback(query: CallbackQuery, **kwargs):
|
|||||||
state_data = await state.get_data()
|
state_data = await state.get_data()
|
||||||
kwargs["state_data"] = state_data
|
kwargs["state_data"] = state_data
|
||||||
|
|
||||||
clear_state(state_data = state_data)
|
clear_state(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 entity_list(message=query, navigation_stack=stack, **kwargs)
|
||||||
|
|
||||||
await entity_list(message = query, navigation_stack = stack, **kwargs)
|
|
||||||
|
|
||||||
def calc_total_pages(items_count: int, page_size: int) -> int:
|
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")
|
page = int(callback_data.data or "1")
|
||||||
|
|
||||||
entity_descriptor = get_entity_descriptor(app, callback_data)
|
entity_descriptor = get_entity_descriptor(app, callback_data)
|
||||||
user_permissions = get_user_permissions(user, entity_descriptor)
|
user_permissions = get_user_permissions(user, entity_descriptor)
|
||||||
entity_type = entity_descriptor.type_
|
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()
|
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(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_ADD_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_ADD_BTN)),
|
||||||
callback_data = ContextData(
|
callback_data=ContextData(
|
||||||
command = CallbackCommand.FIELD_EDITOR,
|
command=CallbackCommand.FIELD_EDITOR,
|
||||||
context = CommandContext.ENTITY_CREATE,
|
context=CommandContext.ENTITY_CREATE,
|
||||||
entity_name = entity_descriptor.name,
|
entity_name=entity_descriptor.name,
|
||||||
field_name = entity_descriptor.field_sequence[0],
|
field_name=form_item.edit_field_sequence[0],
|
||||||
save_state = True).pack()))
|
form_params=form_list.item_form,
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
page_size = await Settings.get(Settings.PAGE_SIZE)
|
if form_list.filtering:
|
||||||
|
entity_filter = await ViewSetting.get_filter(
|
||||||
entity_filter = await ViewSetting.get_filter(session = db_session, user_id = user.id, entity_name = entity_descriptor.class_name)
|
session=db_session,
|
||||||
|
user_id=user.id,
|
||||||
if issubclass(entity_type, OwnedBotEntity):
|
entity_name=entity_descriptor.class_name,
|
||||||
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
|
|
||||||
else:
|
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:
|
for item in items:
|
||||||
if entity_descriptor.item_caption:
|
if form_list.item_repr:
|
||||||
caption = entity_descriptor.item_caption(entity_descriptor, item)
|
caption = form_list.item_repr(entity_descriptor, item)
|
||||||
elif entity_descriptor.fields_descriptors["name"].localizable:
|
elif entity_descriptor.item_repr:
|
||||||
caption = get_local_text(item.name, user.lang)
|
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:
|
else:
|
||||||
caption = item.name
|
caption = f"{entity_descriptor.name}: {item.id}"
|
||||||
|
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = caption,
|
text=caption,
|
||||||
callback_data = ContextData(
|
callback_data=ContextData(
|
||||||
command = CallbackCommand.ENTITY_ITEM,
|
command=CallbackCommand.ENTITY_ITEM,
|
||||||
entity_name = entity_descriptor.name,
|
entity_name=entity_descriptor.name,
|
||||||
entity_id = str(item.id)).pack()))
|
form_params=form_list.item_form,
|
||||||
|
entity_id=str(item.id),
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
add_pagination_controls(keyboard_builder = keyboard_builder,
|
if form_list.pagination:
|
||||||
callback_data = callback_data,
|
add_pagination_controls(
|
||||||
total_pages = total_pages,
|
keyboard_builder=keyboard_builder,
|
||||||
command = CallbackCommand.ENTITY_LIST,
|
callback_data=callback_data,
|
||||||
page = page)
|
total_pages=total_pages,
|
||||||
|
command=CallbackCommand.ENTITY_LIST,
|
||||||
|
page=page,
|
||||||
|
)
|
||||||
|
|
||||||
add_filter_controls(keyboard_builder = keyboard_builder,
|
if form_list.filtering and form_list.filtering_fields:
|
||||||
entity_descriptor = entity_descriptor,
|
add_filter_controls(
|
||||||
filter = entity_filter)
|
keyboard_builder=keyboard_builder,
|
||||||
|
entity_descriptor=entity_descriptor,
|
||||||
|
filter=entity_filter,
|
||||||
|
)
|
||||||
|
|
||||||
context = pop_navigation_context(navigation_stack)
|
context = pop_navigation_context(navigation_stack)
|
||||||
if context:
|
if context:
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
||||||
callback_data = context.pack()))
|
callback_data=context.pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if entity_descriptor.caption:
|
if form_list.caption:
|
||||||
entity_text = get_callable_str(entity_descriptor.caption_plural, entity_descriptor)
|
entity_text = get_callable_str(form_list.caption, entity_descriptor)
|
||||||
else:
|
else:
|
||||||
entity_text = entity_descriptor.name
|
if entity_descriptor.full_name_plural:
|
||||||
if entity_descriptor.description:
|
entity_text = get_callable_str(
|
||||||
entity_desciption = get_callable_str(entity_descriptor.description, entity_descriptor)
|
entity_descriptor.full_name_plural, entity_descriptor
|
||||||
else:
|
)
|
||||||
entity_desciption = None
|
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: FSMContext = kwargs["state"]
|
||||||
state_data = kwargs["state_data"]
|
state_data = kwargs["state_data"]
|
||||||
@@ -168,8 +262,4 @@ async def entity_list(message: CallbackQuery | Message,
|
|||||||
|
|
||||||
send_message = get_send_message(message)
|
send_message = get_send_message(message)
|
||||||
|
|
||||||
await send_message(text = f"{entity_text}{f"\n{entity_desciption}" if entity_desciption else ""}",
|
await send_message(text=entity_text, reply_markup=keyboard_builder.as_markup())
|
||||||
reply_markup = keyboard_builder.as_markup())
|
|
||||||
|
|
||||||
|
|
||||||
from ..navigation import pop_navigation_context, save_navigation_context, clear_state
|
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ 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 babel.support import LazyProxy
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from ....model.settings import Settings
|
from ....model.settings import Settings
|
||||||
from ....model.user import UserBase
|
|
||||||
from ..context import ContextData, CallbackCommand
|
from ..context import ContextData, CallbackCommand
|
||||||
from ..common import get_send_message
|
from ....utils.main import get_send_message
|
||||||
from ....model.descriptors import EntityCaptionCallable
|
from ....model.descriptors import EntityCaptionCallable
|
||||||
|
from ..navigation import save_navigation_context, pop_navigation_context
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ....main import QBotApp
|
from ....main import QBotApp
|
||||||
@@ -20,55 +19,63 @@ logger = getLogger(__name__)
|
|||||||
router = Router()
|
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):
|
async def menu_entry_entities(message: CallbackQuery, **kwargs):
|
||||||
|
|
||||||
callback_data: ContextData = kwargs["callback_data"]
|
callback_data: ContextData = kwargs["callback_data"]
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
state_data = await state.get_data()
|
state_data = await state.get_data()
|
||||||
kwargs["state_data"] = state_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,
|
async def entities_menu(
|
||||||
app: "QBotApp",
|
message: Message | CallbackQuery,
|
||||||
state: FSMContext,
|
app: "QBotApp",
|
||||||
navigation_stack: list[ContextData],
|
state: FSMContext,
|
||||||
**kwargs):
|
navigation_stack: list[ContextData],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
keyboard_builder = InlineKeyboardBuilder()
|
keyboard_builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
entity_metadata = app.entity_metadata
|
entity_metadata = app.entity_metadata
|
||||||
|
|
||||||
for entity in entity_metadata.entity_descriptors.values():
|
for entity in entity_metadata.entity_descriptors.values():
|
||||||
if entity.caption_plural.__class__ == EntityCaptionCallable:
|
if entity.full_name_plural.__class__ == EntityCaptionCallable:
|
||||||
caption = entity.caption_plural(entity) or entity.name
|
caption = entity.full_name_plural(entity) or entity.name
|
||||||
elif entity.caption_plural.__class__ == LazyProxy:
|
elif entity.full_name_plural.__class__ == LazyProxy:
|
||||||
caption = f"{f"{entity.icon} " if entity.icon else ""}{entity.caption_plural.value or entity.name}"
|
caption = f"{f'{entity.icon} ' if entity.icon else ''}{entity.full_name_plural.value or entity.name}"
|
||||||
else:
|
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(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = caption,
|
text=caption,
|
||||||
callback_data = ContextData(command = CallbackCommand.ENTITY_LIST, entity_name = entity.name).pack()))
|
callback_data=ContextData(
|
||||||
|
command=CallbackCommand.ENTITY_LIST, entity_name=entity.name
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
context = pop_navigation_context(navigation_stack)
|
context = pop_navigation_context(navigation_stack)
|
||||||
if context:
|
if context:
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
||||||
callback_data = context.pack()))
|
callback_data=context.pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
state_data = kwargs["state_data"]
|
state_data = kwargs["state_data"]
|
||||||
await state.set_data(state_data)
|
await state.set_data(state_data)
|
||||||
|
|
||||||
send_message = get_send_message(message)
|
send_message = get_send_message(message)
|
||||||
|
|
||||||
await send_message(text = (await Settings.get(Settings.APP_STRINGS_REFERENCES)), reply_markup = keyboard_builder.as_markup())
|
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
|
)
|
||||||
|
|||||||
@@ -1,64 +1,85 @@
|
|||||||
from aiogram import Router, F
|
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.fsm.context import FSMContext
|
||||||
from aiogram.utils.i18n import I18n
|
from aiogram.utils.i18n import I18n
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
from ..navigation import pop_navigation_context, save_navigation_context
|
||||||
from ....model.language import LanguageBase
|
from ....model.language import LanguageBase
|
||||||
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
|
from ..context import ContextData, CallbackCommand
|
||||||
from ..navigation import route_callback
|
from ..common.routing import route_callback
|
||||||
from ..common import get_send_message
|
from ....utils.main import get_send_message
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
router = Router()
|
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):
|
async def menu_entry_language(message: CallbackQuery, **kwargs):
|
||||||
|
|
||||||
callback_data: ContextData = kwargs["callback_data"]
|
callback_data: ContextData = kwargs["callback_data"]
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
state_data = await state.get_data()
|
state_data = await state.get_data()
|
||||||
kwargs["state_data"] = state_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,
|
async def language_menu(
|
||||||
navigation_stack: list[ContextData],
|
message: Message | CallbackQuery,
|
||||||
user: UserBase,
|
navigation_stack: list[ContextData],
|
||||||
**kwargs):
|
user: UserBase,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
send_message = get_send_message(message)
|
send_message = get_send_message(message)
|
||||||
|
|
||||||
inline_keyboard = [
|
inline_keyboard = [
|
||||||
[InlineKeyboardButton(text = locale.localized(user.lang),
|
[
|
||||||
callback_data = ContextData(command = CallbackCommand.SET_LANGUAGE,
|
InlineKeyboardButton(
|
||||||
data = str(locale)).pack())]
|
text=locale.localized(user.lang),
|
||||||
for locale in LanguageBase.all_members.values()]
|
callback_data=ContextData(
|
||||||
|
command=CallbackCommand.SET_LANGUAGE, data=str(locale)
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for locale in LanguageBase.all_members.values()
|
||||||
|
]
|
||||||
|
|
||||||
context = pop_navigation_context(navigation_stack)
|
context = pop_navigation_context(navigation_stack)
|
||||||
if context:
|
if context:
|
||||||
inline_keyboard.append([InlineKeyboardButton(text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
inline_keyboard.append(
|
||||||
callback_data = context.pack())])
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
||||||
|
callback_data=context.pack(),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
state_data = kwargs["state_data"]
|
state_data = kwargs["state_data"]
|
||||||
await state.set_data(state_data)
|
await state.set_data(state_data)
|
||||||
|
|
||||||
await send_message(text = (await Settings.get(Settings.APP_STRINGS_LANGUAGE)),
|
await send_message(
|
||||||
reply_markup = InlineKeyboardMarkup(inline_keyboard = inline_keyboard))
|
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))
|
@router.callback_query(ContextData.filter(F.command == CallbackCommand.SET_LANGUAGE))
|
||||||
async def set_language(message: CallbackQuery, **kwargs):
|
async def set_language(message: CallbackQuery, **kwargs):
|
||||||
|
|
||||||
user: UserBase = kwargs["user"]
|
user: UserBase = kwargs["user"]
|
||||||
callback_data: ContextData = kwargs["callback_data"]
|
callback_data: ContextData = kwargs["callback_data"]
|
||||||
db_session: AsyncSession = kwargs["db_session"]
|
db_session: AsyncSession = kwargs["db_session"]
|
||||||
@@ -73,6 +94,3 @@ async def set_language(message: CallbackQuery, **kwargs):
|
|||||||
i18n: I18n = kwargs["i18n"]
|
i18n: I18n = kwargs["i18n"]
|
||||||
with i18n.use_locale(user.lang):
|
with i18n.use_locale(user.lang):
|
||||||
await route_callback(message, **kwargs)
|
await route_callback(message, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
from ..navigation import pop_navigation_context, save_navigation_context
|
|
||||||
@@ -1,93 +1,88 @@
|
|||||||
from aiogram import Router, F
|
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.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 sqlmodel.ext.asyncio.session import AsyncSession
|
|
||||||
from ....model.settings import Settings
|
from ....model.settings import Settings
|
||||||
from ....model.user import UserBase
|
|
||||||
from ..context import ContextData, CallbackCommand
|
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__)
|
logger = getLogger(__name__)
|
||||||
router = Router()
|
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))
|
@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 = InlineKeyboardBuilder()
|
||||||
|
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_REFERENCES_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_REFERENCES_BTN)),
|
||||||
callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_ENTITIES).pack()))
|
callback_data=ContextData(
|
||||||
|
command=CallbackCommand.MENU_ENTRY_ENTITIES
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_SETTINGS_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_SETTINGS_BTN)),
|
||||||
callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_SETTINGS).pack()))
|
callback_data=ContextData(
|
||||||
|
command=CallbackCommand.MENU_ENTRY_SETTINGS
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
context = pop_navigation_context(navigation_stack)
|
context = pop_navigation_context(navigation_stack)
|
||||||
if context:
|
if context:
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
||||||
callback_data = context.pack()))
|
callback_data=context.pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
send_message = get_send_message(message)
|
send_message = get_send_message(message)
|
||||||
|
|
||||||
await send_message(text = (await Settings.get(Settings.APP_STRINGS_MAIN_NENU)),
|
await send_message(
|
||||||
reply_markup = keyboard_builder.as_markup())
|
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(
|
router.include_routers(
|
||||||
entities_router,
|
entities.router,
|
||||||
settings_router,
|
settings.router,
|
||||||
parameters_router,
|
parameters.router,
|
||||||
language_router,
|
language.router,
|
||||||
editors_router,
|
editor.router,
|
||||||
entity_list_router,
|
editor_callbacks.router,
|
||||||
entity_form_router,
|
entity_list.router,
|
||||||
common_router,
|
entity_form.router,
|
||||||
user_handlers_router
|
entity_form_callbacks.router,
|
||||||
|
filtering_callbacks.router,
|
||||||
|
user_handlers.router,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..navigation import save_navigation_context, pop_navigation_context, clear_state
|
|
||||||
@@ -1,79 +1,94 @@
|
|||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.filters import Command
|
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.fsm.state import StatesGroup, State
|
|
||||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||||
from aiogram.utils.i18n import I18n
|
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
||||||
|
|
||||||
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
|
||||||
|
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 ..navigation import save_navigation_context, pop_navigation_context
|
||||||
|
from ....auth import authorize_command
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
router = Router()
|
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):
|
async def menu_entry_parameters(message: CallbackQuery, **kwargs):
|
||||||
|
|
||||||
callback_data: ContextData = kwargs["callback_data"]
|
callback_data: ContextData = kwargs["callback_data"]
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
state_data = await state.get_data()
|
state_data = await state.get_data()
|
||||||
kwargs["state_data"] = state_data
|
kwargs["state_data"] = state_data
|
||||||
|
|
||||||
clear_state(state_data = state_data)
|
clear_state(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 parameters_menu(message = message, navigation_stack = stack, **kwargs)
|
await parameters_menu(message=message, navigation_stack=stack, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def parameters_menu(message: Message | CallbackQuery,
|
async def parameters_menu(
|
||||||
user: UserBase,
|
message: Message | CallbackQuery,
|
||||||
callback_data: ContextData,
|
user: UserBase,
|
||||||
navigation_stack: list[ContextData],
|
callback_data: ContextData,
|
||||||
**kwargs):
|
navigation_stack: list[ContextData],
|
||||||
|
**kwargs,
|
||||||
if not await authorize_command(user = user, callback_data = callback_data):
|
):
|
||||||
|
if not await authorize_command(user=user, callback_data=callback_data):
|
||||||
await message.answer(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
|
await message.answer(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
|
||||||
|
|
||||||
settings = await Settings.get_params()
|
settings = await Settings.get_params()
|
||||||
|
|
||||||
keyboard_builder = InlineKeyboardBuilder()
|
keyboard_builder = InlineKeyboardBuilder()
|
||||||
for key, value in settings.items():
|
for key, value in settings.items():
|
||||||
|
|
||||||
if not key.is_visible:
|
if not key.is_visible:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if key.caption_value:
|
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:
|
else:
|
||||||
if key.caption:
|
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:
|
else:
|
||||||
caption = key.name
|
caption = key.name
|
||||||
|
|
||||||
if key.type_ == bool:
|
if key.type_ is bool:
|
||||||
caption = f"{"【✔︎】" if value else "【 】"} {caption}"
|
caption = f"{'【✔︎】' if value else '【 】'} {caption}"
|
||||||
else:
|
else:
|
||||||
caption = f"{caption}: {get_value_repr(value = value, field_descriptor = key, locale = user.lang)}"
|
caption = f"{caption}: {get_value_repr(value=value, field_descriptor=key, locale=user.lang)}"
|
||||||
|
|
||||||
|
keyboard_builder.row(
|
||||||
keyboard_builder.row(InlineKeyboardButton(text = caption,
|
InlineKeyboardButton(
|
||||||
callback_data = ContextData(
|
text=caption,
|
||||||
command = CallbackCommand.FIELD_EDITOR,
|
callback_data=ContextData(
|
||||||
context = CommandContext.SETTING_EDIT,
|
command=CallbackCommand.FIELD_EDITOR,
|
||||||
field_name = key.name).pack()))
|
context=CommandContext.SETTING_EDIT,
|
||||||
|
field_name=key.name,
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
context = pop_navigation_context(navigation_stack)
|
context = pop_navigation_context(navigation_stack)
|
||||||
if context:
|
if context:
|
||||||
keyboard_builder.row(InlineKeyboardButton(text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
keyboard_builder.row(
|
||||||
callback_data = context.pack()))
|
InlineKeyboardButton(
|
||||||
|
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
||||||
|
callback_data=context.pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
state_data = kwargs["state_data"]
|
state_data = kwargs["state_data"]
|
||||||
@@ -81,8 +96,7 @@ async def parameters_menu(message: Message | CallbackQuery,
|
|||||||
|
|
||||||
send_message = get_send_message(message)
|
send_message = get_send_message(message)
|
||||||
|
|
||||||
await send_message(text = (await Settings.get(Settings.APP_STRINGS_PARAMETERS)), reply_markup = keyboard_builder.as_markup())
|
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
|
|
||||||
|
|||||||
@@ -1,58 +1,72 @@
|
|||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.filters import Command
|
|
||||||
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 logging import getLogger
|
from logging import getLogger
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
||||||
|
|
||||||
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_send_message
|
||||||
from ..context import ContextData, CallbackCommand
|
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
|
from ..navigation import save_navigation_context, pop_navigation_context
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
router = Router()
|
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):
|
async def menu_entry_settings(message: CallbackQuery, **kwargs):
|
||||||
|
|
||||||
callback_data: ContextData = kwargs["callback_data"]
|
callback_data: ContextData = kwargs["callback_data"]
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
state_data = await state.get_data()
|
state_data = await state.get_data()
|
||||||
kwargs["state_data"] = state_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,
|
async def settings_menu(
|
||||||
user: UserBase,
|
message: Message | CallbackQuery,
|
||||||
navigation_stack: list[ContextData],
|
user: UserBase,
|
||||||
**kwargs):
|
navigation_stack: list[ContextData],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
keyboard_builder = InlineKeyboardBuilder()
|
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(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_PARAMETERS_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_PARAMETERS_BTN)),
|
||||||
callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_PARAMETERS).pack()))
|
callback_data=ContextData(
|
||||||
|
command=CallbackCommand.MENU_ENTRY_PARAMETERS
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_LANGUAGE_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_LANGUAGE_BTN)),
|
||||||
callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_LANGUAGE).pack()))
|
callback_data=ContextData(
|
||||||
|
command=CallbackCommand.MENU_ENTRY_LANGUAGE
|
||||||
|
).pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
context = pop_navigation_context(navigation_stack)
|
context = pop_navigation_context(navigation_stack)
|
||||||
if context:
|
if context:
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
||||||
callback_data = context.pack()))
|
callback_data=context.pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
state_data = kwargs["state_data"]
|
state_data = kwargs["state_data"]
|
||||||
@@ -60,9 +74,7 @@ async def settings_menu(message: Message | CallbackQuery,
|
|||||||
|
|
||||||
send_message = get_send_message(message)
|
send_message = get_send_message(message)
|
||||||
|
|
||||||
|
await send_message(
|
||||||
|
text=(await Settings.get(Settings.APP_STRINGS_SETTINGS)),
|
||||||
await send_message(text = (await Settings.get(Settings.APP_STRINGS_SETTINGS)), reply_markup = keyboard_builder.as_markup())
|
reply_markup=keyboard_builder.as_markup(),
|
||||||
|
)
|
||||||
|
|
||||||
from ..navigation import pop_navigation_context, get_navigation_context
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from aiogram.fsm.context import FSMContext
|
|
||||||
from aiogram.types import Message, CallbackQuery
|
|
||||||
|
|
||||||
from .context import ContextData, CallbackCommand
|
from .context import ContextData, CallbackCommand
|
||||||
|
|
||||||
|
|
||||||
def save_navigation_context(callback_data: ContextData, state_data: dict) -> list[ContextData]:
|
def save_navigation_context(
|
||||||
stack = [ContextData.unpack(item) for item in state_data.get("navigation_stack", [])]
|
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")
|
data_nc = state_data.get("navigation_context")
|
||||||
navigation_context = ContextData.unpack(data_nc) if data_nc else None
|
navigation_context = ContextData.unpack(data_nc) if data_nc else None
|
||||||
if callback_data.back:
|
if callback_data.back:
|
||||||
@@ -13,9 +14,12 @@ def save_navigation_context(callback_data: ContextData, state_data: dict) -> lis
|
|||||||
if stack:
|
if stack:
|
||||||
stack.pop()
|
stack.pop()
|
||||||
else:
|
else:
|
||||||
if (stack and navigation_context and
|
if (
|
||||||
navigation_context.command == callback_data.command and
|
stack
|
||||||
navigation_context.command != CallbackCommand.USER_COMMAND):
|
and navigation_context
|
||||||
|
and navigation_context.command == callback_data.command
|
||||||
|
and navigation_context.command != CallbackCommand.USER_COMMAND
|
||||||
|
):
|
||||||
navigation_context = callback_data
|
navigation_context = callback_data
|
||||||
elif navigation_context:
|
elif navigation_context:
|
||||||
stack.append(navigation_context)
|
stack.append(navigation_context)
|
||||||
@@ -33,63 +37,12 @@ def pop_navigation_context(stack: list[ContextData]) -> ContextData | None:
|
|||||||
return data
|
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")
|
data_nc = state_data.get("navigation_context")
|
||||||
context = ContextData.unpack(data_nc) if data_nc else None
|
context = ContextData.unpack(data_nc) if data_nc else None
|
||||||
return ([ContextData.unpack(item) for item in state_data.get("navigation_stack", [])],
|
return (
|
||||||
context)
|
[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
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
|||||||
from ...main import QBotApp
|
from ...main import QBotApp
|
||||||
from ...model.settings import Settings
|
from ...model.settings import Settings
|
||||||
from ...model.language import LanguageBase
|
from ...model.language import LanguageBase
|
||||||
from .navigation import clear_state
|
from ...utils.main import clear_state
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
@@ -16,40 +16,56 @@ router = Router()
|
|||||||
|
|
||||||
|
|
||||||
@router.message(CommandStart())
|
@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()
|
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 = 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:
|
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:
|
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)
|
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)
|
|
||||||
except Exception as e:
|
|
||||||
|
|
||||||
logger.error("Error creating user", exc_info = True)
|
user = await User.create(
|
||||||
message.answer((await Settings.get(Settings.APP_STRINGS_INTERNAL_ERROR_P_ERROR)).format(error = str(e)))
|
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))
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if user.is_active:
|
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:
|
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)
|
await message.answer(msg_text)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
from dataclasses import dataclass, field
|
from typing import TYPE_CHECKING
|
||||||
from typing import Any, Callable, TYPE_CHECKING
|
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||||
from aiogram.fsm.context import FSMContext
|
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 ..context import ContextData, CallbackCommand
|
||||||
from ....model.user import UserBase
|
|
||||||
from ....model.settings import Settings
|
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:
|
if TYPE_CHECKING:
|
||||||
@@ -18,51 +20,22 @@ if TYPE_CHECKING:
|
|||||||
router = Router()
|
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("/"))
|
@router.message(F.text.startswith("/"))
|
||||||
async def command_text(message: Message, **kwargs):
|
async def command_text(message: Message, **kwargs):
|
||||||
|
|
||||||
str_command = message.text.lstrip("/")
|
str_command = message.text.lstrip("/")
|
||||||
callback_data = ContextData(command = CallbackCommand.USER_COMMAND,
|
callback_data = ContextData(
|
||||||
user_command = str_command)
|
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))
|
@router.callback_query(ContextData.filter(F.command == CallbackCommand.USER_COMMAND))
|
||||||
async def command_callback(message: CallbackQuery, **kwargs):
|
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):
|
async def command_handler(message: Message | CallbackQuery, **kwargs):
|
||||||
|
|
||||||
callback_data: ContextData = kwargs.pop("callback_data")
|
callback_data: ContextData = kwargs.pop("callback_data")
|
||||||
str_command = callback_data.user_command
|
str_command = callback_data.user_command
|
||||||
app: "QBotApp" = kwargs.pop("app")
|
app: "QBotApp" = kwargs.pop("app")
|
||||||
@@ -75,40 +48,43 @@ async def command_handler(message: Message | CallbackQuery, **kwargs):
|
|||||||
state_data = await state.get_data()
|
state_data = await state.get_data()
|
||||||
|
|
||||||
if command.register_navigation:
|
if command.register_navigation:
|
||||||
clear_state(state_data = state_data)
|
clear_state(state_data=state_data)
|
||||||
|
|
||||||
if command.clear_navigation:
|
if command.clear_navigation:
|
||||||
state_data.pop("navigation_stack", None)
|
state_data.pop("navigation_stack", None)
|
||||||
state_data.pop("navigation_context", None)
|
state_data.pop("navigation_context", None)
|
||||||
|
|
||||||
if command.register_navigation:
|
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](
|
callback_context = CommandCallbackContext[app.user_class](
|
||||||
message = message,
|
message=message,
|
||||||
callback_data = callback_data,
|
callback_data=callback_data,
|
||||||
db_session = kwargs.pop("db_session"),
|
db_session=kwargs.pop("db_session"),
|
||||||
user = kwargs.pop("user"),
|
user=kwargs.pop("user"),
|
||||||
app = app,
|
app=app,
|
||||||
state_data = state_data,
|
state_data=state_data,
|
||||||
state = state,
|
state=state,
|
||||||
i18n = kwargs.pop("i18n"),
|
i18n=kwargs.pop("i18n"),
|
||||||
kwargs = kwargs)
|
kwargs=kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
await command.handler(callback_context)
|
await command.handler(callback_context)
|
||||||
|
|
||||||
await state.set_data(state_data)
|
await state.set_data(state_data)
|
||||||
|
|
||||||
if command.register_navigation:
|
if command.register_navigation:
|
||||||
|
stack, navigation_context = get_navigation_context(state_data=state_data)
|
||||||
stack, navigation_context = get_navigation_context(state_data = state_data)
|
back_callback_data = pop_navigation_context(stack=stack)
|
||||||
back_callback_data = pop_navigation_context(stack = stack)
|
|
||||||
if back_callback_data:
|
if back_callback_data:
|
||||||
|
|
||||||
callback_context.keyboard_builder.row(
|
callback_context.keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
|
||||||
callback_data = back_callback_data.pack()))
|
callback_data=back_callback_data.pack(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
send_message = get_send_message(message)
|
send_message = get_send_message(message)
|
||||||
|
|
||||||
@@ -116,10 +92,11 @@ async def command_handler(message: Message | CallbackQuery, **kwargs):
|
|||||||
message = message.message
|
message = message.message
|
||||||
|
|
||||||
if callback_context.message_text:
|
if callback_context.message_text:
|
||||||
await send_message(text = callback_context.message_text,
|
await send_message(
|
||||||
reply_markup = callback_context.keyboard_builder.as_markup())
|
text=callback_context.message_text,
|
||||||
|
reply_markup=callback_context.keyboard_builder.as_markup(),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await message.edit_reply_markup(reply_markup = callback_context.keyboard_builder.as_markup())
|
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
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from babel.support import LazyProxy
|
|
||||||
from pydantic import computed_field, model_validator
|
from pydantic import computed_field, model_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from typing import Literal, Self
|
from typing import Literal, Self
|
||||||
@@ -6,11 +5,8 @@ import warnings
|
|||||||
|
|
||||||
|
|
||||||
class Config(BaseSettings):
|
class Config(BaseSettings):
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_file = ".env",
|
env_file=".env", env_ignore_empty=True, extra="ignore"
|
||||||
env_ignore_empty = True,
|
|
||||||
extra = "ignore"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SECRET_KEY: str = "changethis"
|
SECRET_KEY: str = "changethis"
|
||||||
@@ -35,14 +31,16 @@ class Config(BaseSettings):
|
|||||||
def API_DOMAIN(self) -> str:
|
def API_DOMAIN(self) -> str:
|
||||||
if self.ENVIRONMENT == "local":
|
if self.ENVIRONMENT == "local":
|
||||||
return self.DOMAIN
|
return self.DOMAIN
|
||||||
return f'api.{self.DOMAIN}'
|
return f"api.{self.DOMAIN}"
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def API_URL(self) -> str:
|
def API_URL(self) -> str:
|
||||||
if self.USE_NGROK:
|
if self.USE_NGROK:
|
||||||
return self.NGROK_URL
|
return self.NGROK_URL
|
||||||
return f"{"http" if self.ENVIRONMENT == "local" else "https"}://{self.API_DOMAIN}"
|
return (
|
||||||
|
f"{'http' if self.ENVIRONMENT == 'local' else 'https'}://{self.API_DOMAIN}"
|
||||||
|
)
|
||||||
|
|
||||||
API_PORT: int = 8000
|
API_PORT: int = 8000
|
||||||
|
|
||||||
@@ -50,7 +48,7 @@ class Config(BaseSettings):
|
|||||||
|
|
||||||
ADMIN_TELEGRAM_ID: int
|
ADMIN_TELEGRAM_ID: int
|
||||||
|
|
||||||
USE_NGROK : bool = False
|
USE_NGROK: bool = False
|
||||||
NGROK_AUTH_TOKEN: str = "changethis"
|
NGROK_AUTH_TOKEN: str = "changethis"
|
||||||
NGROK_URL: str = ""
|
NGROK_URL: str = ""
|
||||||
|
|
||||||
@@ -59,7 +57,7 @@ class Config(BaseSettings):
|
|||||||
def _check_default_secret(self, var_name: str, value: str | None) -> None:
|
def _check_default_secret(self, var_name: str, value: str | None) -> None:
|
||||||
if value == "changethis":
|
if value == "changethis":
|
||||||
message = (
|
message = (
|
||||||
f"The value of {var_name} is \"changethis\", "
|
f'The value of {var_name} is "changethis", '
|
||||||
"for security, please change it, at least for deployments."
|
"for security, please change it, at least for deployments."
|
||||||
)
|
)
|
||||||
if self.ENVIRONMENT == "local":
|
if self.ENVIRONMENT == "local":
|
||||||
@@ -77,4 +75,5 @@ class Config(BaseSettings):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
config = Config()
|
config = Config()
|
||||||
@@ -4,15 +4,16 @@ from sqlalchemy.orm import sessionmaker
|
|||||||
|
|
||||||
from ..config import config
|
from ..config import config
|
||||||
|
|
||||||
import logging
|
# import logging
|
||||||
logger = logging.getLogger('sqlalchemy.engine')
|
# logger = logging.getLogger('sqlalchemy.engine')
|
||||||
logger.setLevel(logging.DEBUG)
|
# logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
async_engine = create_async_engine(config.DATABASE_URI)
|
async_engine = create_async_engine(config.DATABASE_URI)
|
||||||
async_session = sessionmaker[AsyncSession](
|
async_session = sessionmaker[AsyncSession](
|
||||||
async_engine, class_=AsyncSession, expire_on_commit=False
|
async_engine, class_=AsyncSession, expire_on_commit=False
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_db() -> AsyncSession: # type: ignore
|
|
||||||
|
async def get_db() -> AsyncSession: # type: ignore
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
yield session
|
yield session
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
from aiogram.fsm.state import State
|
from aiogram.fsm.state import State
|
||||||
from aiogram.fsm.storage.base import BaseStorage, StorageKey, StateType, DefaultKeyBuilder, KeyBuilder
|
from aiogram.fsm.storage.base import (
|
||||||
|
BaseStorage,
|
||||||
|
StorageKey,
|
||||||
|
StateType,
|
||||||
|
DefaultKeyBuilder,
|
||||||
|
KeyBuilder,
|
||||||
|
)
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
import ujson as json
|
import ujson as json
|
||||||
@@ -9,22 +15,17 @@ from ..model.fsm_storage import FSMStorage
|
|||||||
|
|
||||||
|
|
||||||
class DbStorage(BaseStorage):
|
class DbStorage(BaseStorage):
|
||||||
|
def __init__(self, key_builder: KeyBuilder | None = None) -> None:
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
key_builder: KeyBuilder | None = None) -> None:
|
|
||||||
|
|
||||||
if key_builder is None:
|
if key_builder is None:
|
||||||
key_builder = DefaultKeyBuilder()
|
key_builder = DefaultKeyBuilder()
|
||||||
self.key_builder = key_builder
|
self.key_builder = key_builder
|
||||||
|
|
||||||
|
|
||||||
async def set_state(self, key: StorageKey, state: StateType = None) -> None:
|
async def set_state(self, key: StorageKey, state: StateType = None) -> None:
|
||||||
|
|
||||||
db_key = self.key_builder.build(key, "state")
|
db_key = self.key_builder.build(key, "state")
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
db_state = (await session.exec(
|
db_state = (
|
||||||
select(FSMStorage).where(FSMStorage.key == db_key))).first()
|
await session.exec(select(FSMStorage).where(FSMStorage.key == db_key))
|
||||||
|
).first()
|
||||||
|
|
||||||
if db_state:
|
if db_state:
|
||||||
if state is None:
|
if state is None:
|
||||||
@@ -32,53 +33,52 @@ class DbStorage(BaseStorage):
|
|||||||
else:
|
else:
|
||||||
db_state.value = state.state if isinstance(state, State) else state
|
db_state.value = state.state if isinstance(state, State) else state
|
||||||
elif state is not None:
|
elif state is not None:
|
||||||
db_state = FSMStorage(key = db_key, value = state.state if isinstance(state, State) else state)
|
db_state = FSMStorage(
|
||||||
|
key=db_key, value=state.state if isinstance(state, State) else state
|
||||||
|
)
|
||||||
session.add(db_state)
|
session.add(db_state)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
async def get_state(self, key: StorageKey) -> str | None:
|
async def get_state(self, key: StorageKey) -> str | None:
|
||||||
|
|
||||||
db_key = self.key_builder.build(key, "state")
|
db_key = self.key_builder.build(key, "state")
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
db_state = (await session.exec(
|
db_state = (
|
||||||
select(FSMStorage).where(FSMStorage.key == db_key))).first()
|
await session.exec(select(FSMStorage).where(FSMStorage.key == db_key))
|
||||||
|
).first()
|
||||||
return db_state.value if db_state else None
|
return db_state.value if db_state else None
|
||||||
|
|
||||||
|
|
||||||
async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None:
|
async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
db_key = self.key_builder.build(key, "data")
|
db_key = self.key_builder.build(key, "data")
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
db_data = (await session.exec(
|
db_data = (
|
||||||
select(FSMStorage).where(FSMStorage.key == db_key))).first()
|
await session.exec(select(FSMStorage).where(FSMStorage.key == db_key))
|
||||||
|
).first()
|
||||||
|
|
||||||
if db_data:
|
if db_data:
|
||||||
if not data:
|
if not data:
|
||||||
await session.delete(db_data)
|
await session.delete(db_data)
|
||||||
else:
|
else:
|
||||||
db_data.value = json.dumps(data, ensure_ascii = False)
|
db_data.value = json.dumps(data, ensure_ascii=False)
|
||||||
elif data:
|
elif data:
|
||||||
db_data = FSMStorage(key = db_key, value = json.dumps(data, ensure_ascii = False))
|
db_data = FSMStorage(
|
||||||
|
key=db_key, value=json.dumps(data, ensure_ascii=False)
|
||||||
|
)
|
||||||
session.add(db_data)
|
session.add(db_data)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
async def get_data(self, key: StorageKey) -> Dict[str, Any]:
|
async def get_data(self, key: StorageKey) -> Dict[str, Any]:
|
||||||
|
|
||||||
db_key = self.key_builder.build(key, "data")
|
db_key = self.key_builder.build(key, "data")
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
db_data = (await session.exec(
|
db_data = (
|
||||||
select(FSMStorage).where(FSMStorage.key == db_key))).first()
|
await session.exec(select(FSMStorage).where(FSMStorage.key == db_key))
|
||||||
|
).first()
|
||||||
return json.loads(db_data.value) if db_data else {}
|
return json.loads(db_data.value) if db_data else {}
|
||||||
|
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
return await super().close()
|
return await super().close()
|
||||||
|
|
||||||
|
|||||||
47
lifespan.py
47
lifespan.py
@@ -5,9 +5,9 @@ from logging import getLogger
|
|||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def default_lifespan(app: QBotApp):
|
async def default_lifespan(app: QBotApp):
|
||||||
|
|
||||||
logger.debug("starting qbot app")
|
logger.debug("starting qbot app")
|
||||||
|
|
||||||
if app.config.USE_NGROK:
|
if app.config.USE_NGROK:
|
||||||
@@ -19,30 +19,43 @@ async def default_lifespan(app: QBotApp):
|
|||||||
logger.error("pyngrok is not installed")
|
logger.error("pyngrok is not installed")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
tunnel = ngrok.connect(app.config.API_PORT, pyngrok_config = PyngrokConfig(auth_token = app.config.NGROK_AUTH_TOKEN))
|
tunnel = ngrok.connect(
|
||||||
|
app.config.API_PORT,
|
||||||
|
pyngrok_config=PyngrokConfig(auth_token=app.config.NGROK_AUTH_TOKEN),
|
||||||
|
)
|
||||||
app.config.NGROK_URL = tunnel.public_url
|
app.config.NGROK_URL = tunnel.public_url
|
||||||
|
|
||||||
commands_captions = dict[str, list[tuple[str, str]]]()
|
commands_captions = dict[str, list[tuple[str, str]]]()
|
||||||
|
|
||||||
for command_name, command in app.bot_commands.items():
|
for command_name, command in app.bot_commands.items():
|
||||||
if isinstance(command.caption, str):
|
if command.show_in_bot_commands:
|
||||||
if "default" not in commands_captions:
|
if isinstance(command.caption, str) or command.caption is None:
|
||||||
commands_captions["default"] = []
|
if "default" not in commands_captions:
|
||||||
commands_captions["default"].append((command_name, command.caption))
|
commands_captions["default"] = []
|
||||||
for locale, description in command.caption.items():
|
commands_captions["default"].append(
|
||||||
if locale not in commands_captions:
|
(command_name, command.caption or command_name)
|
||||||
commands_captions[locale] = []
|
)
|
||||||
commands_captions[locale].append((command_name, description))
|
else:
|
||||||
|
for locale, description in command.caption.items():
|
||||||
|
if locale not in commands_captions:
|
||||||
|
commands_captions[locale] = []
|
||||||
|
commands_captions[locale].append((command_name, description))
|
||||||
|
|
||||||
for locale, commands in commands_captions.items():
|
for locale, commands in commands_captions.items():
|
||||||
await app.bot.set_my_commands([BotCommand(command = command[0], description=command[1]) for command in commands],
|
await app.bot.set_my_commands(
|
||||||
language_code = None if locale == "default" else locale)
|
[
|
||||||
|
BotCommand(command=command[0], description=command[1])
|
||||||
|
for command in commands
|
||||||
|
],
|
||||||
|
language_code=None if locale == "default" else locale,
|
||||||
|
)
|
||||||
|
|
||||||
|
await app.bot.set_webhook(
|
||||||
await app.bot.set_webhook(url = f"{app.config.API_URL}/api/telegram/webhook",
|
url=f"{app.config.API_URL}/api/telegram/webhook",
|
||||||
drop_pending_updates = True,
|
drop_pending_updates=True,
|
||||||
allowed_updates = ['message', 'callback_query', 'pre_checkout_query'],
|
allowed_updates=["message", "callback_query", "pre_checkout_query"],
|
||||||
secret_token = app.bot_auth_token)
|
secret_token=app.bot_auth_token,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("qbot app started")
|
logger.info("qbot app started")
|
||||||
|
|
||||||
|
|||||||
107
main.py
107
main.py
@@ -1,24 +1,21 @@
|
|||||||
from functools import wraps
|
from typing import Annotated, Callable, Any
|
||||||
from typing import Annotated, Callable, Any, Union, override
|
|
||||||
from typing_extensions import Doc
|
from typing_extensions import Doc
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
from aiogram.filters import CommandStart
|
|
||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
from aiogram.types import Message, BotCommand
|
from aiogram.types import Message
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|
||||||
from aiogram.utils.callback_answer import CallbackAnswerMiddleware
|
from aiogram.utils.callback_answer import CallbackAnswerMiddleware
|
||||||
from aiogram.utils.i18n import I18n
|
from aiogram.utils.i18n import I18n
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.applications import Lifespan, AppType
|
from fastapi.applications import Lifespan, AppType
|
||||||
from logging import getLogger
|
|
||||||
from secrets import token_hex
|
from secrets import token_hex
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .fsm.db_storage import DbStorage
|
from .fsm.db_storage import DbStorage
|
||||||
from .middleware.telegram import AuthMiddleware, I18nMiddleware, ResetStateMiddleware
|
from .middleware.telegram import AuthMiddleware, I18nMiddleware
|
||||||
from .model.user import UserBase
|
from .model.user import UserBase
|
||||||
from .model.entity_metadata import EntityMetadata
|
from .model.entity_metadata import EntityMetadata
|
||||||
from .bot.handlers.user_handlers import Command, CommandCallbackContext
|
from .model.descriptors import BotCommand
|
||||||
|
from .router import Router
|
||||||
|
|
||||||
|
|
||||||
class QBotApp(FastAPI):
|
class QBotApp(FastAPI):
|
||||||
@@ -26,47 +23,67 @@ class QBotApp(FastAPI):
|
|||||||
Main class for the QBot application
|
Main class for the QBot application
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__[UserType: UserBase](self,
|
def __init__[UserType: UserBase](
|
||||||
user_class: Annotated[type[UserType], Doc(
|
self,
|
||||||
"User class that will be used in the application"
|
user_class: (
|
||||||
)] | None = None,
|
Annotated[
|
||||||
config: Config | None = None,
|
type[UserType], Doc("User class that will be used in the application")
|
||||||
bot_start: Annotated[Callable[[Annotated[Callable[[Message, Any], None], Doc(
|
]
|
||||||
"Default handler for the start command"
|
| None
|
||||||
)], Message, Any], None], Doc(
|
) = None,
|
||||||
"Handler for the start command"
|
config: Config | None = None,
|
||||||
)] | None = None,
|
bot_start: (
|
||||||
bot_commands: list[Command] | None = None,
|
Annotated[
|
||||||
lifespan: Lifespan[AppType] | None = None,
|
Callable[
|
||||||
*args,
|
[
|
||||||
**kwargs):
|
Annotated[
|
||||||
|
Callable[[Message, Any], None],
|
||||||
|
Doc("Default handler for the start command"),
|
||||||
|
],
|
||||||
|
Message,
|
||||||
|
Any,
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
Doc("Handler for the start command"),
|
||||||
|
]
|
||||||
|
| None
|
||||||
|
) = None,
|
||||||
|
lifespan: Lifespan[AppType] | None = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
if config is None:
|
if config is None:
|
||||||
config = Config()
|
config = Config()
|
||||||
|
|
||||||
if user_class is None:
|
if user_class is None:
|
||||||
from .model.default_user import DefaultUser
|
from .model.default_user import DefaultUser
|
||||||
|
|
||||||
user_class = DefaultUser
|
user_class = DefaultUser
|
||||||
|
|
||||||
self.user_class = user_class
|
self.user_class = user_class
|
||||||
self.entity_metadata: EntityMetadata = user_class.entity_metadata
|
self.entity_metadata: EntityMetadata = user_class.entity_metadata
|
||||||
self.config = config
|
self.config = config
|
||||||
self.lifespan = lifespan
|
self.lifespan = lifespan
|
||||||
self.bot = Bot(token = self.config.TELEGRAM_BOT_TOKEN, default = DefaultBotProperties(parse_mode = "HTML"))
|
self.bot = Bot(
|
||||||
|
token=self.config.TELEGRAM_BOT_TOKEN,
|
||||||
|
default=DefaultBotProperties(parse_mode="HTML"),
|
||||||
|
)
|
||||||
|
|
||||||
dp = Dispatcher(storage = DbStorage())
|
dp = Dispatcher(storage=DbStorage())
|
||||||
|
|
||||||
i18n = I18n(path = "locales", default_locale = "en", domain = "messages")
|
i18n = I18n(path="locales", default_locale="en", domain="messages")
|
||||||
i18n_middleware = I18nMiddleware(user_class = user_class, i18n = i18n)
|
i18n_middleware = I18nMiddleware(user_class=user_class, i18n=i18n)
|
||||||
i18n_middleware.setup(dp)
|
i18n_middleware.setup(dp)
|
||||||
# dp.callback_query.middleware(ResetStateMiddleware())
|
|
||||||
dp.callback_query.middleware(CallbackAnswerMiddleware())
|
dp.callback_query.middleware(CallbackAnswerMiddleware())
|
||||||
|
|
||||||
from .bot.handlers.start import router as start_router
|
from .bot.handlers.start import router as start_router
|
||||||
|
|
||||||
dp.include_router(start_router)
|
dp.include_router(start_router)
|
||||||
|
|
||||||
from .bot.handlers.menu.main import router as main_menu_router
|
from .bot.handlers.menu.main import router as main_menu_router
|
||||||
auth = AuthMiddleware(user_class = user_class)
|
|
||||||
|
auth = AuthMiddleware(user_class=user_class)
|
||||||
main_menu_router.message.middleware.register(auth)
|
main_menu_router.message.middleware.register(auth)
|
||||||
main_menu_router.callback_query.middleware.register(auth)
|
main_menu_router.callback_query.middleware.register(auth)
|
||||||
dp.include_router(main_menu_router)
|
dp.include_router(main_menu_router)
|
||||||
@@ -76,35 +93,17 @@ class QBotApp(FastAPI):
|
|||||||
self.bot_auth_token = token_hex(128)
|
self.bot_auth_token = token_hex(128)
|
||||||
|
|
||||||
self.start_handler = bot_start
|
self.start_handler = bot_start
|
||||||
self.bot_commands = {c.name: c for c in bot_commands or []}
|
self.bot_commands = dict[str, BotCommand]()
|
||||||
|
|
||||||
from .lifespan import default_lifespan
|
from .lifespan import default_lifespan
|
||||||
|
|
||||||
super().__init__(lifespan = default_lifespan, *args, **kwargs)
|
super().__init__(lifespan=default_lifespan, *args, **kwargs)
|
||||||
|
|
||||||
from .api_route.telegram import router as telegram_router
|
from .api_route.telegram import router as telegram_router
|
||||||
self.include_router(telegram_router, prefix = "/api/telegram", tags = ["telegram"])
|
|
||||||
|
|
||||||
@override
|
self.include_router(telegram_router, prefix="/api/telegram", tags=["telegram"])
|
||||||
def bot_command(self, command: Command): ...
|
|
||||||
|
|
||||||
@override
|
def register_routers(self, *routers: Router):
|
||||||
def bot_command(self, command: str, caption: str | dict[str, str] | None = None): ...
|
for router in routers:
|
||||||
|
for command_name, command in router._commands.items():
|
||||||
def bot_command(self, command: str | Command, caption: str | dict[str, str] | None = None):
|
self.bot_commands[command_name] = command
|
||||||
"""
|
|
||||||
Decorator for registering bot commands
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(func: Callable[[CommandCallbackContext], None]):
|
|
||||||
|
|
||||||
if isinstance(command, str):
|
|
||||||
command = Command(name = command, handler = func, caption = caption)
|
|
||||||
self.bot_commands[command.name] = command
|
|
||||||
|
|
||||||
wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
from .auth import AuthMiddleware
|
from .auth import AuthMiddleware as AuthMiddleware
|
||||||
from .i18n import I18nMiddleware
|
from .i18n import I18nMiddleware as I18nMiddleware
|
||||||
from .reset_state import ResetStateMiddleware
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from aiogram import BaseMiddleware
|
from aiogram import BaseMiddleware
|
||||||
from aiogram.types import TelegramObject, Message, CallbackQuery
|
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||||
from aiogram.utils.i18n import gettext as _
|
|
||||||
from babel.support import LazyProxy
|
from babel.support import LazyProxy
|
||||||
from typing import Any, Awaitable, Callable, Dict
|
from typing import Any, Awaitable, Callable, Dict
|
||||||
|
|
||||||
@@ -8,23 +7,25 @@ from ...model.user import UserBase
|
|||||||
|
|
||||||
|
|
||||||
class AuthMiddleware(BaseMiddleware):
|
class AuthMiddleware(BaseMiddleware):
|
||||||
|
def __init__[UserType: UserBase](
|
||||||
def __init__[UserType: UserBase](self,
|
self,
|
||||||
user_class: type[UserType],
|
user_class: type[UserType],
|
||||||
not_authenticated_msg: LazyProxy | str = "not authenticated",
|
not_authenticated_msg: LazyProxy | str = "not authenticated",
|
||||||
not_active_msg: LazyProxy | str = "not active"):
|
not_active_msg: LazyProxy | str = "not active",
|
||||||
|
):
|
||||||
self.user_class = user_class
|
self.user_class = user_class
|
||||||
self.not_authenticated_msg = not_authenticated_msg
|
self.not_authenticated_msg = not_authenticated_msg
|
||||||
self.not_active_msg = not_active_msg
|
self.not_active_msg = not_active_msg
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
async def __call__(self,
|
self,
|
||||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
event: TelegramObject,
|
event: TelegramObject,
|
||||||
data: Dict[str, Any]) -> Any:
|
data: Dict[str, Any],
|
||||||
|
) -> Any:
|
||||||
user = await self.user_class.get(id = event.from_user.id, session = data["db_session"])
|
user = await self.user_class.get(
|
||||||
|
id=event.from_user.id, session=data["db_session"]
|
||||||
|
)
|
||||||
|
|
||||||
if user and user.is_active:
|
if user and user.is_active:
|
||||||
data["user"] = user
|
data["user"] = user
|
||||||
@@ -34,7 +35,3 @@ class AuthMiddleware(BaseMiddleware):
|
|||||||
if user and not user.is_active:
|
if user and not user.is_active:
|
||||||
return await event.answer(self.not_active_msg)
|
return await event.answer(self.not_active_msg)
|
||||||
return await event.answer(self.not_authenticated_msg)
|
return await event.answer(self.not_authenticated_msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -5,10 +5,6 @@ from ...model.user import UserBase
|
|||||||
|
|
||||||
|
|
||||||
class I18nMiddleware(SimpleI18nMiddleware):
|
class I18nMiddleware(SimpleI18nMiddleware):
|
||||||
"""
|
|
||||||
This middleware stores locale in the FSM storage
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__[UserType: UserBase](
|
def __init__[UserType: UserBase](
|
||||||
self,
|
self,
|
||||||
user_class: type[UserType],
|
user_class: type[UserType],
|
||||||
@@ -22,7 +18,7 @@ class I18nMiddleware(SimpleI18nMiddleware):
|
|||||||
async def get_locale(self, event: TelegramObject, data: Dict[str, Any]) -> str:
|
async def get_locale(self, event: TelegramObject, data: Dict[str, Any]) -> str:
|
||||||
db_session = data.get("db_session")
|
db_session = data.get("db_session")
|
||||||
if db_session and event.model_fields.get("from_user"):
|
if db_session and event.model_fields.get("from_user"):
|
||||||
user = await self.user_class.get(id = event.from_user.id, session = db_session)
|
user = await self.user_class.get(id=event.from_user.id, session=db_session)
|
||||||
if user and user.lang:
|
if user and user.lang:
|
||||||
return user.lang
|
return user.lang
|
||||||
return await super().get_locale(event=event, data=data)
|
return await super().get_locale(event=event, data=data)
|
||||||
@@ -1,30 +1,26 @@
|
|||||||
from typing import Any, Awaitable, Callable, Dict
|
# from typing import Any, Awaitable, Callable, Dict
|
||||||
from aiogram import BaseMiddleware
|
# from aiogram import BaseMiddleware
|
||||||
from aiogram.types import TelegramObject
|
# from aiogram.types import TelegramObject
|
||||||
from aiogram.fsm.context import FSMContext
|
# from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.utils.i18n import gettext as _
|
# from aiogram.utils.i18n import gettext as _
|
||||||
|
|
||||||
from ...bot.handlers.context import ContextData
|
# from ...bot.handlers.context import ContextData
|
||||||
|
|
||||||
|
|
||||||
class ResetStateMiddleware(BaseMiddleware):
|
# class ResetStateMiddleware(BaseMiddleware):
|
||||||
async def __call__(self,
|
# async def __call__(self,
|
||||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
# handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
event: TelegramObject,
|
# event: TelegramObject,
|
||||||
data: Dict[str, Any]) -> Any:
|
# data: Dict[str, Any]) -> Any:
|
||||||
|
|
||||||
save_state = False
|
|
||||||
callback_data = data.get("callback_data")
|
|
||||||
if isinstance(callback_data, ContextData):
|
|
||||||
save_state = callback_data.save_state
|
|
||||||
|
|
||||||
if not save_state:
|
|
||||||
state = data.get("state")
|
|
||||||
if isinstance(state, FSMContext):
|
|
||||||
await state.clear()
|
|
||||||
|
|
||||||
return await handler(event, data)
|
|
||||||
|
|
||||||
|
|
||||||
|
# save_state = False
|
||||||
|
# callback_data = data.get("callback_data")
|
||||||
|
# if isinstance(callback_data, ContextData):
|
||||||
|
# save_state = callback_data.save_state
|
||||||
|
|
||||||
|
# if not save_state:
|
||||||
|
# state = data.get("state")
|
||||||
|
# if isinstance(state, FSMContext):
|
||||||
|
# await state.clear()
|
||||||
|
|
||||||
|
# return await handler(event, data)
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ from .bot_enum import BotEnum, EnumMember
|
|||||||
from ..db import async_session
|
from ..db import async_session
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class EntityPermission(BotEnum):
|
class EntityPermission(BotEnum):
|
||||||
|
|
||||||
LIST = EnumMember("list")
|
LIST = EnumMember("list")
|
||||||
READ = EnumMember("read")
|
READ = EnumMember("read")
|
||||||
CREATE = EnumMember("create")
|
CREATE = EnumMember("create")
|
||||||
@@ -23,24 +21,23 @@ class EntityPermission(BotEnum):
|
|||||||
|
|
||||||
|
|
||||||
def session_dep(func):
|
def session_dep(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrapper(cls, *args, **kwargs):
|
async def wrapper(cls, *args, **kwargs):
|
||||||
|
if "session" in kwargs and kwargs["session"]:
|
||||||
|
return await func(cls, *args, **kwargs)
|
||||||
|
|
||||||
if "session" in kwargs and kwargs["session"]:
|
_session = None
|
||||||
|
|
||||||
|
state = cast(InstanceState, inspect(cls))
|
||||||
|
if hasattr(state, "async_session"):
|
||||||
|
_session = state.async_session
|
||||||
|
|
||||||
|
if not _session:
|
||||||
|
async with async_session() as session:
|
||||||
|
kwargs["session"] = session
|
||||||
return await func(cls, *args, **kwargs)
|
return await func(cls, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
kwargs["session"] = _session
|
||||||
|
return await func(cls, *args, **kwargs)
|
||||||
|
|
||||||
_session = None
|
return wrapper
|
||||||
|
|
||||||
state = cast(InstanceState, inspect(cls))
|
|
||||||
if hasattr(state, "async_session"):
|
|
||||||
_session = state.async_session
|
|
||||||
|
|
||||||
if not _session:
|
|
||||||
async with async_session() as session:
|
|
||||||
kwargs["session"] = session
|
|
||||||
return await func(cls, *args, **kwargs)
|
|
||||||
else:
|
|
||||||
kwargs["session"] = _session
|
|
||||||
return await func(cls, *args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
class Singleton(type):
|
class Singleton(type):
|
||||||
_instances = {}
|
_instances = {}
|
||||||
|
|
||||||
def __call__(cls, *args, **kwargs):
|
def __call__(cls, *args, **kwargs):
|
||||||
if cls not in cls._instances:
|
if cls not in cls._instances:
|
||||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||||
|
|||||||
@@ -1,33 +1,49 @@
|
|||||||
from functools import wraps
|
|
||||||
from types import NoneType, UnionType
|
from types import NoneType, UnionType
|
||||||
from typing import ClassVar, ForwardRef, Optional, Union, cast, get_args, get_origin
|
from typing import (
|
||||||
|
Any,
|
||||||
|
ClassVar,
|
||||||
|
ForwardRef,
|
||||||
|
Optional,
|
||||||
|
Self,
|
||||||
|
Union,
|
||||||
|
get_args,
|
||||||
|
get_origin,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlmodel import SQLModel, BIGINT, Field, select, func, column
|
from sqlmodel import SQLModel, BIGINT, Field, select, func, column
|
||||||
|
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
from sqlmodel.sql.expression import SelectOfScalar
|
||||||
from sqlmodel.main import SQLModelMetaclass, RelationshipInfo
|
from sqlmodel.main import SQLModelMetaclass, RelationshipInfo
|
||||||
|
|
||||||
from .descriptors import EntityDescriptor, EntityField, EntityFieldDescriptor
|
from .descriptors import EntityDescriptor, EntityField, EntityFieldDescriptor, Filter
|
||||||
from .entity_metadata import EntityMetadata
|
from .entity_metadata import EntityMetadata
|
||||||
from . import session_dep
|
from . import session_dep
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .user import UserBase
|
||||||
|
|
||||||
|
|
||||||
class BotEntityMetaclass(SQLModelMetaclass):
|
class BotEntityMetaclass(SQLModelMetaclass):
|
||||||
|
|
||||||
__future_references__ = {}
|
__future_references__ = {}
|
||||||
|
|
||||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||||
|
|
||||||
bot_fields_descriptors = {}
|
bot_fields_descriptors = {}
|
||||||
|
|
||||||
if bases:
|
if bases:
|
||||||
bot_entity_descriptor = bases[0].__dict__.get('bot_entity_descriptor')
|
bot_entity_descriptor = bases[0].__dict__.get("bot_entity_descriptor")
|
||||||
bot_fields_descriptors = {key: EntityFieldDescriptor(**value.__dict__.copy())
|
bot_fields_descriptors = (
|
||||||
for key, value in bot_entity_descriptor.fields_descriptors.items()} if bot_entity_descriptor else {}
|
{
|
||||||
|
key: EntityFieldDescriptor(**value.__dict__.copy())
|
||||||
if '__annotations__' in namespace:
|
for key, value in bot_entity_descriptor.fields_descriptors.items()
|
||||||
|
}
|
||||||
for annotation in namespace['__annotations__']:
|
if bot_entity_descriptor
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "__annotations__" in namespace:
|
||||||
|
for annotation in namespace["__annotations__"]:
|
||||||
if annotation in ["bot_entity_descriptor", "entity_metadata"]:
|
if annotation in ["bot_entity_descriptor", "entity_metadata"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -40,7 +56,6 @@ class BotEntityMetaclass(SQLModelMetaclass):
|
|||||||
descriptor_name = annotation
|
descriptor_name = annotation
|
||||||
|
|
||||||
if attribute_value:
|
if attribute_value:
|
||||||
|
|
||||||
if isinstance(attribute_value, EntityField):
|
if isinstance(attribute_value, EntityField):
|
||||||
descriptor_kwargs = attribute_value.__dict__.copy()
|
descriptor_kwargs = attribute_value.__dict__.copy()
|
||||||
sm_descriptor = descriptor_kwargs.pop("sm_descriptor", None)
|
sm_descriptor = descriptor_kwargs.pop("sm_descriptor", None)
|
||||||
@@ -52,25 +67,28 @@ class BotEntityMetaclass(SQLModelMetaclass):
|
|||||||
|
|
||||||
descriptor_name = descriptor_kwargs.pop("name") or annotation
|
descriptor_name = descriptor_kwargs.pop("name") or annotation
|
||||||
|
|
||||||
type_ = namespace['__annotations__'][annotation]
|
type_ = namespace["__annotations__"][annotation]
|
||||||
|
|
||||||
type_origin = get_origin(type_)
|
type_origin = get_origin(type_)
|
||||||
|
|
||||||
field_descriptor = EntityFieldDescriptor(
|
field_descriptor = EntityFieldDescriptor(
|
||||||
name = descriptor_name,
|
name=descriptor_name,
|
||||||
field_name = annotation,
|
field_name=annotation,
|
||||||
type_ = type_,
|
type_=type_,
|
||||||
type_base = type_,
|
type_base=type_,
|
||||||
**descriptor_kwargs)
|
**descriptor_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
is_list = False
|
is_list = False
|
||||||
if type_origin == list:
|
if type_origin is list:
|
||||||
field_descriptor.is_list = is_list = True
|
field_descriptor.is_list = is_list = True
|
||||||
field_descriptor.type_base = type_ = get_args(type_)[0]
|
field_descriptor.type_base = type_ = get_args(type_)[0]
|
||||||
|
|
||||||
if type_origin == Union and isinstance(get_args(type_)[0], ForwardRef):
|
if type_origin == Union and isinstance(get_args(type_)[0], ForwardRef):
|
||||||
field_descriptor.is_optional = True
|
field_descriptor.is_optional = True
|
||||||
field_descriptor.type_base = type_ = get_args(type_)[0].__forward_arg__
|
field_descriptor.type_base = type_ = get_args(type_)[
|
||||||
|
0
|
||||||
|
].__forward_arg__
|
||||||
|
|
||||||
if type_origin == UnionType and get_args(type_)[1] == NoneType:
|
if type_origin == UnionType and get_args(type_)[1] == NoneType:
|
||||||
field_descriptor.is_optional = True
|
field_descriptor.is_optional = True
|
||||||
@@ -78,12 +96,26 @@ class BotEntityMetaclass(SQLModelMetaclass):
|
|||||||
|
|
||||||
if isinstance(type_, str):
|
if isinstance(type_, str):
|
||||||
type_not_found = True
|
type_not_found = True
|
||||||
for entity_descriptor in EntityMetadata().entity_descriptors.values():
|
for (
|
||||||
|
entity_descriptor
|
||||||
|
) in EntityMetadata().entity_descriptors.values():
|
||||||
if type_ == entity_descriptor.class_name:
|
if type_ == entity_descriptor.class_name:
|
||||||
field_descriptor.type_ = (list[entity_descriptor.type_] if is_list
|
field_descriptor.type_ = (
|
||||||
else Optional[entity_descriptor.type_] if type_origin == Optional
|
list[entity_descriptor.type_]
|
||||||
else entity_descriptor.type_ | None if (type_origin == UnionType and get_args(type_)[1] == NoneType)
|
if is_list
|
||||||
else entity_descriptor.type_)
|
else (
|
||||||
|
Optional[entity_descriptor.type_]
|
||||||
|
if type_origin == Optional
|
||||||
|
else (
|
||||||
|
entity_descriptor.type_ | None
|
||||||
|
if (
|
||||||
|
type_origin == UnionType
|
||||||
|
and get_args(type_)[1] == NoneType
|
||||||
|
)
|
||||||
|
else entity_descriptor.type_
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
type_not_found = False
|
type_not_found = False
|
||||||
break
|
break
|
||||||
if type_not_found:
|
if type_not_found:
|
||||||
@@ -101,27 +133,38 @@ class BotEntityMetaclass(SQLModelMetaclass):
|
|||||||
descriptor_kwargs: dict = entity_descriptor.__dict__.copy()
|
descriptor_kwargs: dict = entity_descriptor.__dict__.copy()
|
||||||
descriptor_name = descriptor_kwargs.pop("name", None)
|
descriptor_name = descriptor_kwargs.pop("name", None)
|
||||||
descriptor_name = descriptor_name or name.lower()
|
descriptor_name = descriptor_name or name.lower()
|
||||||
descriptor_fields_sequence = descriptor_kwargs.pop("field_sequence", None)
|
|
||||||
if not descriptor_fields_sequence:
|
|
||||||
descriptor_fields_sequence = list(bot_fields_descriptors.keys())
|
|
||||||
descriptor_fields_sequence.remove("id")
|
|
||||||
namespace["bot_entity_descriptor"] = EntityDescriptor(
|
namespace["bot_entity_descriptor"] = EntityDescriptor(
|
||||||
name = descriptor_name,
|
name=descriptor_name,
|
||||||
class_name = name,
|
class_name=name,
|
||||||
type_ = name,
|
type_=name,
|
||||||
fields_descriptors = bot_fields_descriptors,
|
fields_descriptors=bot_fields_descriptors,
|
||||||
field_sequence = descriptor_fields_sequence,
|
**descriptor_kwargs,
|
||||||
**descriptor_kwargs)
|
)
|
||||||
else:
|
else:
|
||||||
descriptor_fields_sequence = list(bot_fields_descriptors.keys())
|
|
||||||
descriptor_fields_sequence.remove("id")
|
|
||||||
descriptor_name = name.lower()
|
descriptor_name = name.lower()
|
||||||
namespace["bot_entity_descriptor"] = EntityDescriptor(
|
namespace["bot_entity_descriptor"] = EntityDescriptor(
|
||||||
name = descriptor_name,
|
name=descriptor_name,
|
||||||
class_name = name,
|
class_name=name,
|
||||||
type_ = name,
|
type_=name,
|
||||||
fields_descriptors = bot_fields_descriptors,
|
fields_descriptors=bot_fields_descriptors,
|
||||||
field_sequence = descriptor_fields_sequence)
|
)
|
||||||
|
|
||||||
|
descriptor_fields_sequence = [
|
||||||
|
key
|
||||||
|
for key, val in bot_fields_descriptors.items()
|
||||||
|
if not (val.is_optional or val.name == "id")
|
||||||
|
]
|
||||||
|
|
||||||
|
entity_descriptor: EntityDescriptor = namespace["bot_entity_descriptor"]
|
||||||
|
|
||||||
|
if entity_descriptor.default_form.edit_field_sequence is None:
|
||||||
|
entity_descriptor.default_form.edit_field_sequence = (
|
||||||
|
descriptor_fields_sequence
|
||||||
|
)
|
||||||
|
|
||||||
|
for form in entity_descriptor.forms.values():
|
||||||
|
if form.edit_field_sequence is None:
|
||||||
|
form.edit_field_sequence = descriptor_fields_sequence
|
||||||
|
|
||||||
for field_descriptor in bot_fields_descriptors.values():
|
for field_descriptor in bot_fields_descriptors.values():
|
||||||
field_descriptor.entity_descriptor = namespace["bot_entity_descriptor"]
|
field_descriptor.entity_descriptor = namespace["bot_entity_descriptor"]
|
||||||
@@ -129,14 +172,20 @@ class BotEntityMetaclass(SQLModelMetaclass):
|
|||||||
if "table" not in kwargs:
|
if "table" not in kwargs:
|
||||||
kwargs["table"] = True
|
kwargs["table"] = True
|
||||||
|
|
||||||
if kwargs["table"] == True:
|
if kwargs["table"]:
|
||||||
entity_metadata = EntityMetadata()
|
entity_metadata = EntityMetadata()
|
||||||
entity_metadata.entity_descriptors[descriptor_name] = namespace["bot_entity_descriptor"]
|
entity_metadata.entity_descriptors[descriptor_name] = namespace[
|
||||||
|
"bot_entity_descriptor"
|
||||||
|
]
|
||||||
|
|
||||||
if "__annotations__" in namespace:
|
if "__annotations__" in namespace:
|
||||||
namespace["__annotations__"]["entity_metadata"] = ClassVar[EntityMetadata]
|
namespace["__annotations__"]["entity_metadata"] = ClassVar[
|
||||||
|
EntityMetadata
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
namespace["__annotations__"] = {"entity_metadata": ClassVar[EntityMetadata]}
|
namespace["__annotations__"] = {
|
||||||
|
"entity_metadata": ClassVar[EntityMetadata]
|
||||||
|
}
|
||||||
|
|
||||||
namespace["entity_metadata"] = entity_metadata
|
namespace["entity_metadata"] = entity_metadata
|
||||||
|
|
||||||
@@ -146,78 +195,184 @@ class BotEntityMetaclass(SQLModelMetaclass):
|
|||||||
for field_descriptor in mcs.__future_references__[name]:
|
for field_descriptor in mcs.__future_references__[name]:
|
||||||
type_origin = get_origin(field_descriptor.type_)
|
type_origin = get_origin(field_descriptor.type_)
|
||||||
field_descriptor.type_base = type_
|
field_descriptor.type_base = type_
|
||||||
field_descriptor.type_ = (list[type_] if get_origin(field_descriptor.type_) == list else
|
|
||||||
Optional[type_] if type_origin == Union and isinstance(get_args(field_descriptor.type_)[0], ForwardRef) else
|
field_descriptor.type_ = (
|
||||||
type_ | None if type_origin == UnionType else
|
list[type_]
|
||||||
type_)
|
if get_origin(field_descriptor.type_) is list
|
||||||
|
else (
|
||||||
|
Optional[type_]
|
||||||
|
if type_origin == Union
|
||||||
|
and isinstance(get_args(field_descriptor.type_)[0], ForwardRef)
|
||||||
|
else type_ | None
|
||||||
|
if type_origin == UnionType
|
||||||
|
else type_
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
setattr(namespace["bot_entity_descriptor"], "type_", type_)
|
setattr(namespace["bot_entity_descriptor"], "type_", type_)
|
||||||
|
|
||||||
return type_
|
return type_
|
||||||
|
|
||||||
|
|
||||||
class BotEntity[CreateSchemaType: BaseModel,
|
class BotEntity[CreateSchemaType: BaseModel, UpdateSchemaType: BaseModel](
|
||||||
UpdateSchemaType: BaseModel](SQLModel,
|
SQLModel, metaclass=BotEntityMetaclass, table=False
|
||||||
metaclass = BotEntityMetaclass,
|
):
|
||||||
table = False):
|
|
||||||
|
|
||||||
bot_entity_descriptor: ClassVar[EntityDescriptor]
|
bot_entity_descriptor: ClassVar[EntityDescriptor]
|
||||||
entity_metadata: ClassVar[EntityMetadata]
|
entity_metadata: ClassVar[EntityMetadata]
|
||||||
|
|
||||||
id: int = Field(
|
id: int = Field(primary_key=True, sa_type=BIGINT)
|
||||||
primary_key = True,
|
|
||||||
sa_type = BIGINT)
|
|
||||||
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@session_dep
|
@session_dep
|
||||||
async def get(cls, *,
|
async def get(cls, *, session: AsyncSession | None = None, id: int):
|
||||||
session: AsyncSession | None = None,
|
return await session.get(cls, id, populate_existing=True)
|
||||||
id: int):
|
|
||||||
|
|
||||||
return await session.get(cls, id, populate_existing = True)
|
@classmethod
|
||||||
|
def _static_fiter_condition(
|
||||||
|
cls, select_statement: SelectOfScalar[Self], static_filter: list[Filter]
|
||||||
|
):
|
||||||
|
for sfilt in static_filter:
|
||||||
|
if sfilt.operator == "==":
|
||||||
|
condition = column(sfilt.field_name).__eq__(sfilt.value)
|
||||||
|
elif sfilt.operator == "!=":
|
||||||
|
condition = column(sfilt.field_name).__ne__(sfilt.value)
|
||||||
|
elif sfilt.operator == "<":
|
||||||
|
condition = column(sfilt.field_name).__lt__(sfilt.value)
|
||||||
|
elif sfilt.operator == "<=":
|
||||||
|
condition = column(sfilt.field_name).__le__(sfilt.value)
|
||||||
|
elif sfilt.operator == ">":
|
||||||
|
condition = column(sfilt.field_name).__gt__(sfilt.value)
|
||||||
|
elif sfilt.operator == ">=":
|
||||||
|
condition = column(sfilt.field_name).__ge__(sfilt.value)
|
||||||
|
elif sfilt.operator == "ilike":
|
||||||
|
condition = column(sfilt.field_name).ilike(f"%{sfilt.value}%")
|
||||||
|
elif sfilt.operator == "like":
|
||||||
|
condition = column(sfilt.field_name).like(f"%{sfilt.value}%")
|
||||||
|
elif sfilt.operator == "in":
|
||||||
|
condition = column(sfilt.field_name).in_(sfilt.value)
|
||||||
|
elif sfilt.operator == "not in":
|
||||||
|
condition = column(sfilt.field_name).notin_(sfilt.value)
|
||||||
|
elif sfilt.operator == "is":
|
||||||
|
condition = column(sfilt.field_name).is_(None)
|
||||||
|
elif sfilt.operator == "is not":
|
||||||
|
condition = column(sfilt.field_name).isnot(None)
|
||||||
|
else:
|
||||||
|
condition = None
|
||||||
|
if condition:
|
||||||
|
select_statement = select_statement.where(condition)
|
||||||
|
return select_statement
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _filter_condition(
|
||||||
|
cls,
|
||||||
|
select_statement: SelectOfScalar[Self],
|
||||||
|
filter: str,
|
||||||
|
filter_fields: list[str],
|
||||||
|
):
|
||||||
|
condition = None
|
||||||
|
for field in filter_fields:
|
||||||
|
if condition is not None:
|
||||||
|
condition = condition | (column(field).ilike(f"%{filter}%"))
|
||||||
|
else:
|
||||||
|
condition = column(field).ilike(f"%{filter}%")
|
||||||
|
return select_statement.where(condition)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@session_dep
|
@session_dep
|
||||||
async def get_count(cls, *,
|
async def get_count(
|
||||||
session: AsyncSession | None = None,
|
cls,
|
||||||
filter: str = None) -> int:
|
*,
|
||||||
|
session: AsyncSession | None = None,
|
||||||
|
static_filter: list[Filter] | Any = None,
|
||||||
|
filter: str = None,
|
||||||
|
filter_fields: list[str] = None,
|
||||||
|
ext_filter: Any = None,
|
||||||
|
user: "UserBase" = None,
|
||||||
|
) -> int:
|
||||||
select_statement = select(func.count()).select_from(cls)
|
select_statement = select(func.count()).select_from(cls)
|
||||||
if filter:
|
if static_filter:
|
||||||
select_statement = select_statement.where(column("name").ilike(f"%{filter}%"))
|
if isinstance(static_filter, list):
|
||||||
|
select_statement = cls._static_fiter_condition(
|
||||||
|
select_statement, static_filter
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
select_statement = select_statement.where(static_filter)
|
||||||
|
if filter and filter_fields:
|
||||||
|
select_statement = cls._filter_condition(
|
||||||
|
select_statement, filter, filter_fields
|
||||||
|
)
|
||||||
|
if ext_filter:
|
||||||
|
select_statement = select_statement.where(ext_filter)
|
||||||
|
if user:
|
||||||
|
select_statement = cls._ownership_condition(select_statement, user)
|
||||||
return await session.scalar(select_statement)
|
return await session.scalar(select_statement)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@session_dep
|
@session_dep
|
||||||
async def get_multi(cls, *,
|
async def get_multi(
|
||||||
session: AsyncSession | None = None,
|
cls,
|
||||||
order_by = None,
|
*,
|
||||||
filter:str = None,
|
session: AsyncSession | None = None,
|
||||||
skip: int = 0,
|
order_by=None,
|
||||||
limit: int = None):
|
static_filter: list[Filter] | Any = None,
|
||||||
|
filter: str = None,
|
||||||
|
filter_fields: list[str] = None,
|
||||||
|
ext_filter: Any = None,
|
||||||
|
user: "UserBase" = None,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = None,
|
||||||
|
):
|
||||||
select_statement = select(cls).offset(skip)
|
select_statement = select(cls).offset(skip)
|
||||||
if limit:
|
if limit:
|
||||||
select_statement = select_statement.limit(limit)
|
select_statement = select_statement.limit(limit)
|
||||||
if filter:
|
if static_filter:
|
||||||
select_statement = select_statement.where(column("name").ilike(f"%{filter}%"))
|
if isinstance(static_filter, list):
|
||||||
|
select_statement = cls._static_fiter_condition(
|
||||||
|
select_statement, static_filter
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
select_statement = select_statement.where(static_filter)
|
||||||
|
if filter and filter_fields:
|
||||||
|
select_statement = cls._filter_condition(
|
||||||
|
select_statement, filter, filter_fields
|
||||||
|
)
|
||||||
|
if ext_filter:
|
||||||
|
select_statement = select_statement.where(ext_filter)
|
||||||
|
if user:
|
||||||
|
select_statement = cls._ownership_condition(select_statement, user)
|
||||||
if order_by:
|
if order_by:
|
||||||
select_statement = select_statement.order_by(order_by)
|
select_statement = select_statement.order_by(order_by)
|
||||||
return (await session.exec(select_statement)).all()
|
return (await session.exec(select_statement)).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ownership_condition(
|
||||||
|
cls, select_statement: SelectOfScalar[Self], user: "UserBase"
|
||||||
|
):
|
||||||
|
if cls.bot_entity_descriptor.ownership_fields:
|
||||||
|
condition = None
|
||||||
|
for role in user.roles:
|
||||||
|
if role in cls.bot_entity_descriptor.ownership_fields:
|
||||||
|
owner_col = column(cls.bot_entity_descriptor.ownership_fields[role])
|
||||||
|
if condition is not None:
|
||||||
|
condition = condition | (owner_col == user.id)
|
||||||
|
else:
|
||||||
|
condition = owner_col == user.id
|
||||||
|
else:
|
||||||
|
condition = None
|
||||||
|
break
|
||||||
|
if condition is not None:
|
||||||
|
return select_statement.where(condition)
|
||||||
|
return select_statement
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@session_dep
|
@session_dep
|
||||||
async def create(cls, *,
|
async def create(
|
||||||
session: AsyncSession | None = None,
|
cls,
|
||||||
obj_in: CreateSchemaType,
|
*,
|
||||||
commit: bool = False):
|
session: AsyncSession | None = None,
|
||||||
|
obj_in: CreateSchemaType,
|
||||||
|
commit: bool = False,
|
||||||
|
):
|
||||||
if isinstance(obj_in, cls):
|
if isinstance(obj_in, cls):
|
||||||
obj = obj_in
|
obj = obj_in
|
||||||
else:
|
else:
|
||||||
@@ -227,15 +382,16 @@ class BotEntity[CreateSchemaType: BaseModel,
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@session_dep
|
@session_dep
|
||||||
async def update(cls, *,
|
async def update(
|
||||||
session: AsyncSession | None = None,
|
cls,
|
||||||
id: int,
|
*,
|
||||||
obj_in: UpdateSchemaType,
|
session: AsyncSession | None = None,
|
||||||
commit: bool = False):
|
id: int,
|
||||||
|
obj_in: UpdateSchemaType,
|
||||||
|
commit: bool = False,
|
||||||
|
):
|
||||||
obj = await session.get(cls, id)
|
obj = await session.get(cls, id)
|
||||||
if obj:
|
if obj:
|
||||||
obj_data = obj.model_dump()
|
obj_data = obj.model_dump()
|
||||||
@@ -249,13 +405,11 @@ class BotEntity[CreateSchemaType: BaseModel,
|
|||||||
return obj
|
return obj
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@session_dep
|
@session_dep
|
||||||
async def remove(cls, *,
|
async def remove(
|
||||||
session: AsyncSession | None = None,
|
cls, *, session: AsyncSession | None = None, id: int, commit: bool = False
|
||||||
id: int,
|
):
|
||||||
commit: bool = False):
|
|
||||||
obj = await session.get(cls, id)
|
obj = await session.get(cls, id)
|
||||||
if obj:
|
if obj:
|
||||||
await session.delete(obj)
|
await session.delete(obj)
|
||||||
@@ -263,4 +417,3 @@ class BotEntity[CreateSchemaType: BaseModel,
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
return obj
|
return obj
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -4,30 +4,40 @@ from typing import Any, Self, overload
|
|||||||
|
|
||||||
|
|
||||||
class BotEnumMetaclass(type):
|
class BotEnumMetaclass(type):
|
||||||
|
|
||||||
def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]):
|
def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]):
|
||||||
all_members = {}
|
all_members = {}
|
||||||
if bases and bases[0].__name__ != "BotEnum" and "all_members" in bases[0].__dict__:
|
if (
|
||||||
|
bases
|
||||||
|
and bases[0].__name__ != "BotEnum"
|
||||||
|
and "all_members" in bases[0].__dict__
|
||||||
|
):
|
||||||
all_members = bases[0].__dict__["all_members"]
|
all_members = bases[0].__dict__["all_members"]
|
||||||
|
|
||||||
annotations = {}
|
annotations = {}
|
||||||
|
|
||||||
for key, value in namespace.items():
|
for key, value in namespace.items():
|
||||||
if (key.isupper() and
|
if key.isupper() and not key.startswith("__") and not key.endswith("__"):
|
||||||
not key.startswith("__") and
|
|
||||||
not key.endswith("__")):
|
|
||||||
|
|
||||||
if not isinstance(value, EnumMember):
|
if not isinstance(value, EnumMember):
|
||||||
value = EnumMember(value, None)
|
value = EnumMember(value, None)
|
||||||
|
|
||||||
if key in all_members.keys() and all_members[key].value != value.value:
|
if key in all_members.keys() and all_members[key].value != value.value:
|
||||||
raise ValueError(f"Enum member {key} already exists with different value. Use same value to extend it.")
|
raise ValueError(
|
||||||
|
f"Enum member {key} already exists with different value. Use same value to extend it."
|
||||||
|
)
|
||||||
|
|
||||||
if (value.value in [member.value for member in all_members.values()] and
|
if (
|
||||||
key not in all_members.keys()):
|
value.value in [member.value for member in all_members.values()]
|
||||||
|
and key not in all_members.keys()
|
||||||
|
):
|
||||||
raise ValueError(f"Duplicate enum value {value[0]}")
|
raise ValueError(f"Duplicate enum value {value[0]}")
|
||||||
|
|
||||||
member = EnumMember(value = value.value, loc_obj = value.loc_obj, parent = None, name = key, casting = False)
|
member = EnumMember(
|
||||||
|
value=value.value,
|
||||||
|
loc_obj=value.loc_obj,
|
||||||
|
parent=None,
|
||||||
|
name=key,
|
||||||
|
casting=False,
|
||||||
|
)
|
||||||
|
|
||||||
namespace[key] = member
|
namespace[key] = member
|
||||||
all_members[key] = member
|
all_members[key] = member
|
||||||
@@ -46,22 +56,23 @@ class BotEnumMetaclass(type):
|
|||||||
|
|
||||||
|
|
||||||
class EnumMember(object):
|
class EnumMember(object):
|
||||||
|
@overload
|
||||||
|
def __init__(self, value: str) -> "EnumMember": ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __init__(self, value: str) -> "EnumMember":...
|
def __init__(self, value: "EnumMember") -> "EnumMember": ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __init__(self, value: "EnumMember") -> "EnumMember":...
|
def __init__(self, value: str, loc_obj: dict[str, str]) -> "EnumMember": ...
|
||||||
|
|
||||||
@overload
|
def __init__(
|
||||||
def __init__(self, value: str, loc_obj: dict[str, str]) -> "EnumMember":...
|
self,
|
||||||
|
value: str = None,
|
||||||
def __init__(self,
|
loc_obj: dict[str, str] = None,
|
||||||
value: str = None,
|
parent: type = None,
|
||||||
loc_obj: dict[str, str] = None,
|
name: str = None,
|
||||||
parent: type = None,
|
casting: bool = True,
|
||||||
name: str = None,
|
) -> "EnumMember":
|
||||||
casting: bool = True) -> "EnumMember":
|
|
||||||
if not casting:
|
if not casting:
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._name = name
|
self._name = name
|
||||||
@@ -69,10 +80,9 @@ class EnumMember(object):
|
|||||||
self.loc_obj = loc_obj
|
self.loc_obj = loc_obj
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __new__(cls: Self, *args, **kwargs) -> "EnumMember":...
|
def __new__(cls: Self, *args, **kwargs) -> "EnumMember": ...
|
||||||
|
|
||||||
def __new__(cls, *args, casting: bool = True, **kwargs) -> "EnumMember":
|
def __new__(cls, *args, casting: bool = True, **kwargs) -> "EnumMember":
|
||||||
|
|
||||||
if (cls.__name__ == "EnumMember") or not casting:
|
if (cls.__name__ == "EnumMember") or not casting:
|
||||||
obj = super().__new__(cls)
|
obj = super().__new__(cls)
|
||||||
kwargs["casting"] = False
|
kwargs["casting"] = False
|
||||||
@@ -81,9 +91,13 @@ class EnumMember(object):
|
|||||||
if args.__len__() == 0:
|
if args.__len__() == 0:
|
||||||
return list(cls.all_members.values())[0]
|
return list(cls.all_members.values())[0]
|
||||||
if args.__len__() == 1 and isinstance(args[0], str):
|
if args.__len__() == 1 and isinstance(args[0], str):
|
||||||
return {member.value: member for key, member in cls.all_members.items()}[args[0]]
|
return {member.value: member for key, member in cls.all_members.items()}[
|
||||||
|
args[0]
|
||||||
|
]
|
||||||
elif args.__len__() == 1:
|
elif args.__len__() == 1:
|
||||||
return {member.value: member for key, member in cls.all_members.items()}[args[0].value]
|
return {member.value: member for key, member in cls.all_members.items()}[
|
||||||
|
args[0].value
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
return args[0]
|
return args[0]
|
||||||
|
|
||||||
@@ -91,24 +105,25 @@ class EnumMember(object):
|
|||||||
return str_schema()
|
return str_schema()
|
||||||
|
|
||||||
def __get__(self, instance, owner) -> Self:
|
def __get__(self, instance, owner) -> Self:
|
||||||
# return {member.value: member for key, member in owner.all_members.items()}[self.value]
|
return {
|
||||||
return {member.value: member for key, member in self._parent.all_members.items()}[self.value]
|
member.value: member for key, member in self._parent.all_members.items()
|
||||||
|
}[self.value]
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
instance.__dict__[self] = value
|
instance.__dict__[self] = value
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self._parent.__name__ if self._parent else "EnumMember"}.{self._name}: '{self.value}'>"
|
return f"<{self._parent.__name__ if self._parent else 'EnumMember'}.{self._name}: '{self.value}'>"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def __eq__(self, other : Self | str) -> bool:
|
def __eq__(self, other: Self | str) -> bool:
|
||||||
if other is None:
|
if other is None:
|
||||||
return False
|
return False
|
||||||
if isinstance(other, str):
|
if isinstance(other, str):
|
||||||
return self.value == other
|
return self.value == other
|
||||||
return self.value == other.value
|
return self.value == other.value
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.value)
|
return hash(self.value)
|
||||||
@@ -123,13 +138,11 @@ class EnumMember(object):
|
|||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class BotEnum(EnumMember, metaclass = BotEnumMetaclass):
|
class BotEnum(EnumMember, metaclass=BotEnumMetaclass):
|
||||||
|
|
||||||
all_members: dict[str, EnumMember]
|
all_members: dict[str, EnumMember]
|
||||||
|
|
||||||
|
|
||||||
class EnumType(TypeDecorator):
|
class EnumType(TypeDecorator):
|
||||||
|
|
||||||
impl = String(256)
|
impl = String(256)
|
||||||
|
|
||||||
def __init__(self, enum_type: BotEnum):
|
def __init__(self, enum_type: BotEnum):
|
||||||
|
|||||||
@@ -1,20 +1,85 @@
|
|||||||
from typing import Any, Callable, TYPE_CHECKING
|
from aiogram.types import Message, CallbackQuery
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.utils.i18n import I18n
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from typing import Any, Callable, TYPE_CHECKING, Literal
|
||||||
from babel.support import LazyProxy
|
from babel.support import LazyProxy
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from .role import RoleBase
|
from .role import RoleBase
|
||||||
from . import EntityPermission
|
from . import EntityPermission
|
||||||
|
from ..bot.handlers.context import ContextData
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .bot_entity import BotEntity
|
from .bot_entity import BotEntity
|
||||||
|
from ..main import QBotApp
|
||||||
|
from .user import UserBase
|
||||||
|
|
||||||
EntityCaptionCallable = Callable[["EntityDescriptor"], str]
|
EntityCaptionCallable = Callable[["EntityDescriptor"], str]
|
||||||
EntityItemCaptionCallable = Callable[["EntityDescriptor", Any], str]
|
EntityItemCaptionCallable = Callable[["EntityDescriptor", Any], str]
|
||||||
EntityFieldCaptionCallable = Callable[["EntityFieldDescriptor", Any, Any], str]
|
EntityFieldCaptionCallable = Callable[["EntityFieldDescriptor", Any, Any], str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only = True)
|
@dataclass
|
||||||
class _BaseEntityFieldDescriptor():
|
class FieldEditButton:
|
||||||
|
field_name: str
|
||||||
|
caption: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CommandButton:
|
||||||
|
command: str
|
||||||
|
caption: str | LazyProxy | EntityItemCaptionCallable | None = None
|
||||||
|
context_data: ContextData | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Filter:
|
||||||
|
field_name: str
|
||||||
|
operator: Literal[
|
||||||
|
"==",
|
||||||
|
"!=",
|
||||||
|
">",
|
||||||
|
"<",
|
||||||
|
">=",
|
||||||
|
"<=",
|
||||||
|
"in",
|
||||||
|
"not in",
|
||||||
|
"like",
|
||||||
|
"ilike",
|
||||||
|
"is",
|
||||||
|
"is not",
|
||||||
|
]
|
||||||
|
value_type: Literal["const", "param"]
|
||||||
|
value: Any | None = None
|
||||||
|
param_index: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EntityList:
|
||||||
|
caption: str | LazyProxy | EntityCaptionCallable | None = None
|
||||||
|
item_repr: EntityItemCaptionCallable | None = None
|
||||||
|
show_add_new_button: bool = True
|
||||||
|
item_form: str | None = None
|
||||||
|
pagination: bool = True
|
||||||
|
static_filters: list[Filter] | Any = None
|
||||||
|
filtering: bool = True
|
||||||
|
filtering_fields: list[str] = None
|
||||||
|
order_by: str | Any | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EntityForm:
|
||||||
|
item_repr: EntityItemCaptionCallable | None = None
|
||||||
|
edit_field_sequence: list[str] = None
|
||||||
|
form_buttons: list[list[FieldEditButton | CommandButton]] = None
|
||||||
|
show_edit_button: bool = True
|
||||||
|
show_delete_button: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class _BaseEntityFieldDescriptor:
|
||||||
icon: str = None
|
icon: str = None
|
||||||
caption: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
caption: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
||||||
description: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
description: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
||||||
@@ -24,21 +89,25 @@ class _BaseEntityFieldDescriptor():
|
|||||||
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"
|
||||||
|
ep_form: str | None = None
|
||||||
|
ep_parent_field: str | None = None
|
||||||
|
ep_child_field: str | None = None
|
||||||
|
dt_type: Literal["date", "datetime"] = "date"
|
||||||
default: Any = None
|
default: Any = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only = True)
|
@dataclass(kw_only=True)
|
||||||
class EntityField(_BaseEntityFieldDescriptor):
|
class EntityField(_BaseEntityFieldDescriptor):
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
sm_descriptor: Any = None
|
sm_descriptor: Any = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only = True)
|
@dataclass(kw_only=True)
|
||||||
class Setting(_BaseEntityFieldDescriptor):
|
class Setting(_BaseEntityFieldDescriptor):
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only = True)
|
@dataclass(kw_only=True)
|
||||||
class EntityFieldDescriptor(_BaseEntityFieldDescriptor):
|
class EntityFieldDescriptor(_BaseEntityFieldDescriptor):
|
||||||
name: str
|
name: str
|
||||||
field_name: str
|
field_name: str
|
||||||
@@ -52,42 +121,80 @@ class EntityFieldDescriptor(_BaseEntityFieldDescriptor):
|
|||||||
return self.name.__hash__()
|
return self.name.__hash__()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only = True)
|
@dataclass(kw_only=True)
|
||||||
class _BaseEntityDescriptor:
|
class _BaseEntityDescriptor:
|
||||||
|
|
||||||
icon: str = "📘"
|
icon: str = "📘"
|
||||||
caption: str | LazyProxy | EntityCaptionCallable | None = None
|
full_name: str | LazyProxy | EntityCaptionCallable | None = None
|
||||||
caption_plural: str | LazyProxy | EntityCaptionCallable | None = None
|
full_name_plural: str | LazyProxy | EntityCaptionCallable | None = None
|
||||||
description: str | LazyProxy | EntityCaptionCallable | None = None
|
description: str | LazyProxy | EntityCaptionCallable | None = None
|
||||||
item_caption: EntityItemCaptionCallable | None = None
|
item_repr: EntityItemCaptionCallable | None = None
|
||||||
|
default_list: EntityList = field(default_factory=EntityList)
|
||||||
|
default_form: EntityForm = field(default_factory=EntityForm)
|
||||||
|
lists: dict[str, EntityList] = field(default_factory=dict[str, EntityList])
|
||||||
|
forms: dict[str, EntityForm] = field(default_factory=dict[str, EntityForm])
|
||||||
show_in_entities_menu: bool = True
|
show_in_entities_menu: bool = True
|
||||||
field_sequence: list[str] = None
|
ownership_fields: dict[RoleBase, str] = field(default_factory=dict[RoleBase, str])
|
||||||
edit_button_visible: bool = True
|
permissions: dict[EntityPermission, list[RoleBase]] = field(
|
||||||
edit_buttons: list[list[str | tuple[str, str | LazyProxy | EntityFieldCaptionCallable]]] = None
|
default_factory=lambda: {
|
||||||
permissions: dict[EntityPermission, list[RoleBase]] = field(default_factory = lambda: {
|
EntityPermission.LIST: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||||
EntityPermission.LIST: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
EntityPermission.READ: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||||
EntityPermission.READ: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
EntityPermission.CREATE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||||
EntityPermission.CREATE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
EntityPermission.UPDATE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||||
EntityPermission.UPDATE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
EntityPermission.DELETE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
||||||
EntityPermission.DELETE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
|
EntityPermission.LIST_ALL: [RoleBase.SUPER_USER],
|
||||||
EntityPermission.LIST_ALL: [RoleBase.SUPER_USER],
|
EntityPermission.READ_ALL: [RoleBase.SUPER_USER],
|
||||||
EntityPermission.READ_ALL: [RoleBase.SUPER_USER],
|
EntityPermission.CREATE_ALL: [RoleBase.SUPER_USER],
|
||||||
EntityPermission.CREATE_ALL: [RoleBase.SUPER_USER],
|
EntityPermission.UPDATE_ALL: [RoleBase.SUPER_USER],
|
||||||
EntityPermission.UPDATE_ALL: [RoleBase.SUPER_USER],
|
EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER],
|
||||||
EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER]
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only = True)
|
@dataclass(kw_only=True)
|
||||||
class Entity(_BaseEntityDescriptor):
|
class Entity(_BaseEntityDescriptor):
|
||||||
|
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EntityDescriptor(_BaseEntityDescriptor):
|
class EntityDescriptor(_BaseEntityDescriptor):
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
class_name: str
|
class_name: str
|
||||||
type_: type["BotEntity"]
|
type_: type["BotEntity"]
|
||||||
fields_descriptors: dict[str, EntityFieldDescriptor]
|
fields_descriptors: dict[str, EntityFieldDescriptor]
|
||||||
|
|
||||||
|
|
||||||
|
@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 _BotCommand:
|
||||||
|
name: str
|
||||||
|
caption: str | dict[str, str] | None = None
|
||||||
|
show_in_bot_commands: bool = False
|
||||||
|
register_navigation: bool = True
|
||||||
|
clear_navigation: bool = False
|
||||||
|
clear_state: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class BotCommand(_BotCommand):
|
||||||
|
handler: Callable[[CommandCallbackContext], None]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class Command(_BotCommand): ...
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from .descriptors import EntityDescriptor
|
|||||||
from ._singleton import Singleton
|
from ._singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
class EntityMetadata(metaclass = Singleton):
|
class EntityMetadata(metaclass=Singleton):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.entity_descriptors: dict[str, EntityDescriptor] = {}
|
self.entity_descriptors: dict[str, EntityDescriptor] = {}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from babel.support import LazyProxy
|
|
||||||
from typing import TypeVar
|
|
||||||
|
|
||||||
from .bot_entity import BotEntity
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FieldType: ...
|
|
||||||
|
|
||||||
|
|
||||||
class LocStr(str): ...
|
|
||||||
|
|
||||||
|
|
||||||
class String(FieldType):
|
|
||||||
localizable: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class Integer(FieldType):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Decimal:
|
|
||||||
precision: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Boolean:
|
|
||||||
true_value: str | LazyProxy = "true"
|
|
||||||
false_value: str | LazyProxy = "false"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DateTime:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
EntityType = TypeVar('EntityType', bound = BotEntity)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class EntityReference:
|
|
||||||
entity_type: type[EntityType]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
from sqlmodel import SQLModel, Field
|
from sqlmodel import SQLModel, Field
|
||||||
|
|
||||||
|
|
||||||
class FSMStorage(SQLModel, table = True):
|
class FSMStorage(SQLModel, table=True):
|
||||||
|
|
||||||
__tablename__ = "fsm_storage"
|
__tablename__ = "fsm_storage"
|
||||||
key: str = Field(primary_key = True)
|
key: str = Field(primary_key=True)
|
||||||
value: str | None = None
|
value: str | None = None
|
||||||
@@ -2,5 +2,4 @@ from .bot_enum import BotEnum, EnumMember
|
|||||||
|
|
||||||
|
|
||||||
class LanguageBase(BotEnum):
|
class LanguageBase(BotEnum):
|
||||||
|
|
||||||
EN = EnumMember("en", {"en": "🇬🇧 english"})
|
EN = EnumMember("en", {"en": "🇬🇧 english"})
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# from aiogram.types import Message, CallbackQuery
|
|
||||||
# from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|
||||||
# from typing import Any, Callable, Self, Union, overload
|
|
||||||
# from babel.support import LazyProxy
|
|
||||||
# from dataclasses import dataclass
|
|
||||||
|
|
||||||
# from ..bot.handlers.context import ContextData
|
|
||||||
|
|
||||||
|
|
||||||
# class Menu:
|
|
||||||
|
|
||||||
# @overload
|
|
||||||
# def __init__(self, description: str | LazyProxy): ...
|
|
||||||
|
|
||||||
|
|
||||||
# @overload
|
|
||||||
# def __init__(self, menu_factory: Callable[[InlineKeyboardBuilder, Union[Message, CallbackQuery], Any], str]): ...
|
|
||||||
|
|
||||||
|
|
||||||
# def __init__(self, description: str | LazyProxy = None,
|
|
||||||
# menu_factory: Callable[[InlineKeyboardBuilder, Union[Message, CallbackQuery], Any], str] = None) -> None:
|
|
||||||
|
|
||||||
# self.menu_factory = menu_factory
|
|
||||||
# self.description = description
|
|
||||||
# self.parent: Menu = None
|
|
||||||
# self.items: list[list[Menu]] = []
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
from sqlmodel import BIGINT, Field, select, func, column
|
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
||||||
|
|
||||||
|
|
||||||
from .bot_entity import BotEntity
|
|
||||||
from .descriptors import EntityField
|
|
||||||
from .user import UserBase
|
|
||||||
from . import session_dep
|
|
||||||
|
|
||||||
|
|
||||||
class OwnedBotEntity(BotEntity, table = False):
|
|
||||||
|
|
||||||
user_id: int | None = EntityField(
|
|
||||||
sm_descriptor = Field(sa_type = BIGINT, foreign_key = "user.id", ondelete="SET NULL"),
|
|
||||||
is_visible = False)
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@session_dep
|
|
||||||
async def get_multi_by_user(cls, *,
|
|
||||||
session: AsyncSession | None = None,
|
|
||||||
user_id: int,
|
|
||||||
filter: str = None,
|
|
||||||
order_by = None,
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = None):
|
|
||||||
|
|
||||||
select_statement = select(cls).where(cls.user_id == user_id).offset(skip)
|
|
||||||
if filter:
|
|
||||||
select_statement = select_statement.where(column("name").ilike(f"%{filter}%"))
|
|
||||||
if limit:
|
|
||||||
select_statement = select_statement.limit(limit)
|
|
||||||
if order_by:
|
|
||||||
select_statement = select_statement.order_by(order_by)
|
|
||||||
return (await session.exec(select_statement)).all()
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@session_dep
|
|
||||||
async def get_count_by_user(cls, *,
|
|
||||||
session: AsyncSession | None = None,
|
|
||||||
user_id: int,
|
|
||||||
filter: str = None) -> int:
|
|
||||||
|
|
||||||
select_statement = select(func.count()).select_from(cls).where(cls.user_id == user_id)
|
|
||||||
if filter:
|
|
||||||
select_statement = select_statement.where(column("name").ilike(f"%{filter}%"))
|
|
||||||
|
|
||||||
return await session.scalar(select_statement)
|
|
||||||
@@ -2,6 +2,5 @@ from .bot_enum import BotEnum, EnumMember
|
|||||||
|
|
||||||
|
|
||||||
class RoleBase(BotEnum):
|
class RoleBase(BotEnum):
|
||||||
|
|
||||||
SUPER_USER = EnumMember("super_user")
|
SUPER_USER = EnumMember("super_user")
|
||||||
DEFAULT_USER = EnumMember("default_user")
|
DEFAULT_USER = EnumMember("default_user")
|
||||||
@@ -7,57 +7,58 @@ from typing import Any, get_args, get_origin
|
|||||||
from ..db import async_session
|
from ..db import async_session
|
||||||
from .role import RoleBase
|
from .role import RoleBase
|
||||||
from .descriptors import EntityFieldDescriptor, Setting
|
from .descriptors import EntityFieldDescriptor, Setting
|
||||||
from ..utils import deserialize, serialize
|
from ..utils.serialization import deserialize, serialize
|
||||||
|
|
||||||
import ujson as json
|
import ujson as json
|
||||||
|
|
||||||
|
|
||||||
class DbSettings(SQLModel, table = True):
|
class DbSettings(SQLModel, table=True):
|
||||||
__tablename__ = "settings"
|
__tablename__ = "settings"
|
||||||
name: str = Field(primary_key = True)
|
name: str = Field(primary_key=True)
|
||||||
value: str
|
value: str
|
||||||
|
|
||||||
|
|
||||||
class SettingsMetaclass(type):
|
class SettingsMetaclass(type):
|
||||||
|
|
||||||
def __new__(cls, class_name, base_classes, attributes):
|
def __new__(cls, class_name, base_classes, attributes):
|
||||||
|
|
||||||
settings_descriptors = {}
|
settings_descriptors = {}
|
||||||
|
|
||||||
if base_classes:
|
if base_classes:
|
||||||
settings_descriptors = base_classes[0].__dict__.get("_settings_descriptors", {})
|
settings_descriptors = base_classes[0].__dict__.get(
|
||||||
|
"_settings_descriptors", {}
|
||||||
for annotation in attributes.get('__annotations__', {}):
|
)
|
||||||
|
|
||||||
|
for annotation in attributes.get("__annotations__", {}):
|
||||||
if annotation in ["_settings_descriptors", "_cache", "_cached_settings"]:
|
if annotation in ["_settings_descriptors", "_cache", "_cached_settings"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
attr_value = attributes.get(annotation)
|
attr_value = attributes.get(annotation)
|
||||||
name = annotation
|
name = annotation
|
||||||
|
|
||||||
type_ = attributes['__annotations__'][annotation]
|
type_ = attributes["__annotations__"][annotation]
|
||||||
|
|
||||||
if isinstance(attr_value, Setting):
|
if isinstance(attr_value, Setting):
|
||||||
descriptor_kwargs = attr_value.__dict__.copy()
|
descriptor_kwargs = attr_value.__dict__.copy()
|
||||||
name = descriptor_kwargs.pop("name") or annotation
|
name = descriptor_kwargs.pop("name") or annotation
|
||||||
attributes[annotation] = EntityFieldDescriptor(
|
attributes[annotation] = EntityFieldDescriptor(
|
||||||
name = name,
|
name=name,
|
||||||
field_name = annotation,
|
field_name=annotation,
|
||||||
type_ = type_,
|
type_=type_,
|
||||||
type_base = type_,
|
type_base=type_,
|
||||||
**descriptor_kwargs)
|
**descriptor_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
attributes[annotation] = EntityFieldDescriptor(
|
attributes[annotation] = EntityFieldDescriptor(
|
||||||
name = annotation,
|
name=annotation,
|
||||||
field_name = annotation,
|
field_name=annotation,
|
||||||
type_ = type_,
|
type_=type_,
|
||||||
type_base = type_,
|
type_base=type_,
|
||||||
default = attr_value)
|
default=attr_value,
|
||||||
|
)
|
||||||
|
|
||||||
type_origin = get_origin(type_)
|
type_origin = get_origin(type_)
|
||||||
|
|
||||||
if type_origin == list:
|
if type_origin is list:
|
||||||
attributes[annotation].is_list = True
|
attributes[annotation].is_list = True
|
||||||
attributes[annotation].type_base = type_ = get_args(type_)[0]
|
attributes[annotation].type_base = type_ = get_args(type_)[0]
|
||||||
|
|
||||||
@@ -67,7 +68,11 @@ class SettingsMetaclass(type):
|
|||||||
|
|
||||||
settings_descriptors[name] = attributes[annotation]
|
settings_descriptors[name] = attributes[annotation]
|
||||||
|
|
||||||
if base_classes and base_classes[0].__name__ == "Settings" and hasattr(base_classes[0], annotation):
|
if (
|
||||||
|
base_classes
|
||||||
|
and base_classes[0].__name__ == "Settings"
|
||||||
|
and hasattr(base_classes[0], annotation)
|
||||||
|
):
|
||||||
setattr(base_classes[0], annotation, attributes[annotation])
|
setattr(base_classes[0], annotation, attributes[annotation])
|
||||||
|
|
||||||
attributes["__annotations__"] = {}
|
attributes["__annotations__"] = {}
|
||||||
@@ -76,62 +81,119 @@ class SettingsMetaclass(type):
|
|||||||
return super().__new__(cls, class_name, base_classes, attributes)
|
return super().__new__(cls, class_name, base_classes, attributes)
|
||||||
|
|
||||||
|
|
||||||
class Settings(metaclass = SettingsMetaclass):
|
class Settings(metaclass=SettingsMetaclass):
|
||||||
|
|
||||||
_cache: dict[str, Any] = dict[str, Any]()
|
_cache: dict[str, Any] = dict[str, Any]()
|
||||||
_settings_descriptors: dict[str, EntityFieldDescriptor] = {}
|
_settings_descriptors: dict[str, EntityFieldDescriptor] = {}
|
||||||
|
|
||||||
PAGE_SIZE: int = Setting(default = 10, )
|
PAGE_SIZE: int = Setting(
|
||||||
SECURITY_PARAMETERS_ROLES: list[RoleBase] = Setting(name = "SECPARAMS_ROLES", default = [RoleBase.SUPER_USER], is_visible = False)
|
default=10,
|
||||||
|
)
|
||||||
|
SECURITY_PARAMETERS_ROLES: list[RoleBase] = Setting(
|
||||||
|
name="SECPARAMS_ROLES", default=[RoleBase.SUPER_USER], is_visible=False
|
||||||
|
)
|
||||||
|
|
||||||
APP_STRINGS_WELCOME_P_NAME: str = Setting(name = "AS_WELCOME", default = "Welcome, {name}", is_visible = False)
|
APP_STRINGS_WELCOME_P_NAME: str = Setting(
|
||||||
APP_STRINGS_GREETING_P_NAME: str = Setting(name = "AS_GREETING", default = "Hello, {name}", is_visible = False)
|
name="AS_WELCOME", default="Welcome, {name}", is_visible=False
|
||||||
APP_STRINGS_INTERNAL_ERROR_P_ERROR: str = Setting(name = "AS_INTERNAL_ERROR", default = "Internal error\n{error}", is_visible = False)
|
)
|
||||||
APP_STRINGS_USER_BLOCKED_P_NAME: str = Setting(name = "AS_USER_BLOCKED", default = "User {name} is blocked", is_visible = False)
|
APP_STRINGS_GREETING_P_NAME: str = Setting(
|
||||||
APP_STRINGS_FORBIDDEN: str = Setting(name = "AS_FORBIDDEN", default = "Forbidden", is_visible = False)
|
name="AS_GREETING", default="Hello, {name}", is_visible=False
|
||||||
APP_STRINGS_NOT_FOUND: str = Setting(name = "AS_NOT_FOUND", default = "Object not found", is_visible = False)
|
)
|
||||||
APP_STRINGS_MAIN_NENU: str = Setting(name = "AS_MAIN_MENU", default = "Main menu", is_visible = False)
|
APP_STRINGS_INTERNAL_ERROR_P_ERROR: str = Setting(
|
||||||
APP_STRINGS_REFERENCES: str = Setting(name = "AS_REFERENCES", default = "References", is_visible = False)
|
name="AS_INTERNAL_ERROR", default="Internal error\n{error}", is_visible=False
|
||||||
APP_STRINGS_REFERENCES_BTN: str = Setting(name = "AS_REFERENCES_BTN", default = "📚 References", is_visible = False)
|
)
|
||||||
APP_STRINGS_SETTINGS: str = Setting(name = "AS_SETTINGS", default = "Settings", is_visible = False)
|
APP_STRINGS_USER_BLOCKED_P_NAME: str = Setting(
|
||||||
APP_STRINGS_SETTINGS_BTN: str = Setting(name = "AS_SETTINGS_BTN", default = "⚙️ Settings", is_visible = False)
|
name="AS_USER_BLOCKED", default="User {name} is blocked", is_visible=False
|
||||||
APP_STRINGS_PARAMETERS: str = Setting(name = "AS_PARAMETERS", default = "Parameters", is_visible = False)
|
)
|
||||||
APP_STRINGS_PARAMETERS_BTN: str = Setting(name = "AS_PARAMETERS_BTN", default = "🎛️ Parameters", is_visible = False)
|
APP_STRINGS_FORBIDDEN: str = Setting(
|
||||||
APP_STRINGS_LANGUAGE: str = Setting(name = "AS_LANGUAGE", default = "Language", is_visible = False)
|
name="AS_FORBIDDEN", default="Forbidden", is_visible=False
|
||||||
APP_STRINGS_LANGUAGE_BTN: str = Setting(name = "AS_LANGUAGE_BTN", default = "🗣️ Language", is_visible = False)
|
)
|
||||||
APP_STRINGS_BACK_BTN: str = Setting(name = "AS_BACK_BTN", default = "⬅️ Back", is_visible = False)
|
APP_STRINGS_NOT_FOUND: str = Setting(
|
||||||
APP_STRINGS_DELETE_BTN: str = Setting(name = "AS_DELETE_BTN", default = "🗑️ Delete", is_visible = False)
|
name="AS_NOT_FOUND", default="Object not found", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_MAIN_NENU: str = Setting(
|
||||||
|
name="AS_MAIN_MENU", default="Main menu", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_REFERENCES: str = Setting(
|
||||||
|
name="AS_REFERENCES", default="References", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_REFERENCES_BTN: str = Setting(
|
||||||
|
name="AS_REFERENCES_BTN", default="📚 References", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_SETTINGS: str = Setting(
|
||||||
|
name="AS_SETTINGS", default="Settings", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_SETTINGS_BTN: str = Setting(
|
||||||
|
name="AS_SETTINGS_BTN", default="⚙️ Settings", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_PARAMETERS: str = Setting(
|
||||||
|
name="AS_PARAMETERS", default="Parameters", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_PARAMETERS_BTN: str = Setting(
|
||||||
|
name="AS_PARAMETERS_BTN", default="🎛️ Parameters", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_LANGUAGE: str = Setting(
|
||||||
|
name="AS_LANGUAGE", default="Language", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_LANGUAGE_BTN: str = Setting(
|
||||||
|
name="AS_LANGUAGE_BTN", default="🗣️ Language", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_BACK_BTN: str = Setting(
|
||||||
|
name="AS_BACK_BTN", default="⬅️ Back", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_DELETE_BTN: str = Setting(
|
||||||
|
name="AS_DELETE_BTN", default="🗑️ Delete", is_visible=False
|
||||||
|
)
|
||||||
APP_STRINGS_CONFIRM_DELETE_P_NAME: str = Setting(
|
APP_STRINGS_CONFIRM_DELETE_P_NAME: str = Setting(
|
||||||
name = "AS_CONFIRM_DEL",
|
name="AS_CONFIRM_DEL",
|
||||||
default = "Are you sure you want to delete \"{name}\"?",
|
default='Are you sure you want to delete "{name}"?',
|
||||||
is_visible = False)
|
is_visible=False,
|
||||||
APP_STRINGS_EDIT_BTN: str = Setting(name = "AS_EDIT_BTN", default = "✏️ Edit", is_visible = False)
|
)
|
||||||
APP_STRINGS_ADD_BTN: str = Setting(name = "AS_ADD_BTN", default = "➕ Add", is_visible = False)
|
APP_STRINGS_EDIT_BTN: str = Setting(
|
||||||
APP_STRINGS_YES_BTN: str = Setting(name = "AS_YES_BTN", default = "✅ Yes", is_visible = False)
|
name="AS_EDIT_BTN", default="✏️ Edit", is_visible=False
|
||||||
APP_STRINGS_NO_BTN: str = Setting(name = "AS_NO_BTN", default = "❌ No", is_visible = False)
|
)
|
||||||
APP_STRINGS_CANCEL_BTN: str = Setting(name = "AS_CANCEL_BTN", default = "❌ Cancel", is_visible = False)
|
APP_STRINGS_ADD_BTN: str = Setting(
|
||||||
APP_STRINGS_CLEAR_BTN: str = Setting(name = "AS_CLEAR_BTN", default = "⌫ Clear", is_visible = False)
|
name="AS_ADD_BTN", default="➕ Add", is_visible=False
|
||||||
APP_STRINGS_DONE_BTN: str = Setting(name = "AS_DONE_BTN", default = "✅ Done", is_visible = False)
|
)
|
||||||
APP_STRINGS_SKIP_BTN: str = Setting(name = "AS_SKIP_BTN", default = "⏩️ Skip", is_visible = False)
|
APP_STRINGS_YES_BTN: str = Setting(
|
||||||
|
name="AS_YES_BTN", default="✅ Yes", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_NO_BTN: str = Setting(
|
||||||
|
name="AS_NO_BTN", default="❌ No", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_CANCEL_BTN: str = Setting(
|
||||||
|
name="AS_CANCEL_BTN", default="❌ Cancel", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_CLEAR_BTN: str = Setting(
|
||||||
|
name="AS_CLEAR_BTN", default="⌫ Clear", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_DONE_BTN: str = Setting(
|
||||||
|
name="AS_DONE_BTN", default="✅ Done", is_visible=False
|
||||||
|
)
|
||||||
|
APP_STRINGS_SKIP_BTN: str = Setting(
|
||||||
|
name="AS_SKIP_BTN", default="⏩️ Skip", is_visible=False
|
||||||
|
)
|
||||||
APP_STRINGS_FIELD_EDIT_PROMPT_TEMPLATE_P_NAME_VALUE: str = Setting(
|
APP_STRINGS_FIELD_EDIT_PROMPT_TEMPLATE_P_NAME_VALUE: str = Setting(
|
||||||
name = "AS_FIELDEDIT_PROMPT",
|
name="AS_FIELDEDIT_PROMPT",
|
||||||
default = "Enter new value for \"{name}\" (current value: {value})",
|
default='Enter new value for "{name}" (current value: {value})',
|
||||||
is_visible = False)
|
is_visible=False,
|
||||||
|
)
|
||||||
APP_STRINGS_FIELD_CREATE_PROMPT_TEMPLATE_P_NAME: str = Setting(
|
APP_STRINGS_FIELD_CREATE_PROMPT_TEMPLATE_P_NAME: str = Setting(
|
||||||
name = "AS_FIELDCREATE_PROMPT",
|
name="AS_FIELDCREATE_PROMPT",
|
||||||
default = "Enter new value for \"{name}\"",
|
default='Enter new value for "{name}"',
|
||||||
is_visible = False)
|
is_visible=False,
|
||||||
|
)
|
||||||
APP_STRINGS_STRING_EDITOR_LOCALE_TEMPLATE_P_NAME: str = Setting(
|
APP_STRINGS_STRING_EDITOR_LOCALE_TEMPLATE_P_NAME: str = Setting(
|
||||||
name = "AS_STREDIT_LOC_TEMPLATE",
|
name="AS_STREDIT_LOC_TEMPLATE", default='string for "{name}"', is_visible=False
|
||||||
default = "string for \"{name}\"",
|
)
|
||||||
is_visible = False)
|
APP_STRINGS_VIEW_FILTER_EDIT_PROMPT: str = Setting(
|
||||||
APP_STRINGS_VIEW_FILTER_EDIT_PROMPT: str = Setting(name = "AS_FILTEREDIT_PROMPT", default = "Enter filter value", is_visible = False)
|
name="AS_FILTEREDIT_PROMPT", default="Enter filter value", is_visible=False
|
||||||
APP_STRINGS_INVALID_INPUT: str = Setting(name = "AS_INVALID_INPUT", default = "Invalid input", is_visible = False)
|
)
|
||||||
|
APP_STRINGS_INVALID_INPUT: str = Setting(
|
||||||
|
name="AS_INVALID_INPUT", default="Invalid input", is_visible=False
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get[T](cls, param: T, all_locales = False, locale: str = None) -> T:
|
async def get[T](cls, param: T, all_locales=False, locale: str = None) -> T:
|
||||||
|
|
||||||
name = param.field_name
|
name = param.field_name
|
||||||
|
|
||||||
if name not in cls._cache.keys():
|
if name not in cls._cache.keys():
|
||||||
@@ -144,73 +206,79 @@ class Settings(metaclass = SettingsMetaclass):
|
|||||||
locale = get_i18n().current_locale
|
locale = get_i18n().current_locale
|
||||||
try:
|
try:
|
||||||
obj = json.loads(ret_val)
|
obj = json.loads(ret_val)
|
||||||
except:
|
except Exception:
|
||||||
return ret_val
|
return ret_val
|
||||||
return obj.get(locale, obj[list(obj.keys())[0]])
|
return obj.get(locale, obj[list(obj.keys())[0]])
|
||||||
|
|
||||||
return ret_val
|
return ret_val
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def load_param(cls, param: EntityFieldDescriptor) -> Any:
|
async def load_param(cls, param: EntityFieldDescriptor) -> Any:
|
||||||
|
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
db_setting = (await session.exec(
|
db_setting = (
|
||||||
select(DbSettings)
|
await session.exec(
|
||||||
.where(DbSettings.name == param.field_name))).first()
|
select(DbSettings).where(DbSettings.name == param.field_name)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
if db_setting:
|
if db_setting:
|
||||||
return await deserialize(session = session,
|
return await deserialize(
|
||||||
type_ = param.type_,
|
session=session, type_=param.type_, value=db_setting.value
|
||||||
value = db_setting.value)
|
)
|
||||||
|
|
||||||
return (param.default if param.default else
|
|
||||||
[] if (get_origin(param.type_) is list or param.type_ == list) else
|
|
||||||
datetime(2000, 1, 1) if param.type_ == datetime else
|
|
||||||
param.type_())
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
param.default
|
||||||
|
if param.default
|
||||||
|
else (
|
||||||
|
[]
|
||||||
|
if (get_origin(param.type_) is list or param.type_ is list)
|
||||||
|
else datetime(2000, 1, 1)
|
||||||
|
if param.type_ == datetime
|
||||||
|
else param.type_()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def load_params(cls):
|
async def load_params(cls):
|
||||||
|
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
db_settings = (await session.exec(select(DbSettings))).all()
|
db_settings = (await session.exec(select(DbSettings))).all()
|
||||||
for db_setting in db_settings:
|
for db_setting in db_settings:
|
||||||
if db_setting.name in cls.__dict__:
|
if db_setting.name in cls.__dict__:
|
||||||
setting = cls.__dict__[db_setting.name]
|
setting = cls.__dict__[db_setting.name] # type: EntityFieldDescriptor
|
||||||
cls._cache[db_setting.name] = await deserialize(session = session,
|
cls._cache[db_setting.name] = await deserialize(
|
||||||
type_ = setting.type_,
|
session=session,
|
||||||
value = db_setting.value,
|
type_=setting.type_,
|
||||||
default = setting.default)
|
value=db_setting.value,
|
||||||
|
default=setting.default,
|
||||||
|
)
|
||||||
|
|
||||||
cls._loaded = True
|
cls._loaded = True
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def set_param(cls, param: str | EntityFieldDescriptor, value) -> None:
|
async def set_param(cls, param: str | EntityFieldDescriptor, value) -> None:
|
||||||
if isinstance(param, str):
|
if isinstance(param, str):
|
||||||
param = cls._settings_descriptors[param]
|
param = cls._settings_descriptors[param]
|
||||||
ser_value = serialize(value, param)
|
ser_value = serialize(value, param)
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
db_setting = (await session.exec(
|
db_setting = (
|
||||||
select(DbSettings)
|
await session.exec(
|
||||||
.where(DbSettings.name == param.field_name))).first()
|
select(DbSettings).where(DbSettings.name == param.field_name)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
if db_setting is None:
|
if db_setting is None:
|
||||||
db_setting = DbSettings(name = param.field_name)
|
db_setting = DbSettings(name=param.field_name)
|
||||||
db_setting.value = str(ser_value)
|
db_setting.value = str(ser_value)
|
||||||
session.add(db_setting)
|
session.add(db_setting)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
cls._cache[param.field_name] = value
|
cls._cache[param.field_name] = value
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def list_params(cls) -> dict[str, EntityFieldDescriptor]:
|
def list_params(cls) -> dict[str, EntityFieldDescriptor]:
|
||||||
return cls._settings_descriptors
|
return cls._settings_descriptors
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_params(cls) -> dict[EntityFieldDescriptor, Any]:
|
async def get_params(cls) -> dict[EntityFieldDescriptor, Any]:
|
||||||
|
|
||||||
params = cls.list_params()
|
params = cls.list_params()
|
||||||
return {param: await cls.get(param, all_locales = True) for _, param in params.items()}
|
return {
|
||||||
|
param: await cls.get(param, all_locales=True) for _, param in params.items()
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ from .fsm_storage import FSMStorage as FSMStorage
|
|||||||
from .view_setting import ViewSetting as ViewSetting
|
from .view_setting import ViewSetting as ViewSetting
|
||||||
|
|
||||||
|
|
||||||
class UserBase(BotEntity, table = False):
|
class UserBase(BotEntity, table=False):
|
||||||
|
|
||||||
__tablename__ = "user"
|
__tablename__ = "user"
|
||||||
|
|
||||||
lang: LanguageBase = Field(sa_type = EnumType(LanguageBase), default = LanguageBase.EN)
|
lang: LanguageBase = Field(sa_type=EnumType(LanguageBase), default=LanguageBase.EN)
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
|
|
||||||
roles: list[RoleBase] = Field(sa_type=ARRAY(EnumType(RoleBase)), default = [RoleBase.DEFAULT_USER])
|
roles: list[RoleBase] = Field(
|
||||||
|
sa_type=ARRAY(EnumType(RoleBase)), default=[RoleBase.DEFAULT_USER]
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,36 +4,36 @@ from sqlalchemy.ext.asyncio.session import AsyncSession
|
|||||||
from . import session_dep
|
from . import session_dep
|
||||||
|
|
||||||
|
|
||||||
class ViewSetting(SQLModel, table = True):
|
class ViewSetting(SQLModel, table=True):
|
||||||
|
|
||||||
__tablename__ = "view_setting"
|
__tablename__ = "view_setting"
|
||||||
user_id: int = Field(sa_type = BIGINT, primary_key = True, foreign_key="user.id", ondelete="CASCADE")
|
user_id: int = Field(
|
||||||
entity_name: str = Field(primary_key = True)
|
sa_type=BIGINT, primary_key=True, foreign_key="user.id", ondelete="CASCADE"
|
||||||
|
)
|
||||||
|
entity_name: str = Field(primary_key=True)
|
||||||
filter: str | None = None
|
filter: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@session_dep
|
@session_dep
|
||||||
async def get_filter(cls, *,
|
async def get_filter(
|
||||||
session: AsyncSession | None = None,
|
cls, *, session: AsyncSession | None = None, user_id: int, entity_name: str
|
||||||
user_id: int,
|
):
|
||||||
entity_name: str):
|
|
||||||
|
|
||||||
setting = await session.get(cls, (user_id, entity_name))
|
setting = await session.get(cls, (user_id, entity_name))
|
||||||
return setting.filter if setting else None
|
return setting.filter if setting else None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@session_dep
|
@session_dep
|
||||||
async def set_filter(cls, *,
|
async def set_filter(
|
||||||
session: AsyncSession | None = None,
|
cls,
|
||||||
user_id: int,
|
*,
|
||||||
entity_name: str,
|
session: AsyncSession | None = None,
|
||||||
filter: str):
|
user_id: int,
|
||||||
|
entity_name: str,
|
||||||
setting = await session.get(cls, (user_id, entity_name))
|
filter: str,
|
||||||
if setting:
|
):
|
||||||
setting.filter = filter
|
setting = await session.get(cls, (user_id, entity_name))
|
||||||
else:
|
if setting:
|
||||||
setting = cls(user_id = user_id, entity_name = entity_name, filter = filter)
|
setting.filter = filter
|
||||||
session.add(setting)
|
else:
|
||||||
await session.commit()
|
setting = cls(user_id=user_id, entity_name=entity_name, filter=filter)
|
||||||
|
session.add(setting)
|
||||||
|
await session.commit()
|
||||||
|
|||||||
32
router.py
Normal file
32
router.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from typing import Callable, overload
|
||||||
|
from .model.descriptors import BotCommand, Command, CommandCallbackContext
|
||||||
|
|
||||||
|
|
||||||
|
class Router:
|
||||||
|
def __init__(self):
|
||||||
|
self._commands = dict[str, BotCommand]()
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def command(self, command: Command): ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def command(self, command: str, caption: str | dict[str, str] | None = None): ...
|
||||||
|
|
||||||
|
def command(
|
||||||
|
self, command: str | Command, caption: str | dict[str, str] | None = None
|
||||||
|
):
|
||||||
|
def decorator(func: Callable[[CommandCallbackContext], None]):
|
||||||
|
if isinstance(command, str):
|
||||||
|
cmd = BotCommand(name=command, handler=func, caption=caption)
|
||||||
|
else:
|
||||||
|
cmd = BotCommand(handler=func, **command.__dict__)
|
||||||
|
self._commands[cmd.name] = cmd
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
199
utils/main.py
Normal file
199
utils/main.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
from babel.support import LazyProxy
|
||||||
|
from inspect import signature
|
||||||
|
from aiogram.types import Message, CallbackQuery
|
||||||
|
from typing import Any, TYPE_CHECKING
|
||||||
|
import ujson as json
|
||||||
|
|
||||||
|
from ..model.bot_entity import BotEntity
|
||||||
|
from ..model.bot_enum import BotEnum
|
||||||
|
from ..model.settings import Settings
|
||||||
|
|
||||||
|
from ..model.descriptors import (
|
||||||
|
EntityFieldDescriptor,
|
||||||
|
EntityDescriptor,
|
||||||
|
EntityItemCaptionCallable,
|
||||||
|
EntityFieldCaptionCallable,
|
||||||
|
EntityPermission,
|
||||||
|
EntityCaptionCallable,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..bot.handlers.context import ContextData, CommandContext
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..model.user import UserBase
|
||||||
|
from ..main import QBotApp
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_permissions(
|
||||||
|
user: "UserBase", entity_descriptor: EntityDescriptor
|
||||||
|
) -> list[EntityPermission]:
|
||||||
|
permissions = list[EntityPermission]()
|
||||||
|
for permission, roles in entity_descriptor.permissions.items():
|
||||||
|
for role in roles:
|
||||||
|
if role in user.roles:
|
||||||
|
permissions.append(permission)
|
||||||
|
break
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_text(text: str, locale: str) -> str:
|
||||||
|
try:
|
||||||
|
obj = json.loads(text) # @IgnoreException
|
||||||
|
except Exception:
|
||||||
|
return text
|
||||||
|
else:
|
||||||
|
return obj.get(locale, obj[list(obj.keys())[0]])
|
||||||
|
|
||||||
|
|
||||||
|
def check_entity_permission(
|
||||||
|
entity: BotEntity, user: "UserBase", permission: EntityPermission
|
||||||
|
) -> bool:
|
||||||
|
perm_mapping = {
|
||||||
|
EntityPermission.LIST: EntityPermission.LIST_ALL,
|
||||||
|
EntityPermission.READ: EntityPermission.READ_ALL,
|
||||||
|
EntityPermission.UPDATE: EntityPermission.UPDATE_ALL,
|
||||||
|
EntityPermission.CREATE: EntityPermission.CREATE_ALL,
|
||||||
|
EntityPermission.DELETE: EntityPermission.DELETE_ALL,
|
||||||
|
}
|
||||||
|
|
||||||
|
if permission not in perm_mapping:
|
||||||
|
raise ValueError(f"Invalid permission: {permission}")
|
||||||
|
|
||||||
|
entity_descriptor = entity.__class__.bot_entity_descriptor
|
||||||
|
permissions = get_user_permissions(user, entity_descriptor)
|
||||||
|
|
||||||
|
if perm_mapping[permission] in permissions:
|
||||||
|
return True
|
||||||
|
|
||||||
|
ownership_filds = entity_descriptor.ownership_fields
|
||||||
|
|
||||||
|
for role in user.roles:
|
||||||
|
if role in ownership_filds:
|
||||||
|
if getattr(entity, ownership_filds[role]) == user.id:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if permission in permissions:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_send_message(message: Message | CallbackQuery):
|
||||||
|
if isinstance(message, Message):
|
||||||
|
return message.answer
|
||||||
|
else:
|
||||||
|
return message.message.edit_text
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def get_entity_item_repr(
|
||||||
|
entity: BotEntity, item_repr: EntityItemCaptionCallable | None = None
|
||||||
|
) -> str:
|
||||||
|
descr = entity.bot_entity_descriptor
|
||||||
|
if item_repr:
|
||||||
|
return item_repr(descr, entity)
|
||||||
|
return (
|
||||||
|
descr.item_repr(descr, entity)
|
||||||
|
if descr.item_repr
|
||||||
|
else f"{
|
||||||
|
get_callable_str(descr.full_name, descr, entity)
|
||||||
|
if descr.full_name
|
||||||
|
else descr.name
|
||||||
|
}: {str(entity.id)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_value_repr(
|
||||||
|
value: Any, field_descriptor: EntityFieldDescriptor, locale: str | None = None
|
||||||
|
) -> str:
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
type_ = field_descriptor.type_base
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return "【✔︎】" if value else "【 】"
|
||||||
|
elif field_descriptor.is_list:
|
||||||
|
if issubclass(type_, BotEntity):
|
||||||
|
return f"[{', '.join([get_entity_item_repr(item) for item in value])}]"
|
||||||
|
elif issubclass(type_, BotEnum):
|
||||||
|
return f"[{', '.join(item.localized(locale) for item in value)}]"
|
||||||
|
elif type_ is str:
|
||||||
|
return f"[{', '.join([f'"{item}"' for item in value])}]"
|
||||||
|
else:
|
||||||
|
return f"[{', '.join([str(item) for item in value])}]"
|
||||||
|
elif issubclass(type_, BotEntity):
|
||||||
|
return get_entity_item_repr(value)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def get_entity_descriptor(
|
||||||
|
app: "QBotApp", callback_data: ContextData
|
||||||
|
) -> EntityDescriptor:
|
||||||
|
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
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime, time
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from types import NoneType, UnionType
|
|
||||||
from sqlmodel import select, column
|
from sqlmodel import select, column
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
from typing import Any, Union, get_origin, get_args, TYPE_CHECKING
|
from typing import Any, Union, get_origin, get_args
|
||||||
|
from types import UnionType, NoneType
|
||||||
import ujson as json
|
import ujson as json
|
||||||
|
|
||||||
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 EntityFieldDescriptor, EntityDescriptor
|
from ..model.descriptors import EntityFieldDescriptor
|
||||||
from ..model import EntityPermission
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ..model.user import UserBase
|
|
||||||
|
|
||||||
|
|
||||||
async def deserialize[T](session: AsyncSession, type_: type[T], value: str = None) -> T:
|
async def deserialize[T](session: AsyncSession, type_: type[T], value: str = None) -> T:
|
||||||
@@ -20,12 +16,12 @@ async def deserialize[T](session: AsyncSession, type_: type[T], value: str = Non
|
|||||||
is_optional = False
|
is_optional = False
|
||||||
if type_origin in [UnionType, Union]:
|
if type_origin in [UnionType, Union]:
|
||||||
args = get_args(type_)
|
args = get_args(type_)
|
||||||
if args[1] == NoneType:
|
if args[1] is NoneType:
|
||||||
type_ = args[0]
|
type_ = args[0]
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
is_optional = True
|
is_optional = True
|
||||||
if get_origin(type_) == list:
|
if get_origin(type_) is list:
|
||||||
arg_type = None
|
arg_type = None
|
||||||
args = get_args(type_)
|
args = get_args(type_)
|
||||||
if args:
|
if args:
|
||||||
@@ -34,7 +30,9 @@ async def deserialize[T](session: AsyncSession, type_: type[T], value: str = Non
|
|||||||
if arg_type:
|
if arg_type:
|
||||||
if issubclass(arg_type, BotEntity):
|
if issubclass(arg_type, BotEntity):
|
||||||
ret = list[arg_type]()
|
ret = list[arg_type]()
|
||||||
items = (await session.exec(select(arg_type).where(column("id").in_(values)))).all()
|
items = (
|
||||||
|
await session.exec(select(arg_type).where(column("id").in_(values)))
|
||||||
|
).all()
|
||||||
for item in items:
|
for item in items:
|
||||||
ret.append(item)
|
ret.append(item)
|
||||||
return ret
|
return ret
|
||||||
@@ -52,13 +50,22 @@ async def deserialize[T](session: AsyncSession, type_: type[T], value: str = Non
|
|||||||
if is_optional and not value:
|
if is_optional and not value:
|
||||||
return None
|
return None
|
||||||
return type_(value)
|
return type_(value)
|
||||||
elif type_ == datetime:
|
elif type_ is time:
|
||||||
if is_optional and not value:
|
if is_optional and not value:
|
||||||
return None
|
return None
|
||||||
return datetime.fromisoformat(value)
|
return time.fromisoformat(value.replace("-", ":"))
|
||||||
elif type_ == bool:
|
elif type_ is datetime:
|
||||||
|
if is_optional and not value:
|
||||||
|
return None
|
||||||
|
if value[-3] == ":":
|
||||||
|
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
||||||
|
elif value[-3] == "-":
|
||||||
|
return datetime.strptime(value, "%Y-%m-%d %H-%M")
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid datetime format")
|
||||||
|
elif type_ is bool:
|
||||||
return value == "True"
|
return value == "True"
|
||||||
elif type_ == Decimal:
|
elif type_ is Decimal:
|
||||||
if is_optional and not value:
|
if is_optional and not value:
|
||||||
return None
|
return None
|
||||||
return Decimal(value)
|
return Decimal(value)
|
||||||
@@ -69,38 +76,17 @@ async def deserialize[T](session: AsyncSession, type_: type[T], value: str = Non
|
|||||||
|
|
||||||
|
|
||||||
def serialize(value: Any, field_descriptor: EntityFieldDescriptor) -> str:
|
def serialize(value: Any, field_descriptor: EntityFieldDescriptor) -> str:
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
type_ = field_descriptor.type_base
|
type_ = field_descriptor.type_base
|
||||||
|
|
||||||
if field_descriptor.is_list:
|
if field_descriptor.is_list:
|
||||||
if issubclass(type_, BotEntity):
|
if issubclass(type_, BotEntity):
|
||||||
return json.dumps([item.id for item in value], ensure_ascii = False)
|
return json.dumps([item.id for item in value], ensure_ascii=False)
|
||||||
elif issubclass(type_, BotEnum):
|
elif issubclass(type_, BotEnum):
|
||||||
return json.dumps([item.value for item in value], ensure_ascii = False)
|
return json.dumps([item.value for item in value], ensure_ascii=False)
|
||||||
else:
|
else:
|
||||||
return json.dumps(value, ensure_ascii = False)
|
return json.dumps(value, ensure_ascii=False)
|
||||||
elif issubclass(type_, BotEntity):
|
elif issubclass(type_, BotEntity):
|
||||||
return str(value.id) if value else ""
|
return str(value.id) if value else ""
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
def get_user_permissions(user: "UserBase", entity_descriptor: EntityDescriptor) -> list[EntityPermission]:
|
|
||||||
|
|
||||||
permissions = list[EntityPermission]()
|
|
||||||
for permission, roles in entity_descriptor.permissions.items():
|
|
||||||
for role in roles:
|
|
||||||
if role in user.roles:
|
|
||||||
permissions.append(permission)
|
|
||||||
break
|
|
||||||
return permissions
|
|
||||||
|
|
||||||
|
|
||||||
def get_local_text(text: str, locale: str) -> str:
|
|
||||||
try:
|
|
||||||
obj = json.loads(text) #@IgnoreException
|
|
||||||
except:
|
|
||||||
return text
|
|
||||||
else:
|
|
||||||
return obj.get(locale, obj[list(obj.keys())[0]])
|
|
||||||
Reference in New Issue
Block a user