diff --git a/__init__.py b/__init__.py
index f885b72..bc69867 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1 +1,13 @@
-from .main import QBotApp as QBotApp, Config as Config
\ No newline at end of file
+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,
+)
diff --git a/api_route/telegram.py b/api_route/telegram.py
index 1fd0d42..80f5884 100644
--- a/api_route/telegram.py
+++ b/api_route/telegram.py
@@ -1,8 +1,9 @@
from typing import Annotated
from fastapi import APIRouter, Depends, Request, Response
from sqlmodel.ext.asyncio.session import AsyncSession
-
from ..main import QBotApp
+
+
from ..db import get_db
from aiogram.types import Update
from logging import getLogger
@@ -14,24 +15,25 @@ router = APIRouter()
@router.post("/webhook")
-async def telegram_webhook(db_session: Annotated[AsyncSession, Depends(get_db)],
- request: Request):
-
+async def telegram_webhook(
+ db_session: Annotated[AsyncSession, Depends(get_db)], request: Request
+):
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")
if request_token != app.bot_auth_token:
logger.warning("Unauthorized request %s", request)
- return Response(status_code = 403)
+ return Response(status_code=403)
try:
update = Update(**await request.json())
- except:
- logger.error("Invalid request", exc_info = True)
- return Response(status_code = 400)
+ except Exception:
+ logger.error("Invalid request", exc_info=True)
+ return Response(status_code=400)
try:
- await app.dp.feed_webhook_update(app.bot, update, db_session = db_session, app = app)
- except:
- logger.error("Error processing update", exc_info = True)
- return Response(status_code = 200)
-
\ No newline at end of file
+ await app.dp.feed_webhook_update(
+ app.bot, update, db_session=db_session, app=app
+ )
+ except Exception:
+ logger.error("Error processing update", exc_info=True)
+ return Response(status_code=200)
diff --git a/auth/__init__.py b/auth/__init__.py
new file mode 100644
index 0000000..2691a00
--- /dev/null
+++ b/auth/__init__.py
@@ -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
diff --git a/bot/command_context_filter.py b/bot/command_context_filter.py
index c08dd53..277b796 100644
--- a/bot/command_context_filter.py
+++ b/bot/command_context_filter.py
@@ -8,7 +8,6 @@ logger = getLogger(__name__)
class CallbackCommandFilter(Filter):
-
def __init__(self, command: CallbackCommand):
self.command = command
@@ -19,11 +18,9 @@ class CallbackCommandFilter(Filter):
if context_data:
try:
context_data = ContextData.unpack(context_data)
- except Exception as e:
- logger.error(f"Error unpacking context data", exc_info = True)
+ except Exception:
+ logger.error("Error unpacking context data", exc_info=True)
return False
else:
return context_data.command == self.command
return False
-
-
\ No newline at end of file
diff --git a/bot/handlers/common/__init__.py b/bot/handlers/common/__init__.py
deleted file mode 100644
index 65f3fa1..0000000
--- a/bot/handlers/common/__init__.py
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/bot/handlers/common/filtering.py b/bot/handlers/common/filtering.py
new file mode 100644
index 0000000..f19dc5b
--- /dev/null
+++ b/bot/handlers/common/filtering.py
@@ -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(),
+ )
+ )
diff --git a/bot/handlers/common/filtering_callbacks.py b/bot/handlers/common/filtering_callbacks.py
new file mode 100644
index 0000000..3ebfe42
--- /dev/null
+++ b/bot/handlers/common/filtering_callbacks.py
@@ -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)
diff --git a/bot/handlers/common/pagination.py b/bot/handlers/common/pagination.py
new file mode 100644
index 0000000..120b970
--- /dev/null
+++ b/bot/handlers/common/pagination.py
@@ -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)
diff --git a/bot/handlers/common/routing.py b/bot/handlers/common/routing.py
new file mode 100644
index 0000000..804df67
--- /dev/null
+++ b/bot/handlers/common/routing.py
@@ -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")
diff --git a/bot/handlers/context.py b/bot/handlers/context.py
index 1a20275..4c46298 100644
--- a/bot/handlers/context.py
+++ b/bot/handlers/context.py
@@ -1,8 +1,8 @@
from aiogram.filters.callback_data import CallbackData as BaseCallbackData
from enum import StrEnum
-class CallbackCommand(StrEnum):
+class CallbackCommand(StrEnum):
FIELD_EDITOR = "fe"
FIELD_EDITOR_CALLBACK = "fc"
ENTITY_LIST = "el"
@@ -16,25 +16,28 @@ class CallbackCommand(StrEnum):
SET_LANGUAGE = "ls"
DATE_PICKER_MONTH = "dm"
DATE_PICKER_YEAR = "dy"
- #STRING_EDITOR_LOCALE = "sl"
+ TIME_PICKER = "tp"
+ # STRING_EDITOR_LOCALE = "sl"
ENTITY_PICKER_PAGE = "ep"
ENTITY_PICKER_TOGGLE_ITEM = "et"
VIEW_FILTER_EDIT = "vf"
USER_COMMAND = "uc"
-class CommandContext(StrEnum):
+class CommandContext(StrEnum):
SETTING_EDIT = "se"
ENTITY_CREATE = "ec"
ENTITY_EDIT = "ee"
ENTITY_FIELD_EDIT = "ef"
-class ContextData(BaseCallbackData, prefix = "cd"):
+
+class ContextData(BaseCallbackData, prefix="cd"):
command: CallbackCommand
context: CommandContext | None = None
entity_name: str | None = None
entity_id: int | None = None
field_name: str | None = None
+ form_params: str | None = None
user_command: str | None = None
data: str | None = None
back: bool = False
diff --git a/bot/handlers/editors/__init__.py b/bot/handlers/editors/__init__.py
deleted file mode 100644
index 5ee2768..0000000
--- a/bot/handlers/editors/__init__.py
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/bot/handlers/editors/boolean.py b/bot/handlers/editors/boolean.py
index 9dd70c8..f435d8b 100644
--- a/bot/handlers/editors/boolean.py
+++ b/bot/handlers/editors/boolean.py
@@ -5,22 +5,23 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from babel.support import LazyProxy
from logging import getLogger
-from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
+from ....model.descriptors import EntityFieldDescriptor
from ..context import ContextData, CallbackCommand
-from ..common import get_send_message
-from .common import wrap_editor
+from ....utils.main import get_send_message
+from .wrapper import wrap_editor
logger = getLogger(__name__)
router = Router()
-async def bool_editor(message: Message | CallbackQuery,
- edit_prompt: str,
- field_descriptor: EntityFieldDescriptor,
- callback_data: ContextData,
- **kwargs):
-
+async def bool_editor(
+ message: Message | CallbackQuery,
+ edit_prompt: str,
+ field_descriptor: EntityFieldDescriptor,
+ callback_data: ContextData,
+ **kwargs,
+):
keyboard_builder = InlineKeyboardBuilder()
if isinstance(field_descriptor.bool_true_value, LazyProxy):
@@ -34,41 +35,44 @@ async def bool_editor(message: Message | CallbackQuery,
false_caption = field_descriptor.bool_false_value
keyboard_builder.row(
- InlineKeyboardButton(text = true_caption,
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR_CALLBACK,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = str(True),
- save_state = True).pack()),
- InlineKeyboardButton(text = false_caption,
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR_CALLBACK,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = str(False),
- save_state = True).pack())
+ InlineKeyboardButton(
+ text=true_caption,
+ callback_data=ContextData(
+ command=CallbackCommand.FIELD_EDITOR_CALLBACK,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=str(True),
+ ).pack(),
+ ),
+ InlineKeyboardButton(
+ text=false_caption,
+ callback_data=ContextData(
+ command=CallbackCommand.FIELD_EDITOR_CALLBACK,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=str(False),
+ ).pack(),
+ ),
)
state_data = kwargs["state_data"]
- await wrap_editor(keyboard_builder = keyboard_builder,
- field_descriptor = field_descriptor,
- callback_data = callback_data,
- state_data = state_data)
+ await wrap_editor(
+ keyboard_builder=keyboard_builder,
+ field_descriptor=field_descriptor,
+ callback_data=callback_data,
+ state_data=state_data,
+ )
state: FSMContext = kwargs["state"]
- await state.set_data(state_data)
+ await state.set_data(state_data)
send_message = get_send_message(message)
- await send_message(text = edit_prompt, reply_markup = keyboard_builder.as_markup())
-
-
-
-
-
\ No newline at end of file
+ await send_message(text=edit_prompt, reply_markup=keyboard_builder.as_markup())
diff --git a/bot/handlers/editors/common.py b/bot/handlers/editors/common.py
index 3f405a2..5d3c47a 100644
--- a/bot/handlers/editors/common.py
+++ b/bot/handlers/editors/common.py
@@ -1,67 +1,75 @@
-from types import NoneType, UnionType
-from typing import get_args, get_origin
-from aiogram.fsm.context import FSMContext
-from aiogram.types import InlineKeyboardButton
-from aiogram.utils.keyboard import InlineKeyboardBuilder
+from aiogram.types import Message, CallbackQuery
+from decimal import Decimal
+from datetime import datetime, time
-from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
+from ....model.bot_entity import BotEntity
+from ....model.bot_enum import BotEnum
+from ....model.descriptors import EntityFieldDescriptor
from ....model.settings import Settings
-from ..context import ContextData, CallbackCommand, CommandContext
-from ..navigation import get_navigation_context, pop_navigation_context
+from ....model.user import UserBase
+from ....utils.main import get_callable_str, get_value_repr
+from ..context import ContextData, CommandContext
+from .boolean import bool_editor
+from .date import date_picker, time_picker
+from .entity import entity_picker
+from .string import string_editor
-async def wrap_editor(keyboard_builder: InlineKeyboardBuilder,
- field_descriptor: EntityFieldDescriptor,
- callback_data: ContextData,
- state_data: dict):
-
- if callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT, CommandContext.ENTITY_FIELD_EDIT]:
-
- btns = []
- entity_descriptor = field_descriptor.entity_descriptor
- field_index = (entity_descriptor.field_sequence.index(field_descriptor.name)
- if callback_data.context in [CommandContext.ENTITY_CREATE, CommandContext.ENTITY_EDIT]
- else 0)
-
- stack, context = get_navigation_context(state_data = state_data)
- context = pop_navigation_context(stack)
+async def show_editor(message: Message | CallbackQuery, **kwargs):
+ field_descriptor: EntityFieldDescriptor = kwargs["field_descriptor"]
+ current_value = kwargs["current_value"]
+ user: UserBase = kwargs["user"]
+ callback_data: ContextData = kwargs.get("callback_data", None)
+ state_data: dict = kwargs["state_data"]
- if field_index > 0:
- btns.append(InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = entity_descriptor.field_sequence[field_index - 1]).pack()))
-
- if field_descriptor.is_optional:
- btns.append(InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_SKIP_BTN)),
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR_CALLBACK,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = "skip").pack()))
-
- keyboard_builder.row(*btns)
+ value_type = field_descriptor.type_base
-
- keyboard_builder.row(InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_CANCEL_BTN)),
- callback_data = context.pack()))
+ if field_descriptor.edit_prompt:
+ edit_prompt = get_callable_str(
+ field_descriptor.edit_prompt, field_descriptor, None, current_value
+ )
+ else:
+ if field_descriptor.caption:
+ caption_str = get_callable_str(
+ field_descriptor.caption, field_descriptor, None, current_value
+ )
+ else:
+ caption_str = field_descriptor.name
+ if callback_data.context == CommandContext.ENTITY_EDIT:
+ edit_prompt = (
+ await Settings.get(
+ Settings.APP_STRINGS_FIELD_EDIT_PROMPT_TEMPLATE_P_NAME_VALUE
+ )
+ ).format(
+ name=caption_str,
+ value=get_value_repr(current_value, field_descriptor, user.lang),
+ )
+ else:
+ edit_prompt = (
+ await Settings.get(
+ Settings.APP_STRINGS_FIELD_CREATE_PROMPT_TEMPLATE_P_NAME
+ )
+ ).format(name=caption_str)
- elif callback_data.context == CommandContext.SETTING_EDIT:
+ kwargs["edit_prompt"] = edit_prompt
- keyboard_builder.row(
- InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_CANCEL_BTN)),
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR_CALLBACK,
- context = callback_data.context,
- field_name = callback_data.field_name,
- data = "cancel").pack()))
-
+ if value_type not in [int, float, Decimal, str]:
+ state_data.update({"context_data": callback_data.pack()})
+
+ if value_type is bool:
+ await bool_editor(message=message, **kwargs)
+
+ elif value_type in [int, float, Decimal, str]:
+ await string_editor(message=message, **kwargs)
+
+ elif value_type is datetime:
+ await date_picker(message=message, **kwargs)
+
+ elif value_type is time:
+ await time_picker(message=message, **kwargs)
+
+ elif issubclass(value_type, BotEntity) or issubclass(value_type, BotEnum):
+ await entity_picker(message=message, **kwargs)
+
+ else:
+ raise ValueError(f"Unsupported field type: {value_type}")
diff --git a/bot/handlers/editors/date.py b/bot/handlers/editors/date.py
index f43c323..375490d 100644
--- a/bot/handlers/editors/date.py
+++ b/bot/handlers/editors/date.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timedelta
+from datetime import datetime, time, timedelta
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
@@ -6,10 +6,11 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from typing import TYPE_CHECKING
-from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
+from ....model.descriptors import EntityFieldDescriptor
+from ....model.settings import Settings
from ..context import ContextData, CallbackCommand
-from ..common import get_send_message, get_field_descriptor, get_entity_descriptor
-from .common import wrap_editor
+from ....utils.main import get_send_message, get_field_descriptor
+from .wrapper import wrap_editor
if TYPE_CHECKING:
from ....main import QBotApp
@@ -19,154 +20,343 @@ logger = getLogger(__name__)
router = Router()
-async def date_picker(message: Message | CallbackQuery,
- field_descriptor: EntityFieldDescriptor,
- callback_data: ContextData,
- current_value: datetime,
- state: FSMContext,
- edit_prompt: str | None = None,
- **kwargs):
-
- if not current_value:
- start_date = datetime.now()
- else:
- start_date = current_value
-
- start_date = start_date.replace(day = 1)
-
- previous_month = start_date - timedelta(days = 1)
- next_month = start_date.replace(day = 28) + timedelta(days = 4)
-
- keyboard_builder = InlineKeyboardBuilder()
- keyboard_builder.row(InlineKeyboardButton(text = "◀️",
- callback_data = ContextData(
- command = CallbackCommand.DATE_PICKER_MONTH,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = previous_month.strftime("%Y-%m-%d"),
- save_state = True).pack()),
- InlineKeyboardButton(text = start_date.strftime("%b %Y"),
- callback_data = ContextData(
- command = CallbackCommand.DATE_PICKER_YEAR,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = start_date.strftime("%Y-%m-%d"),
- save_state = True).pack()),
- InlineKeyboardButton(text = "▶️",
- callback_data = ContextData(
- command = CallbackCommand.DATE_PICKER_MONTH,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = next_month.strftime("%Y-%m-%d"),
- save_state = True).pack()))
-
- first_day = start_date - timedelta(days = start_date.weekday())
- weeks = (((start_date.replace(day = 28) + timedelta(days = 4)).replace(day = 1) - first_day).days - 1) // 7 + 1
- for week in range(weeks):
- buttons = []
- for day in range(7):
- current_day = first_day + timedelta(days = week * 7 + day)
- buttons.append(InlineKeyboardButton(text = current_day.strftime("%d"),
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR_CALLBACK,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = current_day.strftime("%Y-%m-%d"),
- save_state = True).pack()))
-
- keyboard_builder.row(*buttons)
-
- state_data = kwargs["state_data"]
-
- await wrap_editor(keyboard_builder = keyboard_builder,
- field_descriptor = field_descriptor,
- callback_data = callback_data,
- state_data = state_data)
-
- await state.set_data(state_data)
-
- if edit_prompt:
- send_message = get_send_message(message)
- await send_message(text = edit_prompt, reply_markup = keyboard_builder.as_markup())
- else:
- await message.edit_reply_markup(reply_markup = keyboard_builder.as_markup())
-
-
-@router.callback_query(ContextData.filter(F.command == CallbackCommand.DATE_PICKER_YEAR))
-async def date_picker_year(query: CallbackQuery,
- callback_data: ContextData,
- app: "QBotApp",
- state: FSMContext,
- **kwargs):
-
- start_date = datetime.strptime(callback_data.data, "%Y-%m-%d")
-
- state_data = await state.get_data()
- kwargs["state_data"] = state_data
-
- keyboard_builder = InlineKeyboardBuilder()
- keyboard_builder.row(InlineKeyboardButton(text = "🔼",
- callback_data = ContextData(
- command = CallbackCommand.DATE_PICKER_YEAR,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = start_date.replace(year = start_date.year - 20).strftime("%Y-%m-%d")).pack()))
-
- for r in range(4):
- buttons = []
- for c in range(5):
- current_date = start_date.replace(year = start_date.year + r * 5 + c - 10)
- buttons.append(InlineKeyboardButton(text = current_date.strftime("%Y"),
- callback_data = ContextData(
- command = CallbackCommand.DATE_PICKER_MONTH,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = current_date.strftime("%Y-%m-%d"),
- save_state = True).pack()))
-
- keyboard_builder.row(*buttons)
-
- keyboard_builder.row(InlineKeyboardButton(text = "🔽",
- callback_data = ContextData(
- command = CallbackCommand.DATE_PICKER_YEAR,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = start_date.replace(year = start_date.year + 20).strftime("%Y-%m-%d")).pack()))
-
- field_descriptor = get_field_descriptor(app, callback_data)
-
- await wrap_editor(keyboard_builder = keyboard_builder,
- field_descriptor = field_descriptor,
- callback_data = callback_data,
- state_data = state_data)
-
- await query.message.edit_reply_markup(reply_markup = keyboard_builder.as_markup())
-
-
-@router.callback_query(ContextData.filter(F.command == CallbackCommand.DATE_PICKER_MONTH))
-async def date_picker_month(query: CallbackQuery, callback_data: ContextData, app: "QBotApp", **kwargs):
+@router.callback_query(ContextData.filter(F.command == CallbackCommand.TIME_PICKER))
+async def time_picker_callback(
+ query: CallbackQuery, callback_data: ContextData, app: "QBotApp", **kwargs
+):
+ if not callback_data.data:
+ return
field_descriptor = get_field_descriptor(app, callback_data)
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
- await date_picker(query.message,
- field_descriptor = field_descriptor,
- callback_data = callback_data,
- current_value = datetime.strptime(callback_data.data, "%Y-%m-%d"),
- **kwargs)
\ No newline at end of file
+ await time_picker(
+ query.message,
+ field_descriptor=field_descriptor,
+ callback_data=callback_data,
+ current_value=datetime.strptime(callback_data.data, "%Y-%m-%d %H-%M")
+ if len(callback_data.data) > 10
+ else time.fromisoformat(callback_data.data.replace("-", ":")),
+ **kwargs,
+ )
+
+
+async def time_picker(
+ message: Message | CallbackQuery,
+ field_descriptor: EntityFieldDescriptor,
+ callback_data: ContextData,
+ current_value: datetime | time,
+ state: FSMContext,
+ edit_prompt: str | None = None,
+ **kwargs,
+):
+ keyboard_builder = InlineKeyboardBuilder()
+
+ for i in range(12):
+ keyboard_builder.row(
+ InlineKeyboardButton(
+ text=(
+ "▶︎ {v:02d} ◀︎" if i == (current_value.hour % 12) else "{v:02d}"
+ ).format(v=i if current_value.hour < 12 else i + 12),
+ callback_data=ContextData(
+ command=CallbackCommand.TIME_PICKER,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=current_value.replace(
+ hour=i if current_value.hour < 12 else i + 12
+ ).strftime(
+ "%Y-%m-%d %H-%M"
+ if isinstance(current_value, datetime)
+ else "%H-%M"
+ )
+ if i != current_value.hour % 12
+ else None,
+ ).pack(),
+ ),
+ InlineKeyboardButton(
+ text=(
+ "▶︎ {v:02d} ◀︎" if i == current_value.minute // 5 else "{v:02d}"
+ ).format(v=i * 5),
+ callback_data=ContextData(
+ command=CallbackCommand.TIME_PICKER,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=current_value.replace(minute=i * 5).strftime(
+ "%Y-%m-%d %H-%M"
+ if isinstance(current_value, datetime)
+ else "%H-%M"
+ )
+ if i != current_value.minute // 5
+ else None,
+ ).pack(),
+ ),
+ )
+ keyboard_builder.row(
+ InlineKeyboardButton(
+ text="AM/PM",
+ callback_data=ContextData(
+ command=CallbackCommand.TIME_PICKER,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=current_value.replace(
+ hour=current_value.hour + 12
+ if current_value.hour < 12
+ else current_value.hour - 12
+ ).strftime(
+ "%Y-%m-%d %H-%M" if isinstance(current_value, datetime) else "%H-%M"
+ ),
+ ).pack(),
+ ),
+ InlineKeyboardButton(
+ text=await Settings.get(Settings.APP_STRINGS_DONE_BTN),
+ callback_data=ContextData(
+ command=CallbackCommand.FIELD_EDITOR_CALLBACK,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=current_value.strftime(
+ "%Y-%m-%d %H-%M" if isinstance(current_value, datetime) else "%H-%M"
+ ),
+ ).pack(),
+ ),
+ )
+
+ state_data = kwargs["state_data"]
+
+ await wrap_editor(
+ keyboard_builder=keyboard_builder,
+ field_descriptor=field_descriptor,
+ callback_data=callback_data,
+ state_data=state_data,
+ )
+
+ await state.set_data(state_data)
+
+ if edit_prompt:
+ send_message = get_send_message(message)
+ await send_message(text=edit_prompt, reply_markup=keyboard_builder.as_markup())
+ else:
+ await message.edit_reply_markup(reply_markup=keyboard_builder.as_markup())
+
+
+async def date_picker(
+ message: Message | CallbackQuery,
+ field_descriptor: EntityFieldDescriptor,
+ callback_data: ContextData,
+ current_value: datetime,
+ state: FSMContext,
+ edit_prompt: str | None = None,
+ **kwargs,
+):
+ if not current_value:
+ start_date = datetime.now()
+ else:
+ start_date = current_value
+
+ start_date = current_value.replace(day=1)
+
+ previous_month = start_date - timedelta(days=1)
+ next_month = start_date.replace(day=28) + timedelta(days=4)
+
+ keyboard_builder = InlineKeyboardBuilder()
+ keyboard_builder.row(
+ InlineKeyboardButton(
+ text="◀️",
+ callback_data=ContextData(
+ command=CallbackCommand.DATE_PICKER_MONTH,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=previous_month.strftime("%Y-%m-%d %H-%M"),
+ ).pack(),
+ ),
+ InlineKeyboardButton(
+ text=start_date.strftime("%b %Y"),
+ callback_data=ContextData(
+ command=CallbackCommand.DATE_PICKER_YEAR,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=start_date.strftime("%Y-%m-%d %H-%M"),
+ ).pack(),
+ ),
+ InlineKeyboardButton(
+ text="▶️",
+ callback_data=ContextData(
+ command=CallbackCommand.DATE_PICKER_MONTH,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=next_month.strftime("%Y-%m-%d %H-%M"),
+ ).pack(),
+ ),
+ )
+
+ first_day = start_date - timedelta(days=start_date.weekday())
+ weeks = (
+ (
+ (start_date.replace(day=28) + timedelta(days=4)).replace(day=1) - first_day
+ ).days
+ - 1
+ ) // 7 + 1
+ for week in range(weeks):
+ buttons = []
+ for day in range(7):
+ current_day = first_day + timedelta(days=week * 7 + day)
+ buttons.append(
+ InlineKeyboardButton(
+ text=current_day.strftime("%d"),
+ callback_data=ContextData(
+ command=CallbackCommand.FIELD_EDITOR_CALLBACK
+ if field_descriptor.dt_type == "date"
+ else CallbackCommand.TIME_PICKER,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=current_day.strftime("%Y-%m-%d %H-%M"),
+ ).pack(),
+ )
+ )
+
+ keyboard_builder.row(*buttons)
+
+ state_data = kwargs["state_data"]
+
+ await wrap_editor(
+ keyboard_builder=keyboard_builder,
+ field_descriptor=field_descriptor,
+ callback_data=callback_data,
+ state_data=state_data,
+ )
+
+ await state.set_data(state_data)
+
+ if edit_prompt:
+ send_message = get_send_message(message)
+ await send_message(text=edit_prompt, reply_markup=keyboard_builder.as_markup())
+ else:
+ await message.edit_reply_markup(reply_markup=keyboard_builder.as_markup())
+
+
+@router.callback_query(
+ ContextData.filter(F.command == CallbackCommand.DATE_PICKER_YEAR)
+)
+async def date_picker_year(
+ query: CallbackQuery,
+ callback_data: ContextData,
+ app: "QBotApp",
+ state: FSMContext,
+ **kwargs,
+):
+ start_date = datetime.strptime(callback_data.data, "%Y-%m-%d %H-%M")
+
+ state_data = await state.get_data()
+ kwargs["state_data"] = state_data
+
+ keyboard_builder = InlineKeyboardBuilder()
+ keyboard_builder.row(
+ InlineKeyboardButton(
+ text="🔼",
+ callback_data=ContextData(
+ command=CallbackCommand.DATE_PICKER_YEAR,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=start_date.replace(year=start_date.year - 20).strftime(
+ "%Y-%m-%d %H-%M"
+ ),
+ ).pack(),
+ )
+ )
+
+ for r in range(4):
+ buttons = []
+ for c in range(5):
+ current_date = start_date.replace(year=start_date.year + r * 5 + c - 10)
+ buttons.append(
+ InlineKeyboardButton(
+ text=current_date.strftime("%Y"),
+ callback_data=ContextData(
+ command=CallbackCommand.DATE_PICKER_MONTH,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=current_date.strftime("%Y-%m-%d %H-%M"),
+ ).pack(),
+ )
+ )
+
+ keyboard_builder.row(*buttons)
+
+ keyboard_builder.row(
+ InlineKeyboardButton(
+ text="🔽",
+ callback_data=ContextData(
+ command=CallbackCommand.DATE_PICKER_YEAR,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=start_date.replace(year=start_date.year + 20).strftime(
+ "%Y-%m-%d %H-%M"
+ ),
+ ).pack(),
+ )
+ )
+
+ field_descriptor = get_field_descriptor(app, callback_data)
+
+ await wrap_editor(
+ keyboard_builder=keyboard_builder,
+ field_descriptor=field_descriptor,
+ callback_data=callback_data,
+ state_data=state_data,
+ )
+
+ await query.message.edit_reply_markup(reply_markup=keyboard_builder.as_markup())
+
+
+@router.callback_query(
+ ContextData.filter(F.command == CallbackCommand.DATE_PICKER_MONTH)
+)
+async def date_picker_month(
+ query: CallbackQuery, callback_data: ContextData, app: "QBotApp", **kwargs
+):
+ field_descriptor = get_field_descriptor(app, callback_data)
+ state: FSMContext = kwargs["state"]
+ state_data = await state.get_data()
+ kwargs["state_data"] = state_data
+
+ await date_picker(
+ query.message,
+ field_descriptor=field_descriptor,
+ callback_data=callback_data,
+ current_value=datetime.strptime(callback_data.data, "%Y-%m-%d %H-%M"),
+ **kwargs,
+ )
diff --git a/bot/handlers/editors/entity.py b/bot/handlers/editors/entity.py
index 1e348fc..b9ffa3d 100644
--- a/bot/handlers/editors/entity.py
+++ b/bot/handlers/editors/entity.py
@@ -1,25 +1,32 @@
-from types import UnionType
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
+from sqlmodel import column
from sqlmodel.ext.asyncio.session import AsyncSession
-from typing import get_args, get_origin, TYPE_CHECKING
+from typing import TYPE_CHECKING
from ....model.bot_entity import BotEntity
-from ....model.owned_bot_entity import OwnedBotEntity
+
+# from ....model.owned_bot_entity import OwnedBotEntity
from ....model.bot_enum import BotEnum
from ....model.settings import Settings
from ....model.user import UserBase
from ....model.view_setting import ViewSetting
-from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
+from ....model.descriptors import EntityFieldDescriptor, Filter
from ....model import EntityPermission
-from ....utils import serialize, deserialize, get_user_permissions
+from ....utils.main import (
+ get_user_permissions,
+ get_send_message,
+ get_field_descriptor,
+ get_callable_str,
+)
+from ....utils.serialization import serialize, deserialize
from ..context import ContextData, CallbackCommand
-from ..common import (get_send_message, get_local_text, get_field_descriptor,
- get_entity_descriptor, add_pagination_controls, add_filter_controls)
-from .common import wrap_editor
+from ..common.pagination import add_pagination_controls
+from ..common.filtering import add_filter_controls
+from .wrapper import wrap_editor
if TYPE_CHECKING:
from ....main import QBotApp
@@ -28,179 +35,295 @@ logger = getLogger(__name__)
router = Router()
-async def entity_picker(message: Message | CallbackQuery,
- field_descriptor: EntityFieldDescriptor,
- edit_prompt: str,
- current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
- **kwargs):
-
+async def entity_picker(
+ message: Message | CallbackQuery,
+ field_descriptor: EntityFieldDescriptor,
+ edit_prompt: str,
+ current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
+ **kwargs,
+):
state_data: dict = kwargs["state_data"]
- state_data.update({"current_value": serialize(current_value, field_descriptor),
- "value": serialize(current_value, field_descriptor),
- "edit_prompt": edit_prompt})
+ state_data.update(
+ {
+ "current_value": serialize(current_value, field_descriptor),
+ "value": serialize(current_value, field_descriptor),
+ "edit_prompt": edit_prompt,
+ }
+ )
- await render_entity_picker(field_descriptor = field_descriptor,
- message = message,
- current_value = current_value,
- edit_prompt = edit_prompt,
- **kwargs)
+ await render_entity_picker(
+ field_descriptor=field_descriptor,
+ message=message,
+ current_value=current_value,
+ edit_prompt=edit_prompt,
+ **kwargs,
+ )
def calc_total_pages(items_count: int, page_size: int) -> int:
-
return max(items_count // page_size + (1 if items_count % page_size else 0), 1)
-async def render_entity_picker(*,
- field_descriptor: EntityFieldDescriptor,
- message: Message | CallbackQuery,
- callback_data: ContextData,
- user: UserBase,
- db_session: AsyncSession,
- state: FSMContext,
- current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
- edit_prompt: str,
- page: int = 1,
- **kwargs):
-
-
- if callback_data.command in [CallbackCommand.ENTITY_PICKER_PAGE, CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM]:
+async def render_entity_picker(
+ *,
+ field_descriptor: EntityFieldDescriptor,
+ message: Message | CallbackQuery,
+ callback_data: ContextData,
+ user: UserBase,
+ db_session: AsyncSession,
+ state: FSMContext,
+ current_value: BotEntity | BotEnum | list[BotEntity] | list[BotEnum],
+ edit_prompt: str,
+ page: int = 1,
+ **kwargs,
+):
+ if callback_data.command in [
+ CallbackCommand.ENTITY_PICKER_PAGE,
+ CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM,
+ ]:
page = int(callback_data.data.split("&")[0])
- # is_list = False
-
- # type_origin = get_origin(field_descriptor.type_)
- # if type_origin == UnionType:
- # type_ = get_args(field_descriptor.type_)[0]
-
- # elif type_origin == list:
- # type_ = get_args(field_descriptor.type_)[0]
- # is_list = True
-
- # else:
- # type_ = field_descriptor.type_
-
type_ = field_descriptor.type_base
is_list = field_descriptor.is_list
-
+
if not issubclass(type_, BotEntity) and not issubclass(type_, BotEnum):
raise ValueError("Unsupported type")
-
+
page_size = await Settings.get(Settings.PAGE_SIZE)
+ form_list = None
if issubclass(type_, BotEnum):
items_count = len(type_.all_members)
total_pages = calc_total_pages(items_count, page_size)
page = min(page, total_pages)
- enum_items = list(type_.all_members.values())[page_size * (page - 1):page_size * page]
- items = [{"text": f"{"" if not is_list else "【✔︎】 " if item in (current_value or []) else "【 】 "}{item.localized(user.lang)}",
- "value": item.value} for item in enum_items]
- else:
+ enum_items = list(type_.all_members.values())[
+ page_size * (page - 1) : page_size * page
+ ]
+ items = [
+ {
+ "text": f"{'' if not is_list else '【✔︎】 ' if item in (current_value or []) else '【 】 '}{item.localized(user.lang)}",
+ "value": item.value,
+ }
+ for item in enum_items
+ ]
+ elif issubclass(type_, BotEntity):
+ form_name = field_descriptor.ep_form or "default"
+ form_list = type_.bot_entity_descriptor.lists.get(
+ form_name, type_.bot_entity_descriptor.default_list
+ )
permissions = get_user_permissions(user, type_.bot_entity_descriptor)
- entity_filter = await ViewSetting.get_filter(session = db_session, user_id = user.id, entity_name = type_.bot_entity_descriptor.class_name)
- if (EntityPermission.LIST_ALL in permissions or
- (EntityPermission.LIST in permissions and
- not issubclass(type_, OwnedBotEntity))):
- items_count = await type_.get_count(session = db_session, filter = entity_filter)
- total_pages = calc_total_pages(items_count, page_size)
- page = min(page, total_pages)
+ if form_list.filtering:
+ entity_filter = await ViewSetting.get_filter(
+ session=db_session,
+ user_id=user.id,
+ entity_name=type_.bot_entity_descriptor.class_name,
+ )
+ else:
+ entity_filter = None
+ list_all = EntityPermission.LIST_ALL in permissions
+
+ if list_all or EntityPermission.LIST in permissions:
+ if (
+ field_descriptor.ep_parent_field
+ and field_descriptor.ep_parent_field
+ and callback_data.entity_id
+ ):
+ entity = await field_descriptor.entity_descriptor.type_.get(
+ session=db_session, id=callback_data.entity_id
+ )
+ value = getattr(entity, field_descriptor.ep_parent_field)
+ ext_filter = column(field_descriptor.ep_child_field).__eq__(value)
+
+ else:
+ ext_filter = None
+
+ if form_list.pagination:
+ items_count = await type_.get_count(
+ session=db_session,
+ static_filter=(
+ [
+ Filter(
+ field_name=f.field_name,
+ operator=f.operator,
+ value_type="const",
+ value=f.value,
+ )
+ for f in form_list.static_filters
+ if f.value_type == "const"
+ ]
+ if isinstance(form_list.static_filters, list)
+ else form_list.static_filters
+ ),
+ ext_filter=ext_filter,
+ filter=entity_filter,
+ filter_fields=form_list.filtering_fields,
+ user=user if not list_all else None,
+ )
+ total_pages = calc_total_pages(items_count, page_size)
+ page = min(page, total_pages)
+ skip = page_size * (page - 1)
+ limit = page_size
+ else:
+ skip = 0
+ limit = None
entity_items = await type_.get_multi(
- session = db_session, order_by = type_.name, filter = entity_filter,
- skip = page_size * (page - 1), limit = page_size)
- elif (EntityPermission.LIST in permissions and
- issubclass(type_, OwnedBotEntity)):
- items_count = await type_.get_count_by_user(session = db_session, user_id = user.id, filter = entity_filter)
- total_pages = calc_total_pages(items_count, page_size)
- page = min(page, total_pages)
- entity_items = await type_.get_multi_by_user(
- session = db_session, user_id = user.id, order_by = type_.name, filter = entity_filter,
- skip = page_size * (page - 1), limit = page_size)
+ session=db_session,
+ order_by=form_list.order_by,
+ static_filter=(
+ [
+ Filter(
+ field_name=f.field_name,
+ operator=f.operator,
+ value_type="const",
+ value=f.value,
+ )
+ for f in form_list.static_filters
+ if f.value_type == "const"
+ ]
+ if isinstance(form_list.static_filters, list)
+ else form_list.static_filters
+ ),
+ ext_filter=ext_filter,
+ filter=entity_filter,
+ user=user if not list_all else None,
+ skip=skip,
+ limit=limit,
+ )
else:
items_count = 0
total_pages = 1
page = 1
entity_items = list[BotEntity]()
-
- items = [{"text": f"{"" if not is_list else "【✔︎】 " if item in (current_value or []) else "【 】 "}{
- type_.bot_entity_descriptor.item_caption(type_.bot_entity_descriptor, item) if type_.bot_entity_descriptor.item_caption
- else get_local_text(item.name, user.lang) if type_.bot_entity_descriptor.fields_descriptors["name"].localizable else item.name}",
- "value": str(item.id)} for item in entity_items]
-
- # total_pages = items_count // page_size + (1 if items_count % page_size else 0)
-
+
+ items = [
+ {
+ "text": f"{
+ ''
+ if not is_list
+ else '【✔︎】 '
+ if item in (current_value or [])
+ else '【 】 '
+ }{
+ type_.bot_entity_descriptor.item_repr(
+ type_.bot_entity_descriptor, item
+ )
+ if type_.bot_entity_descriptor.item_repr
+ else get_callable_str(
+ type_.bot_entity_descriptor.full_name,
+ type_.bot_entity_descriptor,
+ item,
+ )
+ if type_.bot_entity_descriptor.full_name
+ else f'{type_.bot_entity_descriptor.name}: {str(item.id)}'
+ }",
+ "value": str(item.id),
+ }
+ for item in entity_items
+ ]
+
keyboard_builder = InlineKeyboardBuilder()
for item in items:
keyboard_builder.row(
- InlineKeyboardButton(text = item["text"],
- callback_data = ContextData(
- command = CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM if is_list else CallbackCommand.FIELD_EDITOR_CALLBACK,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- data = f"{page}&{item['value']}" if is_list else item["value"],
- save_state = True).pack()))
-
- add_pagination_controls(keyboard_builder = keyboard_builder,
- callback_data = callback_data,
- total_pages = total_pages,
- command = CallbackCommand.ENTITY_PICKER_PAGE,
- page = page)
-
- if issubclass(type_, BotEntity):
- add_filter_controls(keyboard_builder = keyboard_builder,
- entity_descriptor = type_.bot_entity_descriptor,
- filter = entity_filter)
+ InlineKeyboardButton(
+ text=item["text"],
+ callback_data=ContextData(
+ command=(
+ CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM
+ if is_list
+ else CallbackCommand.FIELD_EDITOR_CALLBACK
+ ),
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ data=f"{page}&{item['value']}" if is_list else item["value"],
+ ).pack(),
+ )
+ )
+
+ if form_list and form_list.pagination:
+ add_pagination_controls(
+ keyboard_builder=keyboard_builder,
+ callback_data=callback_data,
+ total_pages=total_pages,
+ command=CallbackCommand.ENTITY_PICKER_PAGE,
+ page=page,
+ )
+
+ if (
+ issubclass(type_, BotEntity)
+ and form_list.filtering
+ and form_list.filtering_fields
+ ):
+ add_filter_controls(
+ keyboard_builder=keyboard_builder,
+ entity_descriptor=type_.bot_entity_descriptor,
+ filter=entity_filter,
+ )
if is_list:
keyboard_builder.row(
- InlineKeyboardButton(text = await Settings.get(Settings.APP_STRINGS_DONE_BTN),
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR_CALLBACK,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- save_state = True).pack()))
+ InlineKeyboardButton(
+ text=await Settings.get(Settings.APP_STRINGS_DONE_BTN),
+ callback_data=ContextData(
+ command=CallbackCommand.FIELD_EDITOR_CALLBACK,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ ).pack(),
+ )
+ )
state_data = kwargs["state_data"]
- await wrap_editor(keyboard_builder = keyboard_builder,
- field_descriptor = field_descriptor,
- callback_data = callback_data,
- state_data = state_data)
+ await wrap_editor(
+ keyboard_builder=keyboard_builder,
+ field_descriptor=field_descriptor,
+ callback_data=callback_data,
+ state_data=state_data,
+ )
await state.set_data(state_data)
send_message = get_send_message(message)
- await send_message(text = edit_prompt, reply_markup = keyboard_builder.as_markup())
+ await send_message(text=edit_prompt, reply_markup=keyboard_builder.as_markup())
-@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_PAGE))
-@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM))
-async def entity_picker_callback(query: CallbackQuery,
- callback_data: ContextData,
- db_session: AsyncSession,
- app: "QBotApp",
- state: FSMContext,
- **kwargs):
-
+@router.callback_query(
+ ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_PAGE)
+)
+@router.callback_query(
+ ContextData.filter(F.command == CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM)
+)
+async def entity_picker_callback(
+ query: CallbackQuery,
+ callback_data: ContextData,
+ db_session: AsyncSession,
+ app: "QBotApp",
+ state: FSMContext,
+ **kwargs,
+):
state_data = await state.get_data()
kwargs["state_data"] = state_data
- field_descriptor = get_field_descriptor(app = app, callback_data = callback_data)
+ field_descriptor = get_field_descriptor(app=app, callback_data=callback_data)
- current_value = await deserialize(session = db_session, type_ = field_descriptor.type_, value = state_data["current_value"])
+ # current_value = await deserialize(session = db_session, type_ = field_descriptor.type_, value = state_data["current_value"])
edit_prompt = state_data["edit_prompt"]
- value = await deserialize(session = db_session, type_ = field_descriptor.type_, value = state_data["value"])
+ value = await deserialize(
+ session=db_session, type_=field_descriptor.type_, value=state_data["value"]
+ )
if callback_data.command == CallbackCommand.ENTITY_PICKER_TOGGLE_ITEM:
page, id_value = callback_data.data.split("&")
page = int(page)
- type_ = field_descriptor.type_base
+ type_ = field_descriptor.type_base
if issubclass(type_, BotEnum):
item = type_(id_value)
if item in value:
@@ -208,7 +331,7 @@ async def entity_picker_callback(query: CallbackQuery,
else:
value.append(item)
else:
- item = await type_.get(session = db_session, id = int(id_value))
+ item = await type_.get(session=db_session, id=int(id_value))
if item in value:
value.remove(item)
else:
@@ -221,16 +344,16 @@ async def entity_picker_callback(query: CallbackQuery,
page = int(callback_data.data)
else:
raise ValueError("Unsupported command")
-
- await render_entity_picker(field_descriptor = field_descriptor,
- message = query,
- callback_data = callback_data,
- current_value = value,
- edit_prompt = edit_prompt,
- db_session = db_session,
- app = app,
- state = state,
- page = page,
- **kwargs)
-
+ await render_entity_picker(
+ field_descriptor=field_descriptor,
+ message=query,
+ callback_data=callback_data,
+ current_value=value,
+ edit_prompt=edit_prompt,
+ db_session=db_session,
+ app=app,
+ state=state,
+ page=page,
+ **kwargs,
+ )
diff --git a/bot/handlers/editors/main.py b/bot/handlers/editors/main.py
new file mode 100644
index 0000000..50580ac
--- /dev/null
+++ b/bot/handlers/editors/main.py
@@ -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,
+)
diff --git a/bot/handlers/editors/main_callbacks.py b/bot/handlers/editors/main_callbacks.py
new file mode 100644
index 0000000..1b18c79
--- /dev/null
+++ b/bot/handlers/editors/main_callbacks.py
@@ -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)
diff --git a/bot/handlers/editors/string.py b/bot/handlers/editors/string.py
index 5b98dcb..91013a0 100644
--- a/bot/handlers/editors/string.py
+++ b/bot/handlers/editors/string.py
@@ -1,98 +1,97 @@
-from types import UnionType
from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, CopyTextButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
-from typing import Any, get_args, get_origin
+from typing import Any
-from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
+from ....model.descriptors import EntityFieldDescriptor
from ....model.language import LanguageBase
from ....model.settings import Settings
-from ....utils import serialize
+from ....utils.main import get_send_message, get_local_text
+from ....utils.serialization import serialize
from ..context import ContextData, CallbackCommand
-from ..common import get_send_message, get_local_text
-from .common import wrap_editor
+from .wrapper import wrap_editor
logger = getLogger(__name__)
router = Router()
-async def string_editor(message: Message | CallbackQuery,
- field_descriptor: EntityFieldDescriptor,
- callback_data: ContextData,
- current_value: Any,
- edit_prompt: str,
- state: FSMContext,
- locale_index: int = 0,
- **kwargs):
-
+async def string_editor(
+ message: Message | CallbackQuery,
+ field_descriptor: EntityFieldDescriptor,
+ callback_data: ContextData,
+ current_value: Any,
+ edit_prompt: str,
+ state: FSMContext,
+ locale_index: int = 0,
+ **kwargs,
+):
keyboard_builder = InlineKeyboardBuilder()
state_data: dict = kwargs["state_data"]
_edit_prompt = edit_prompt
- # type_ = field_descriptor.type_
- # type_origin = get_origin(type_)
- # if type_origin == UnionType:
- # type_ = get_args(type_)[0]
+ context_data = ContextData(
+ command=CallbackCommand.FIELD_EDITOR_CALLBACK,
+ context=callback_data.context,
+ entity_name=callback_data.entity_name,
+ entity_id=callback_data.entity_id,
+ field_name=callback_data.field_name,
+ form_params=callback_data.form_params,
+ )
- if field_descriptor.type_base == str and field_descriptor.localizable:
-
+ if field_descriptor.type_base is str and field_descriptor.localizable:
current_locale = list(LanguageBase.all_members.values())[locale_index]
- context_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR_CALLBACK,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- save_state = True)
-
- _edit_prompt = f"{edit_prompt}\n{(await Settings.get(
- Settings.APP_STRINGS_STRING_EDITOR_LOCALE_TEMPLATE_P_NAME)).format(name = current_locale)}"
- _current_value = get_local_text(current_value, current_locale) if current_value else None
-
- state_data.update({
- "context_data": context_data.pack(),
- "edit_prompt": edit_prompt,
- "locale_index": str(locale_index),
- "current_value": current_value})
-
- else:
- context_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR_CALLBACK,
- context = callback_data.context,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- field_name = callback_data.field_name,
- save_state = True)
+ _edit_prompt = f"{edit_prompt}\n{
+ (
+ await Settings.get(
+ Settings.APP_STRINGS_STRING_EDITOR_LOCALE_TEMPLATE_P_NAME
+ )
+ ).format(name=current_locale)
+ }"
+ _current_value = (
+ get_local_text(current_value, current_locale) if current_value else None
+ )
+
+ state_data.update(
+ {
+ "context_data": context_data.pack(),
+ "edit_prompt": edit_prompt,
+ "locale_index": str(locale_index),
+ "current_value": current_value,
+ }
+ )
+
+ else:
_current_value = serialize(current_value, field_descriptor)
- state_data.update({
- "context_data": context_data.pack()})
-
- if _current_value:
+ state_data.update({"context_data": context_data.pack()})
- _current_value_caption = f"{_current_value[:30]}..." if len(_current_value) > 30 else _current_value
- keyboard_builder.row(InlineKeyboardButton(text = _current_value_caption,
- copy_text = CopyTextButton(text = _current_value)))
+ if _current_value:
+ _current_value_caption = (
+ f"{_current_value[:30]}..." if len(_current_value) > 30 else _current_value
+ )
+ keyboard_builder.row(
+ InlineKeyboardButton(
+ text=_current_value_caption,
+ copy_text=CopyTextButton(text=_current_value),
+ )
+ )
state_data = kwargs["state_data"]
- await wrap_editor(keyboard_builder = keyboard_builder,
- field_descriptor = field_descriptor,
- callback_data = callback_data,
- state_data = state_data)
+ await wrap_editor(
+ keyboard_builder=keyboard_builder,
+ field_descriptor=field_descriptor,
+ callback_data=callback_data,
+ state_data=state_data,
+ )
await state.set_data(state_data)
-
- send_message = get_send_message(message)
- await send_message(text = _edit_prompt, reply_markup = keyboard_builder.as_markup())
-
-# async def context_command_fiter(*args, **kwargs):
-# print(args, kwargs)
-# return True
+ send_message = get_send_message(message)
+ await send_message(text=_edit_prompt, reply_markup=keyboard_builder.as_markup())
diff --git a/bot/handlers/editors/wrapper.py b/bot/handlers/editors/wrapper.py
new file mode 100644
index 0000000..2962395
--- /dev/null
+++ b/bot/handlers/editors/wrapper.py
@@ -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(),
+ )
+ )
diff --git a/bot/handlers/forms/entity_form.py b/bot/handlers/forms/entity_form.py
index 8e04148..3916a8b 100644
--- a/bot/handlers/forms/entity_form.py
+++ b/bot/handlers/forms/entity_form.py
@@ -1,24 +1,28 @@
-from typing import get_args, get_origin, TYPE_CHECKING
+from typing import TYPE_CHECKING
from aiogram import Router, F
-from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import StatesGroup, State
-from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
-from aiogram.utils.i18n import I18n
+from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
-from ....model.bot_entity import BotEntity
-from ....model.bot_enum import BotEnum
-from ....model.owned_bot_entity import OwnedBotEntity
+from ....model.descriptors import FieldEditButton, CommandButton
from ....model.settings import Settings
from ....model.user import UserBase
-from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
from ....model import EntityPermission
-from ....utils import serialize, deserialize, get_user_permissions
+from ....utils.main import (
+ check_entity_permission,
+ get_send_message,
+ clear_state,
+ get_value_repr,
+ get_callable_str,
+ get_entity_descriptor,
+)
from ..context import ContextData, CallbackCommand, CommandContext
-from ..common import get_send_message, get_local_text, get_entity_descriptor, get_callable_str, get_value_repr
+from ..navigation import (
+ pop_navigation_context,
+ save_navigation_context,
+)
if TYPE_CHECKING:
from ....main import QBotApp
@@ -30,204 +34,212 @@ router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_ITEM))
async def entity_item_callback(query: CallbackQuery, **kwargs):
-
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
- clear_state(state_data = state_data)
- stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
-
- await entity_item(query = query, navigation_stack = stack, **kwargs)
+ clear_state(state_data=state_data)
+ stack = save_navigation_context(callback_data=callback_data, state_data=state_data)
+
+ await entity_item(query=query, navigation_stack=stack, **kwargs)
-async def entity_item(query: CallbackQuery,
- callback_data: ContextData,
- db_session: AsyncSession,
- user: UserBase,
- app: "QBotApp",
- navigation_stack: list[ContextData],
- **kwargs):
-
+async def entity_item(
+ query: CallbackQuery,
+ callback_data: ContextData,
+ db_session: AsyncSession,
+ user: UserBase,
+ app: "QBotApp",
+ navigation_stack: list[ContextData],
+ **kwargs,
+):
entity_descriptor = get_entity_descriptor(app, callback_data)
- user_permissions = get_user_permissions(user, entity_descriptor)
- entity_type: BotEntity = entity_descriptor.type_
+ # user_permissions = get_user_permissions(user, entity_descriptor)
+ entity_type = entity_descriptor.type_
keyboard_builder = InlineKeyboardBuilder()
- entity_item = await entity_type.get(session = db_session, id = callback_data.entity_id)
+ entity_item = await entity_type.get(session=db_session, id=callback_data.entity_id)
if not entity_item:
- return await query.answer(text = (await Settings.get(Settings.APP_STRINGS_NOT_FOUND)))
+ return await query.answer(
+ text=(await Settings.get(Settings.APP_STRINGS_NOT_FOUND))
+ )
- is_owned = issubclass(entity_type, OwnedBotEntity)
+ # is_owned = issubclass(entity_type, OwnedBotEntity)
- if (EntityPermission.READ not in user_permissions and
- EntityPermission.READ_ALL not in user_permissions):
+ if not check_entity_permission(
+ entity=entity_item, user=user, permission=EntityPermission.READ
+ ):
+ return await query.answer(
+ text=(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
+ )
- return await query.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
+ can_edit = check_entity_permission(
+ entity=entity_item, user=user, permission=EntityPermission.UPDATE
+ )
- if (is_owned and
- EntityPermission.READ_ALL not in user_permissions and
- entity_item.user_id != user.id):
-
- return await query.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
-
- can_edit = (EntityPermission.UPDATE_ALL in user_permissions or
- (EntityPermission.UPDATE in user_permissions and not is_owned) or
- (EntityPermission.UPDATE in user_permissions and is_owned and
- entity_item.user_id == user.id))
-
- can_delete = (EntityPermission.DELETE_ALL in user_permissions or
- (EntityPermission.DELETE in user_permissions and not is_owned) or
- (EntityPermission.DELETE in user_permissions and is_owned and
- entity_item.user_id == user.id))
+ form = entity_descriptor.forms.get(
+ callback_data.form_params or "default", entity_descriptor.default_form
+ )
if can_edit:
- for edit_buttons_row in entity_descriptor.edit_buttons:
+ for edit_buttons_row in form.form_buttons:
btn_row = []
- for field_name in edit_buttons_row:
- field_name, btn_caption = field_name if isinstance(field_name, tuple) else (field_name, None)
- if field_name in entity_descriptor.fields_descriptors:
- field_descriptor = entity_descriptor.fields_descriptors[field_name]
- # if field_descriptor.is_list and issubclass(field_descriptor.type_base, BotEntity):
- # await field_descriptor.type_base.
- field_value = getattr(entity_item, field_descriptor.field_name)
- if btn_caption:
- btn_text = get_callable_str(btn_caption, field_descriptor, entity_item, field_value)
- else:
- if field_descriptor.type_base == bool:
- btn_text = f"{"【✔︎】 " if field_value else "【 】 "}{
- get_callable_str(field_descriptor.caption, field_descriptor, entity_item, field_value) if field_descriptor.caption
- else field_name}"
+ for button in edit_buttons_row:
+ if isinstance(button, FieldEditButton):
+ field_name = button.field_name
+ btn_caption = button.caption
+ if field_name in entity_descriptor.fields_descriptors:
+ field_descriptor = entity_descriptor.fields_descriptors[
+ field_name
+ ]
+ field_value = getattr(entity_item, field_descriptor.field_name)
+ if btn_caption:
+ btn_text = get_callable_str(
+ btn_caption, field_descriptor, entity_item, field_value
+ )
else:
- btn_text = (f"✏️ {get_callable_str(field_descriptor.caption, field_descriptor, entity_item, field_value)}"
- if field_descriptor.caption else f"✏️ {field_name}")
+ if field_descriptor.type_base is bool:
+ btn_text = f"{'【✔︎】 ' if field_value else '【 】 '}{
+ get_callable_str(
+ field_descriptor.caption,
+ field_descriptor,
+ entity_item,
+ field_value,
+ )
+ if field_descriptor.caption
+ else field_name
+ }"
+ else:
+ btn_text = (
+ f"✏️ {get_callable_str(field_descriptor.caption, field_descriptor, entity_item, field_value)}"
+ if field_descriptor.caption
+ else f"✏️ {field_name}"
+ )
+ btn_row.append(
+ InlineKeyboardButton(
+ text=btn_text,
+ callback_data=ContextData(
+ command=CallbackCommand.FIELD_EDITOR,
+ context=CommandContext.ENTITY_FIELD_EDIT,
+ entity_name=entity_descriptor.name,
+ entity_id=str(entity_item.id),
+ field_name=field_name,
+ ).pack(),
+ )
+ )
+
+ elif isinstance(button, CommandButton):
+ btn_caption = button.caption
+ if btn_caption:
+ btn_text = get_callable_str(
+ btn_caption, entity_descriptor, entity_item
+ )
+ else:
+ btn_text = button.command
btn_row.append(
InlineKeyboardButton(
- text = btn_text,
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR,
- context = CommandContext.ENTITY_FIELD_EDIT,
- entity_name = entity_descriptor.name,
- entity_id = str(entity_item.id),
- field_name = field_name).pack()))
+ text=btn_text,
+ callback_data=(
+ button.context_data.pack()
+ if button.context_data
+ else ContextData(
+ command=CallbackCommand.USER_COMMAND,
+ user_command=button.command,
+ data=str(entity_item.id),
+ ).pack()
+ ),
+ )
+ )
+
if btn_row:
keyboard_builder.row(*btn_row)
edit_delete_row = []
- if can_edit and entity_descriptor.edit_button_visible:
+ if can_edit and form.show_edit_button:
edit_delete_row.append(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_EDIT_BTN)),
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR,
- context = CommandContext.ENTITY_EDIT,
- entity_name = entity_descriptor.name,
- entity_id = str(entity_item.id),
- field_name = entity_descriptor.field_sequence[0]).pack()))
-
- if can_delete:
+ text=(await Settings.get(Settings.APP_STRINGS_EDIT_BTN)),
+ callback_data=ContextData(
+ command=CallbackCommand.FIELD_EDITOR,
+ context=CommandContext.ENTITY_EDIT,
+ entity_name=entity_descriptor.name,
+ entity_id=str(entity_item.id),
+ form_params=callback_data.form_params,
+ field_name=form.edit_field_sequence[0],
+ ).pack(),
+ )
+ )
+
+ if (
+ check_entity_permission(
+ entity=entity_item, user=user, permission=EntityPermission.DELETE
+ )
+ and form.show_delete_button
+ ):
edit_delete_row.append(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_DELETE_BTN)),
- callback_data = ContextData(
- command = CallbackCommand.ENTITY_DELETE,
- entity_name = entity_descriptor.name,
- entity_id = str(entity_item.id)).pack()))
-
+ text=(await Settings.get(Settings.APP_STRINGS_DELETE_BTN)),
+ callback_data=ContextData(
+ command=CallbackCommand.ENTITY_DELETE,
+ entity_name=entity_descriptor.name,
+ form_params=callback_data.form_params,
+ entity_id=str(entity_item.id),
+ ).pack(),
+ )
+ )
+
if edit_delete_row:
keyboard_builder.row(*edit_delete_row)
- entity_caption = get_callable_str(entity_descriptor.caption, entity_descriptor, entity_item)
- entity_item_name = get_local_text(entity_item.name, user.lang) if entity_descriptor.fields_descriptors["name"].localizable else entity_item.name
+ if form.item_repr:
+ item_text = form.item_repr(entity_descriptor, entity_item)
+ else:
+ entity_caption = (
+ get_callable_str(
+ entity_descriptor.full_name, entity_descriptor, entity_item
+ )
+ if entity_descriptor.full_name
+ else entity_descriptor.name
+ )
- item_text = f"{entity_caption or entity_descriptor.name}: {entity_item_name}"
+ entity_item_repr = (
+ get_callable_str(
+ entity_descriptor.item_repr, entity_descriptor, entity_item
+ )
+ if entity_descriptor.item_repr
+ else str(entity_item.id)
+ )
+
+ item_text = f"{entity_caption or entity_descriptor.name}: {entity_item_repr}"
+
+ for field_descriptor in entity_descriptor.fields_descriptors.values():
+ if field_descriptor.is_visible:
+ field_caption = get_callable_str(
+ field_descriptor.caption, field_descriptor, entity_item
+ )
+ value = get_value_repr(
+ value=getattr(entity_item, field_descriptor.name),
+ field_descriptor=field_descriptor,
+ locale=user.lang,
+ )
+ item_text += f"\n{field_caption or field_descriptor.name}:{f' {value}' if value else ''}"
- for field_descriptor in entity_descriptor.fields_descriptors.values():
- if field_descriptor.name == "name" or not field_descriptor.is_visible:
- continue
- field_caption = get_callable_str(field_descriptor.caption, field_descriptor, entity_item)
- value = get_value_repr(value = getattr(entity_item, field_descriptor.name),
- field_descriptor = field_descriptor,
- locale = user.lang)
- item_text += f"\n{field_caption or field_descriptor.name}:{f" {value}" if value else ""}"
-
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
- callback_data = context.pack()))
-
+ text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
+ callback_data=context.pack(),
+ )
+ )
+
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
await state.set_data(state_data)
send_message = get_send_message(query)
- await send_message(text = item_text, reply_markup = keyboard_builder.as_markup())
-
-
-@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_DELETE))
-async def entity_delete_callback(query: CallbackQuery, **kwargs):
-
- callback_data: ContextData = kwargs["callback_data"]
- user: UserBase = kwargs["user"]
- db_session: AsyncSession = kwargs["db_session"]
- app: "QBotApp" = kwargs["app"]
-
- entity_descriptor = get_entity_descriptor(app = app, callback_data = callback_data)
- user_permissions = get_user_permissions(user, entity_descriptor)
-
- entity = await entity_descriptor.type_.get(session = db_session, id = int(callback_data.entity_id))
-
- if not (EntityPermission.DELETE_ALL in user_permissions or
- (EntityPermission.DELETE in user_permissions and not issubclass(entity_descriptor.type_, OwnedBotEntity)) or
- (EntityPermission.DELETE in user_permissions and issubclass(entity_descriptor.type_, OwnedBotEntity) and
- entity.user_id == user.id)):
-
- return await query.answer(text = (await Settings.get(Settings.APP_STRINGS_FORBIDDEN)))
-
- if callback_data.data == "yes":
-
- await entity_descriptor.type_.remove(
- session = db_session, id = int(callback_data.entity_id), commit = True)
-
- await route_callback(message = query, **kwargs)
-
- elif callback_data.data == "no":
- await route_callback(message = query, back = False, **kwargs)
-
- elif not callback_data.data:
-
- field_descriptor = entity_descriptor.fields_descriptors["name"]
-
- entity = await entity_descriptor.type_.get(session = db_session, id = int(callback_data.entity_id))
-
- return await query.message.edit_text(
- text = (await Settings.get(Settings.APP_STRINGS_CONFIRM_DELETE_P_NAME)).format(
- name = get_value_repr(
- value = getattr(entity, field_descriptor.name),
- field_descriptor = field_descriptor,
- locale = user.lang)),
- reply_markup = InlineKeyboardBuilder().row(
- InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_YES_BTN)),
- callback_data = ContextData(
- command = CallbackCommand.ENTITY_DELETE,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- data = "yes").pack()),
- InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_NO_BTN)),
- callback_data = ContextData(
- command = CallbackCommand.ENTITY_ITEM,
- entity_name = callback_data.entity_name,
- entity_id = callback_data.entity_id,
- data = "no").pack())).as_markup())
-
-
-
-from ..navigation import pop_navigation_context, save_navigation_context, clear_state, route_callback
+ await send_message(text=item_text, reply_markup=keyboard_builder.as_markup())
diff --git a/bot/handlers/forms/entity_form_callbacks.py b/bot/handlers/forms/entity_form_callbacks.py
new file mode 100644
index 0000000..66c32a9
--- /dev/null
+++ b/bot/handlers/forms/entity_form_callbacks.py
@@ -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(),
+ )
diff --git a/bot/handlers/forms/entity_list.py b/bot/handlers/forms/entity_list.py
index 0588011..f4196f1 100644
--- a/bot/handlers/forms/entity_list.py
+++ b/bot/handlers/forms/entity_list.py
@@ -1,26 +1,29 @@
-from typing import get_args, get_origin, TYPE_CHECKING
+from typing import TYPE_CHECKING
from aiogram import Router, F
-from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import StatesGroup, State
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
-from aiogram.utils.i18n import I18n
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
from ....model.bot_entity import BotEntity
-from ....model.bot_enum import BotEnum
-from ....model.owned_bot_entity import OwnedBotEntity
from ....model.settings import Settings
from ....model.user import UserBase
from ....model.view_setting import ViewSetting
-from ....model.descriptors import EntityFieldDescriptor, EntityDescriptor
+from ....model.descriptors import EntityDescriptor, Filter
from ....model import EntityPermission
-from ....utils import serialize, deserialize, get_user_permissions
+from ....utils.main import (
+ get_user_permissions,
+ get_send_message,
+ clear_state,
+ get_entity_descriptor,
+ get_callable_str,
+)
+from ....utils.serialization import deserialize
from ..context import ContextData, CallbackCommand, CommandContext
-from ..common import (add_pagination_controls, get_local_text, get_entity_descriptor,
- get_callable_str, get_send_message, add_filter_controls)
+from ..common.pagination import add_pagination_controls
+from ..common.filtering import add_filter_controls
+from ..navigation import pop_navigation_context, save_navigation_context
if TYPE_CHECKING:
from ....main import QBotApp
@@ -32,135 +35,226 @@ router = Router()
@router.callback_query(ContextData.filter(F.command == CallbackCommand.ENTITY_LIST))
async def entity_list_callback(query: CallbackQuery, **kwargs):
-
callback_data: ContextData = kwargs["callback_data"]
if callback_data.data == "skip":
return
-
+
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
- clear_state(state_data = state_data)
- stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
-
- await entity_list(message = query, navigation_stack = stack, **kwargs)
+ clear_state(state_data=state_data)
+ stack = save_navigation_context(callback_data=callback_data, state_data=state_data)
+
+ await entity_list(message=query, navigation_stack=stack, **kwargs)
+
def calc_total_pages(items_count: int, page_size: int) -> int:
- return max(items_count // page_size + (1 if items_count % page_size else 0), 1)
+ return max(items_count // page_size + (1 if items_count % page_size else 0), 1)
-async def entity_list(message: CallbackQuery | Message,
- callback_data: ContextData,
- db_session: AsyncSession,
- user: UserBase,
- app: "QBotApp",
- navigation_stack: list[ContextData],
- **kwargs):
-
+
+async def _prepare_static_filter(
+ db_session: AsyncSession,
+ entity_descriptor: EntityDescriptor,
+ static_filters: list[Filter],
+ params: list[str],
+) -> list[Filter]:
+ return (
+ [
+ Filter(
+ field_name=f.field_name,
+ operator=f.operator,
+ value_type="const",
+ value=(
+ f.value
+ if f.value_type == "const"
+ else await deserialize(
+ session=db_session,
+ type_=entity_descriptor.fields_descriptors[
+ f.field_name
+ ].type_base,
+ value=params[f.param_index],
+ )
+ ),
+ )
+ for f in static_filters
+ ]
+ if static_filters
+ else None
+ )
+
+
+async def entity_list(
+ message: CallbackQuery | Message,
+ callback_data: ContextData,
+ db_session: AsyncSession,
+ user: UserBase,
+ app: "QBotApp",
+ navigation_stack: list[ContextData],
+ **kwargs,
+):
page = int(callback_data.data or "1")
entity_descriptor = get_entity_descriptor(app, callback_data)
user_permissions = get_user_permissions(user, entity_descriptor)
entity_type = entity_descriptor.type_
+ form_params = (
+ callback_data.form_params.split("&") if callback_data.form_params else []
+ )
+ form_name = form_params.pop(0) if form_params else "default"
+ form_list = entity_descriptor.lists.get(
+ form_name or "default", entity_descriptor.default_list
+ )
+ form_item = entity_descriptor.forms.get(
+ form_list.item_form or "default", entity_descriptor.default_form
+ )
keyboard_builder = InlineKeyboardBuilder()
- if EntityPermission.CREATE in user_permissions or EntityPermission.CREATE_ALL in user_permissions:
+ if (
+ EntityPermission.CREATE in user_permissions
+ or EntityPermission.CREATE_ALL in user_permissions
+ ) and form_list.show_add_new_button:
keyboard_builder.row(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_ADD_BTN)),
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR,
- context = CommandContext.ENTITY_CREATE,
- entity_name = entity_descriptor.name,
- field_name = entity_descriptor.field_sequence[0],
- save_state = True).pack()))
+ text=(await Settings.get(Settings.APP_STRINGS_ADD_BTN)),
+ callback_data=ContextData(
+ command=CallbackCommand.FIELD_EDITOR,
+ context=CommandContext.ENTITY_CREATE,
+ entity_name=entity_descriptor.name,
+ field_name=form_item.edit_field_sequence[0],
+ form_params=form_list.item_form,
+ ).pack(),
+ )
+ )
- page_size = await Settings.get(Settings.PAGE_SIZE)
-
- entity_filter = await ViewSetting.get_filter(session = db_session, user_id = user.id, entity_name = entity_descriptor.class_name)
-
- if issubclass(entity_type, OwnedBotEntity):
- if EntityPermission.READ_ALL in user_permissions or EntityPermission.LIST_ALL in user_permissions:
- items_count = await entity_type.get_count(session = db_session, filter = entity_filter)
- total_pages = calc_total_pages(items_count, page_size)
- page = min(page, total_pages)
- items = await entity_type.get_multi(
- session = db_session, order_by = entity_type.name, filter = entity_filter,
- skip = page_size * (page - 1), limit = page_size)
- elif EntityPermission.READ in user_permissions or EntityPermission.LIST in user_permissions:
- items_count = await entity_type.get_count_by_user(session = db_session, user_id = user.id, filter = entity_filter)
- total_pages = calc_total_pages(items_count, page_size)
- page = min(page, total_pages)
- items = await entity_type.get_multi_by_user(
- session = db_session, user_id = user.id, order_by = entity_type.name, filter = entity_filter,
- skip = page_size * (page - 1), limit = page_size)
- else:
- items = list[OwnedBotEntity]()
- items_count = 0
- total_pages = 1
- page = 1
- elif issubclass(entity_type, BotEntity):
- if (EntityPermission.READ in user_permissions or EntityPermission.LIST in user_permissions or
- EntityPermission.READ_ALL in user_permissions or EntityPermission.LIST_ALL in user_permissions):
- items_count = await entity_type.get_count(session = db_session, filter = entity_filter)
- total_pages = calc_total_pages(items_count, page_size)
- page = min(page, total_pages)
- items = await entity_type.get_multi(
- session = db_session, order_by = entity_type.name, filter = entity_filter,
- skip = page_size * (page - 1), limit = page_size)
-
- else:
- items = list[BotEntity]()
- total_pages = 1
- page = 1
- items_count = 0
+ if form_list.filtering:
+ entity_filter = await ViewSetting.get_filter(
+ session=db_session,
+ user_id=user.id,
+ entity_name=entity_descriptor.class_name,
+ )
else:
- raise ValueError(f"Unsupported entity type: {entity_type}")
-
-
+ entity_filter = None
+
+ list_all = (
+ EntityPermission.LIST_ALL in user_permissions
+ or EntityPermission.READ_ALL in user_permissions
+ )
+ if (
+ list_all
+ or EntityPermission.LIST in user_permissions
+ or EntityPermission.READ in user_permissions
+ ):
+ if form_list.pagination:
+ page_size = await Settings.get(Settings.PAGE_SIZE)
+ items_count = await entity_type.get_count(
+ session=db_session,
+ static_filter=await _prepare_static_filter(
+ db_session=db_session,
+ entity_descriptor=entity_descriptor,
+ static_filters=form_list.static_filters,
+ params=form_params,
+ ),
+ filter=entity_filter,
+ filter_fields=form_list.filtering_fields,
+ user=user if not list_all else None,
+ )
+ total_pages = calc_total_pages(items_count, page_size)
+ page = min(page, total_pages)
+ skip = page_size * (page - 1)
+ limit = page_size
+ else:
+ skip = 0
+ limit = None
+
+ items = await entity_type.get_multi(
+ session=db_session,
+ order_by=form_list.order_by,
+ static_filter=await _prepare_static_filter(
+ db_session=db_session,
+ entity_descriptor=entity_descriptor,
+ static_filters=form_list.static_filters,
+ params=form_params,
+ ),
+ filter=entity_filter,
+ filter_fields=form_list.filtering_fields,
+ user=user if not list_all else None,
+ skip=skip,
+ limit=limit,
+ )
+ else:
+ items = list[BotEntity]()
+ items_count = 0
+ total_pages = 1
+ page = 1
for item in items:
- if entity_descriptor.item_caption:
- caption = entity_descriptor.item_caption(entity_descriptor, item)
- elif entity_descriptor.fields_descriptors["name"].localizable:
- caption = get_local_text(item.name, user.lang)
+ if form_list.item_repr:
+ caption = form_list.item_repr(entity_descriptor, item)
+ elif entity_descriptor.item_repr:
+ caption = entity_descriptor.item_repr(entity_descriptor, item)
+ elif entity_descriptor.full_name:
+ caption = f"{
+ get_callable_str(
+ callable_str=entity_descriptor.full_name,
+ descriptor=entity_descriptor,
+ entity=item,
+ )
+ }: {item.id}"
else:
- caption = item.name
+ caption = f"{entity_descriptor.name}: {item.id}"
+
keyboard_builder.row(
InlineKeyboardButton(
- text = caption,
- callback_data = ContextData(
- command = CallbackCommand.ENTITY_ITEM,
- entity_name = entity_descriptor.name,
- entity_id = str(item.id)).pack()))
-
- add_pagination_controls(keyboard_builder = keyboard_builder,
- callback_data = callback_data,
- total_pages = total_pages,
- command = CallbackCommand.ENTITY_LIST,
- page = page)
-
- add_filter_controls(keyboard_builder = keyboard_builder,
- entity_descriptor = entity_descriptor,
- filter = entity_filter)
-
+ text=caption,
+ callback_data=ContextData(
+ command=CallbackCommand.ENTITY_ITEM,
+ entity_name=entity_descriptor.name,
+ form_params=form_list.item_form,
+ entity_id=str(item.id),
+ ).pack(),
+ )
+ )
+
+ if form_list.pagination:
+ add_pagination_controls(
+ keyboard_builder=keyboard_builder,
+ callback_data=callback_data,
+ total_pages=total_pages,
+ command=CallbackCommand.ENTITY_LIST,
+ page=page,
+ )
+
+ if form_list.filtering and form_list.filtering_fields:
+ add_filter_controls(
+ keyboard_builder=keyboard_builder,
+ entity_descriptor=entity_descriptor,
+ filter=entity_filter,
+ )
+
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
- callback_data = context.pack()))
-
- if entity_descriptor.caption:
- entity_text = get_callable_str(entity_descriptor.caption_plural, entity_descriptor)
+ text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
+ callback_data=context.pack(),
+ )
+ )
+
+ if form_list.caption:
+ entity_text = get_callable_str(form_list.caption, entity_descriptor)
else:
- entity_text = entity_descriptor.name
- if entity_descriptor.description:
- entity_desciption = get_callable_str(entity_descriptor.description, entity_descriptor)
- else:
- entity_desciption = None
+ if entity_descriptor.full_name_plural:
+ entity_text = get_callable_str(
+ entity_descriptor.full_name_plural, entity_descriptor
+ )
+ else:
+ entity_text = entity_descriptor.name
+
+ if entity_descriptor.description:
+ entity_text = f"{entity_text} {get_callable_str(entity_descriptor.description, entity_descriptor)}"
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
@@ -168,8 +262,4 @@ async def entity_list(message: CallbackQuery | Message,
send_message = get_send_message(message)
- await send_message(text = f"{entity_text}{f"\n{entity_desciption}" if entity_desciption else ""}",
- reply_markup = keyboard_builder.as_markup())
-
-
-from ..navigation import pop_navigation_context, save_navigation_context, clear_state
+ await send_message(text=entity_text, reply_markup=keyboard_builder.as_markup())
diff --git a/bot/handlers/menu/entities.py b/bot/handlers/menu/entities.py
index 728d77b..7f9939a 100644
--- a/bot/handlers/menu/entities.py
+++ b/bot/handlers/menu/entities.py
@@ -4,13 +4,12 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from babel.support import LazyProxy
from logging import getLogger
-from sqlmodel.ext.asyncio.session import AsyncSession
from typing import TYPE_CHECKING
from ....model.settings import Settings
-from ....model.user import UserBase
from ..context import ContextData, CallbackCommand
-from ..common import get_send_message
+from ....utils.main import get_send_message
from ....model.descriptors import EntityCaptionCallable
+from ..navigation import save_navigation_context, pop_navigation_context
if TYPE_CHECKING:
from ....main import QBotApp
@@ -20,55 +19,63 @@ logger = getLogger(__name__)
router = Router()
-@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_ENTITIES))
+@router.callback_query(
+ ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_ENTITIES)
+)
async def menu_entry_entities(message: CallbackQuery, **kwargs):
-
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
- stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
+ stack = save_navigation_context(callback_data=callback_data, state_data=state_data)
- await entities_menu(message = message, navigation_stack = stack, **kwargs)
+ await entities_menu(message=message, navigation_stack=stack, **kwargs)
-async def entities_menu(message: Message | CallbackQuery,
- app: "QBotApp",
- state: FSMContext,
- navigation_stack: list[ContextData],
- **kwargs):
-
+async def entities_menu(
+ message: Message | CallbackQuery,
+ app: "QBotApp",
+ state: FSMContext,
+ navigation_stack: list[ContextData],
+ **kwargs,
+):
keyboard_builder = InlineKeyboardBuilder()
entity_metadata = app.entity_metadata
for entity in entity_metadata.entity_descriptors.values():
- if entity.caption_plural.__class__ == EntityCaptionCallable:
- caption = entity.caption_plural(entity) or entity.name
- elif entity.caption_plural.__class__ == LazyProxy:
- caption = f"{f"{entity.icon} " if entity.icon else ""}{entity.caption_plural.value or entity.name}"
+ if entity.full_name_plural.__class__ == EntityCaptionCallable:
+ caption = entity.full_name_plural(entity) or entity.name
+ elif entity.full_name_plural.__class__ == LazyProxy:
+ caption = f"{f'{entity.icon} ' if entity.icon else ''}{entity.full_name_plural.value or entity.name}"
else:
- caption = f"{f"{entity.icon} " if entity.icon else ""}{entity.caption_plural or entity.name}"
-
+ caption = f"{f'{entity.icon} ' if entity.icon else ''}{entity.full_name_plural or entity.name}"
+
keyboard_builder.row(
InlineKeyboardButton(
- text = caption,
- callback_data = ContextData(command = CallbackCommand.ENTITY_LIST, entity_name = entity.name).pack()))
-
+ text=caption,
+ callback_data=ContextData(
+ command=CallbackCommand.ENTITY_LIST, entity_name=entity.name
+ ).pack(),
+ )
+ )
+
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
- callback_data = context.pack()))
-
+ text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
+ callback_data=context.pack(),
+ )
+ )
+
state_data = kwargs["state_data"]
await state.set_data(state_data)
-
+
send_message = get_send_message(message)
- await send_message(text = (await Settings.get(Settings.APP_STRINGS_REFERENCES)), reply_markup = keyboard_builder.as_markup())
-
-
-from ..navigation import save_navigation_context, pop_navigation_context
+ await send_message(
+ text=(await Settings.get(Settings.APP_STRINGS_REFERENCES)),
+ reply_markup=keyboard_builder.as_markup(),
+ )
diff --git a/bot/handlers/menu/language.py b/bot/handlers/menu/language.py
index 18061dc..bdfc63a 100644
--- a/bot/handlers/menu/language.py
+++ b/bot/handlers/menu/language.py
@@ -1,64 +1,85 @@
from aiogram import Router, F
-from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
+from aiogram.types import (
+ Message,
+ CallbackQuery,
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
+)
from aiogram.fsm.context import FSMContext
from aiogram.utils.i18n import I18n
from logging import getLogger
from sqlmodel.ext.asyncio.session import AsyncSession
+from ..navigation import pop_navigation_context, save_navigation_context
from ....model.language import LanguageBase
from ....model.settings import Settings
from ....model.user import UserBase
from ..context import ContextData, CallbackCommand
-from ..navigation import route_callback
-from ..common import get_send_message
+from ..common.routing import route_callback
+from ....utils.main import get_send_message
logger = getLogger(__name__)
router = Router()
-@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_LANGUAGE))
+@router.callback_query(
+ ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_LANGUAGE)
+)
async def menu_entry_language(message: CallbackQuery, **kwargs):
-
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
- stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
+ stack = save_navigation_context(callback_data=callback_data, state_data=state_data)
- await language_menu(message, navigation_stack = stack, **kwargs)
-
+ await language_menu(message, navigation_stack=stack, **kwargs)
-async def language_menu(message: Message | CallbackQuery,
- navigation_stack: list[ContextData],
- user: UserBase,
- **kwargs):
+async def language_menu(
+ message: Message | CallbackQuery,
+ navigation_stack: list[ContextData],
+ user: UserBase,
+ **kwargs,
+):
send_message = get_send_message(message)
inline_keyboard = [
- [InlineKeyboardButton(text = locale.localized(user.lang),
- callback_data = ContextData(command = CallbackCommand.SET_LANGUAGE,
- data = str(locale)).pack())]
- for locale in LanguageBase.all_members.values()]
-
+ [
+ InlineKeyboardButton(
+ text=locale.localized(user.lang),
+ callback_data=ContextData(
+ command=CallbackCommand.SET_LANGUAGE, data=str(locale)
+ ).pack(),
+ )
+ ]
+ for locale in LanguageBase.all_members.values()
+ ]
+
context = pop_navigation_context(navigation_stack)
if context:
- inline_keyboard.append([InlineKeyboardButton(text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
- callback_data = context.pack())])
-
+ inline_keyboard.append(
+ [
+ InlineKeyboardButton(
+ text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
+ callback_data=context.pack(),
+ )
+ ]
+ )
+
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
await state.set_data(state_data)
-
- await send_message(text = (await Settings.get(Settings.APP_STRINGS_LANGUAGE)),
- reply_markup = InlineKeyboardMarkup(inline_keyboard = inline_keyboard))
+
+ await send_message(
+ text=(await Settings.get(Settings.APP_STRINGS_LANGUAGE)),
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=inline_keyboard),
+ )
@router.callback_query(ContextData.filter(F.command == CallbackCommand.SET_LANGUAGE))
async def set_language(message: CallbackQuery, **kwargs):
-
user: UserBase = kwargs["user"]
callback_data: ContextData = kwargs["callback_data"]
db_session: AsyncSession = kwargs["db_session"]
@@ -73,6 +94,3 @@ async def set_language(message: CallbackQuery, **kwargs):
i18n: I18n = kwargs["i18n"]
with i18n.use_locale(user.lang):
await route_callback(message, **kwargs)
-
-
-from ..navigation import pop_navigation_context, save_navigation_context
\ No newline at end of file
diff --git a/bot/handlers/menu/main.py b/bot/handlers/menu/main.py
index 1f3e973..5ef2028 100644
--- a/bot/handlers/menu/main.py
+++ b/bot/handlers/menu/main.py
@@ -1,93 +1,88 @@
from aiogram import Router, F
-from aiogram.filters import Command
-from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
-from sqlmodel.ext.asyncio.session import AsyncSession
from ....model.settings import Settings
-from ....model.user import UserBase
from ..context import ContextData, CallbackCommand
-from ..common import get_send_message
+from ....utils.main import get_send_message
+from ..navigation import save_navigation_context, pop_navigation_context
+
+import qbot.bot.handlers.menu.entities as entities
+import qbot.bot.handlers.menu.settings as settings
+import qbot.bot.handlers.menu.parameters as parameters
+import qbot.bot.handlers.menu.language as language
+import qbot.bot.handlers.editors.main as editor
+import qbot.bot.handlers.editors.main_callbacks as editor_callbacks
+import qbot.bot.handlers.forms.entity_list as entity_list
+import qbot.bot.handlers.forms.entity_form as entity_form
+import qbot.bot.handlers.forms.entity_form_callbacks as entity_form_callbacks
+import qbot.bot.handlers.common.filtering_callbacks as filtering_callbacks
+import qbot.bot.handlers.user_handlers as user_handlers
logger = getLogger(__name__)
router = Router()
-# @router.message(Command("menu"))
-# async def command_menu(message: Message, **kwargs):
-
-# await clear_state(state = kwargs["state"], clear_nav = True)
-# callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_MAIN)
-# stack = await save_navigation_context(callback_data = callback_data, state = kwargs["state"])
-# kwargs.update({"navigation_stack": stack, "callback_data": callback_data})
-
-# await main_menu(message, **kwargs)
-
-
-# @router.callback_query(CallbackData.filter(F.command == CallbackCommand.MENU_ENTRY))
-# async def menu_entry(query: CallbackQuery, callback_data: CallbackData, user: UserBase, db_session: AsyncSession, app: QBotApp):
-
-# pass
-
-
@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_MAIN))
-async def menu_entry_main(message: CallbackQuery, **kwargs):
+async def menu_entry_main(message: CallbackQuery, **kwargs):
+ stack = await save_navigation_context(
+ callback_data=kwargs["callback_data"], state=kwargs["state"]
+ )
- stack = await save_navigation_context(callback_data = kwargs["callback_data"], state = kwargs["state"])
-
- await main_menu(message, navigation_stack = stack, **kwargs)
+ await main_menu(message, navigation_stack=stack, **kwargs)
-async def main_menu(message: Message | CallbackQuery, navigation_stack: list[ContextData], **kwargs):
-
+async def main_menu(
+ message: Message | CallbackQuery, navigation_stack: list[ContextData], **kwargs
+):
keyboard_builder = InlineKeyboardBuilder()
-
- keyboard_builder.row(
- InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_REFERENCES_BTN)),
- callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_ENTITIES).pack()))
-
keyboard_builder.row(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_SETTINGS_BTN)),
- callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_SETTINGS).pack()))
-
+ text=(await Settings.get(Settings.APP_STRINGS_REFERENCES_BTN)),
+ callback_data=ContextData(
+ command=CallbackCommand.MENU_ENTRY_ENTITIES
+ ).pack(),
+ )
+ )
+
+ keyboard_builder.row(
+ InlineKeyboardButton(
+ text=(await Settings.get(Settings.APP_STRINGS_SETTINGS_BTN)),
+ callback_data=ContextData(
+ command=CallbackCommand.MENU_ENTRY_SETTINGS
+ ).pack(),
+ )
+ )
+
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
- callback_data = context.pack()))
-
+ text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
+ callback_data=context.pack(),
+ )
+ )
+
send_message = get_send_message(message)
- await send_message(text = (await Settings.get(Settings.APP_STRINGS_MAIN_NENU)),
- reply_markup = keyboard_builder.as_markup())
+ await send_message(
+ text=(await Settings.get(Settings.APP_STRINGS_MAIN_NENU)),
+ reply_markup=keyboard_builder.as_markup(),
+ )
-from .entities import router as entities_router
-from .settings import router as settings_router
-from .parameters import router as parameters_router
-from .language import router as language_router
-from ..editors import router as editors_router
-from ..forms.entity_list import router as entity_list_router
-from ..forms.entity_form import router as entity_form_router
-from ..common import router as common_router
-from ..user_handlers import router as user_handlers_router
-
router.include_routers(
- entities_router,
- settings_router,
- parameters_router,
- language_router,
- editors_router,
- entity_list_router,
- entity_form_router,
- common_router,
- user_handlers_router
+ entities.router,
+ settings.router,
+ parameters.router,
+ language.router,
+ editor.router,
+ editor_callbacks.router,
+ entity_list.router,
+ entity_form.router,
+ entity_form_callbacks.router,
+ filtering_callbacks.router,
+ user_handlers.router,
)
-
-from ..navigation import save_navigation_context, pop_navigation_context, clear_state
\ No newline at end of file
diff --git a/bot/handlers/menu/parameters.py b/bot/handlers/menu/parameters.py
index c5d77de..f19453e 100644
--- a/bot/handlers/menu/parameters.py
+++ b/bot/handlers/menu/parameters.py
@@ -1,79 +1,94 @@
from aiogram import Router, F
-from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import StatesGroup, State
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
-from aiogram.utils.i18n import I18n
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
-from sqlmodel.ext.asyncio.session import AsyncSession
from ....model.settings import Settings
from ....model.user import UserBase
from ..context import ContextData, CallbackCommand, CommandContext
-
+from ....utils.main import (
+ get_send_message,
+ clear_state,
+ get_value_repr,
+ get_callable_str,
+)
from ..navigation import save_navigation_context, pop_navigation_context
+from ....auth import authorize_command
logger = getLogger(__name__)
router = Router()
-@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_PARAMETERS))
+@router.callback_query(
+ ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_PARAMETERS)
+)
async def menu_entry_parameters(message: CallbackQuery, **kwargs):
-
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
- clear_state(state_data = state_data)
- stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
+ clear_state(state_data=state_data)
+ stack = save_navigation_context(callback_data=callback_data, state_data=state_data)
- await parameters_menu(message = message, navigation_stack = stack, **kwargs)
+ await parameters_menu(message=message, navigation_stack=stack, **kwargs)
-async def parameters_menu(message: Message | CallbackQuery,
- user: UserBase,
- callback_data: ContextData,
- navigation_stack: list[ContextData],
- **kwargs):
-
- if not await authorize_command(user = user, callback_data = callback_data):
+async def parameters_menu(
+ message: Message | CallbackQuery,
+ user: UserBase,
+ callback_data: ContextData,
+ navigation_stack: list[ContextData],
+ **kwargs,
+):
+ if not await authorize_command(user=user, callback_data=callback_data):
await message.answer(await Settings.get(Settings.APP_STRINGS_FORBIDDEN))
settings = await Settings.get_params()
-
+
keyboard_builder = InlineKeyboardBuilder()
for key, value in settings.items():
-
if not key.is_visible:
continue
if key.caption_value:
- caption = get_callable_str(callable_str = key.caption_value, descriptor = key, entity = None, value = value)
+ caption = get_callable_str(
+ callable_str=key.caption_value, descriptor=key, entity=None, value=value
+ )
else:
if key.caption:
- caption = get_callable_str(callable_str = key.caption, descriptor = key, entity = None, value = value)
+ caption = get_callable_str(
+ callable_str=key.caption, descriptor=key, entity=None, value=value
+ )
else:
- caption = key.name
-
- if key.type_ == bool:
- caption = f"{"【✔︎】" if value else "【 】"} {caption}"
- else:
- caption = f"{caption}: {get_value_repr(value = value, field_descriptor = key, locale = user.lang)}"
-
+ caption = key.name
+
+ if key.type_ is bool:
+ caption = f"{'【✔︎】' if value else '【 】'} {caption}"
+ else:
+ caption = f"{caption}: {get_value_repr(value=value, field_descriptor=key, locale=user.lang)}"
+
+ keyboard_builder.row(
+ InlineKeyboardButton(
+ text=caption,
+ callback_data=ContextData(
+ command=CallbackCommand.FIELD_EDITOR,
+ context=CommandContext.SETTING_EDIT,
+ field_name=key.name,
+ ).pack(),
+ )
+ )
- keyboard_builder.row(InlineKeyboardButton(text = caption,
- callback_data = ContextData(
- command = CallbackCommand.FIELD_EDITOR,
- context = CommandContext.SETTING_EDIT,
- field_name = key.name).pack()))
-
context = pop_navigation_context(navigation_stack)
if context:
- keyboard_builder.row(InlineKeyboardButton(text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
- callback_data = context.pack()))
+ keyboard_builder.row(
+ InlineKeyboardButton(
+ text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
+ callback_data=context.pack(),
+ )
+ )
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
@@ -81,8 +96,7 @@ async def parameters_menu(message: Message | CallbackQuery,
send_message = get_send_message(message)
- await send_message(text = (await Settings.get(Settings.APP_STRINGS_PARAMETERS)), reply_markup = keyboard_builder.as_markup())
-
-
-from ..navigation import pop_navigation_context, get_navigation_context, clear_state
-from ..common import get_send_message, get_value_repr, get_callable_str, authorize_command
\ No newline at end of file
+ await send_message(
+ text=(await Settings.get(Settings.APP_STRINGS_PARAMETERS)),
+ reply_markup=keyboard_builder.as_markup(),
+ )
diff --git a/bot/handlers/menu/settings.py b/bot/handlers/menu/settings.py
index 3971698..96a445f 100644
--- a/bot/handlers/menu/settings.py
+++ b/bot/handlers/menu/settings.py
@@ -1,68 +1,80 @@
from aiogram import Router, F
-from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from logging import getLogger
-from sqlmodel.ext.asyncio.session import AsyncSession
from ....model.settings import Settings
from ....model.user import UserBase
+from ....utils.main import get_send_message
from ..context import ContextData, CallbackCommand
-from ..common import get_send_message, authorize_command
+from ....auth import authorize_command
from ..navigation import save_navigation_context, pop_navigation_context
logger = getLogger(__name__)
router = Router()
-@router.callback_query(ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_SETTINGS))
+@router.callback_query(
+ ContextData.filter(F.command == CallbackCommand.MENU_ENTRY_SETTINGS)
+)
async def menu_entry_settings(message: CallbackQuery, **kwargs):
-
callback_data: ContextData = kwargs["callback_data"]
state: FSMContext = kwargs["state"]
state_data = await state.get_data()
kwargs["state_data"] = state_data
- stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
+ stack = save_navigation_context(callback_data=callback_data, state_data=state_data)
- await settings_menu(message, navigation_stack = stack, **kwargs)
+ await settings_menu(message, navigation_stack=stack, **kwargs)
-async def settings_menu(message: Message | CallbackQuery,
- user: UserBase,
- navigation_stack: list[ContextData],
- **kwargs):
-
+async def settings_menu(
+ message: Message | CallbackQuery,
+ user: UserBase,
+ navigation_stack: list[ContextData],
+ **kwargs,
+):
keyboard_builder = InlineKeyboardBuilder()
-
- if await authorize_command(user = user, callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_PARAMETERS)):
+
+ if await authorize_command(
+ user=user,
+ callback_data=ContextData(command=CallbackCommand.MENU_ENTRY_PARAMETERS),
+ ):
keyboard_builder.row(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_PARAMETERS_BTN)),
- callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_PARAMETERS).pack()))
-
+ text=(await Settings.get(Settings.APP_STRINGS_PARAMETERS_BTN)),
+ callback_data=ContextData(
+ command=CallbackCommand.MENU_ENTRY_PARAMETERS
+ ).pack(),
+ )
+ )
+
keyboard_builder.row(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_LANGUAGE_BTN)),
- callback_data = ContextData(command = CallbackCommand.MENU_ENTRY_LANGUAGE).pack()))
-
+ text=(await Settings.get(Settings.APP_STRINGS_LANGUAGE_BTN)),
+ callback_data=ContextData(
+ command=CallbackCommand.MENU_ENTRY_LANGUAGE
+ ).pack(),
+ )
+ )
+
context = pop_navigation_context(navigation_stack)
if context:
keyboard_builder.row(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
- callback_data = context.pack()))
-
+ text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
+ callback_data=context.pack(),
+ )
+ )
+
state: FSMContext = kwargs["state"]
state_data = kwargs["state_data"]
await state.set_data(state_data)
-
+
send_message = get_send_message(message)
-
-
- await send_message(text = (await Settings.get(Settings.APP_STRINGS_SETTINGS)), reply_markup = keyboard_builder.as_markup())
-
-
-from ..navigation import pop_navigation_context, get_navigation_context
\ No newline at end of file
+ await send_message(
+ text=(await Settings.get(Settings.APP_STRINGS_SETTINGS)),
+ reply_markup=keyboard_builder.as_markup(),
+ )
diff --git a/bot/handlers/navigation.py b/bot/handlers/navigation.py
index 36193f9..b722a1f 100644
--- a/bot/handlers/navigation.py
+++ b/bot/handlers/navigation.py
@@ -1,21 +1,25 @@
-from aiogram.fsm.context import FSMContext
-from aiogram.types import Message, CallbackQuery
-
from .context import ContextData, CallbackCommand
-def save_navigation_context(callback_data: ContextData, state_data: dict) -> list[ContextData]:
- stack = [ContextData.unpack(item) for item in state_data.get("navigation_stack", [])]
+def save_navigation_context(
+ callback_data: ContextData, state_data: dict
+) -> list[ContextData]:
+ stack = [
+ ContextData.unpack(item) for item in state_data.get("navigation_stack", [])
+ ]
data_nc = state_data.get("navigation_context")
navigation_context = ContextData.unpack(data_nc) if data_nc else None
if callback_data.back:
- callback_data.back = False
+ callback_data.back = False
if stack:
stack.pop()
else:
- if (stack and navigation_context and
- navigation_context.command == callback_data.command and
- navigation_context.command != CallbackCommand.USER_COMMAND):
+ if (
+ stack
+ and navigation_context
+ and navigation_context.command == callback_data.command
+ and navigation_context.command != CallbackCommand.USER_COMMAND
+ ):
navigation_context = callback_data
elif navigation_context:
stack.append(navigation_context)
@@ -31,65 +35,14 @@ def pop_navigation_context(stack: list[ContextData]) -> ContextData | None:
data = stack[-1]
data.back = True
return data
-
-def get_navigation_context(state_data: dict) -> tuple[list[ContextData], ContextData | None]:
+
+def get_navigation_context(
+ state_data: dict,
+) -> tuple[list[ContextData], ContextData | None]:
data_nc = state_data.get("navigation_context")
context = ContextData.unpack(data_nc) if data_nc else None
- return ([ContextData.unpack(item) for item in state_data.get("navigation_stack", [])],
- context)
-
-
-def clear_state(state_data: dict, clear_nav: bool = False):
- if clear_nav:
- state_data.clear()
- else:
- stack = state_data.get("navigation_stack")
- context = state_data.get("navigation_context")
- state_data.clear()
- if stack:
- state_data["navigation_stack"] = stack
- if context:
- state_data["navigation_context"] = context
-
-
-async def route_callback(message: Message | CallbackQuery, back: bool = True, **kwargs):
-
- state_data = kwargs["state_data"]
- stack, context = get_navigation_context(state_data)
- if back:
- context = pop_navigation_context(stack)
- stack = save_navigation_context(callback_data = context, state_data = state_data)
- kwargs.update({"callback_data": context, "navigation_stack": stack})
- if context:
- if context.command == CallbackCommand.MENU_ENTRY_MAIN:
- await main_menu(message, **kwargs)
- elif context.command == CallbackCommand.MENU_ENTRY_SETTINGS:
- await settings_menu(message, **kwargs)
- elif context.command == CallbackCommand.MENU_ENTRY_PARAMETERS:
- await parameters_menu(message, **kwargs)
- elif context.command == CallbackCommand.MENU_ENTRY_LANGUAGE:
- await language_menu(message, **kwargs)
- elif context.command == CallbackCommand.MENU_ENTRY_ENTITIES:
- await entities_menu(message, **kwargs)
- elif context.command == CallbackCommand.ENTITY_LIST:
- await entity_list(message, **kwargs)
- elif context.command == CallbackCommand.ENTITY_ITEM:
- await entity_item(message, **kwargs)
- elif context.command == CallbackCommand.FIELD_EDITOR:
- await field_editor(message, **kwargs)
-
- else:
- raise ValueError(f"Unknown command {context.command}")
- else:
- raise ValueError("No navigation context")
-
-
-from .menu.main import main_menu
-from .menu.settings import settings_menu
-from .menu.parameters import parameters_menu
-from .menu.language import language_menu
-from .menu.entities import entities_menu
-from .forms.entity_list import entity_list
-from .forms.entity_form import entity_item
-from .editors import field_editor
\ No newline at end of file
+ return (
+ [ContextData.unpack(item) for item in state_data.get("navigation_stack", [])],
+ context,
+ )
diff --git a/bot/handlers/start.py b/bot/handlers/start.py
index 8d8464c..a5aa0cd 100644
--- a/bot/handlers/start.py
+++ b/bot/handlers/start.py
@@ -8,7 +8,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
from ...main import QBotApp
from ...model.settings import Settings
from ...model.language import LanguageBase
-from .navigation import clear_state
+from ...utils.main import clear_state
logger = getLogger(__name__)
@@ -16,40 +16,56 @@ router = Router()
@router.message(CommandStart())
-async def start(message: Message, db_session: AsyncSession, app: QBotApp, state: FSMContext):
-
+async def start(
+ message: Message, db_session: AsyncSession, app: QBotApp, state: FSMContext
+):
state_data = await state.get_data()
- clear_state(state_data = state_data, clear_nav = True)
+ clear_state(state_data=state_data, clear_nav=True)
User = app.user_class
- user = await User.get(session = db_session, id = message.from_user.id)
-
+ user = await User.get(session=db_session, id=message.from_user.id)
+
if not user:
- msg_text = (await Settings.get(Settings.APP_STRINGS_WELCOME_P_NAME)).format(name = message.from_user.full_name)
-
+ msg_text = (await Settings.get(Settings.APP_STRINGS_WELCOME_P_NAME)).format(
+ name=message.from_user.full_name
+ )
+
try:
- if message.from_user.language_code in [item.value for item in LanguageBase.all_members.values()]:
+ if message.from_user.language_code in [
+ item.value for item in LanguageBase.all_members.values()
+ ]:
lang = LanguageBase(message.from_user.language_code)
- user = await User.create(session = db_session,
- obj_in = User(
- id = message.from_user.id,
- name = message.from_user.full_name,
- lang = lang,
- is_active = True),
- commit = True)
+
+ user = await User.create(
+ session=db_session,
+ obj_in=User(
+ id=message.from_user.id,
+ name=message.from_user.full_name,
+ lang=lang,
+ is_active=True,
+ ),
+ commit=True,
+ )
+
except Exception as e:
-
- logger.error("Error creating user", exc_info = True)
- message.answer((await Settings.get(Settings.APP_STRINGS_INTERNAL_ERROR_P_ERROR)).format(error = str(e)))
+ logger.error("Error creating user", exc_info=True)
+ message.answer(
+ (
+ await Settings.get(Settings.APP_STRINGS_INTERNAL_ERROR_P_ERROR)
+ ).format(error=str(e))
+ )
return
-
+
else:
if user.is_active:
- msg_text = (await Settings.get(Settings.APP_STRINGS_GREETING_P_NAME)).format(name = user.name)
+ msg_text = (
+ await Settings.get(Settings.APP_STRINGS_GREETING_P_NAME)
+ ).format(name=user.name)
else:
- msg_text = (await Settings.get(Settings.APP_STRINGS_USER_BLOCKED_P_NAME)).format(name = user.name)
-
+ msg_text = (
+ await Settings.get(Settings.APP_STRINGS_USER_BLOCKED_P_NAME)
+ ).format(name=user.name)
+
await message.answer(msg_text)
-
diff --git a/bot/handlers/user_handlers/__init__.py b/bot/handlers/user_handlers/__init__.py
index 38afa98..a6af36f 100644
--- a/bot/handlers/user_handlers/__init__.py
+++ b/bot/handlers/user_handlers/__init__.py
@@ -1,14 +1,16 @@
-from dataclasses import dataclass, field
-from typing import Any, Callable, TYPE_CHECKING
+from typing import TYPE_CHECKING
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
-from aiogram.utils.i18n import I18n
-from aiogram.utils.keyboard import InlineKeyboardBuilder
-from sqlmodel.ext.asyncio.session import AsyncSession
from ..context import ContextData, CallbackCommand
-from ....model.user import UserBase
from ....model.settings import Settings
+from ....utils.main import get_send_message, clear_state
+from ....model.descriptors import CommandCallbackContext
+from ..navigation import (
+ save_navigation_context,
+ get_navigation_context,
+ pop_navigation_context,
+)
if TYPE_CHECKING:
@@ -18,51 +20,22 @@ if TYPE_CHECKING:
router = Router()
-@dataclass(kw_only = True)
-class CommandCallbackContext[UT: UserBase]:
-
- keyboard_builder: InlineKeyboardBuilder = field(default_factory = InlineKeyboardBuilder)
- message_text: str | None = None
- register_navigation: bool = True
- message: Message | CallbackQuery
- callback_data: ContextData
- db_session: AsyncSession
- user: UT
- app: "QBotApp"
- state_data: dict[str, Any]
- state: FSMContext
- i18n: I18n
- kwargs: dict[str, Any] = field(default_factory = dict)
-
-
-@dataclass(kw_only = True)
-class Command:
-
- name: str
- handler: Callable[[CommandCallbackContext], None]
- caption: str | dict[str, str] | None = None
- register_navigation: bool = True
- clear_navigation: bool = False
- clear_state: bool = True
-
@router.message(F.text.startswith("/"))
async def command_text(message: Message, **kwargs):
-
str_command = message.text.lstrip("/")
- callback_data = ContextData(command = CallbackCommand.USER_COMMAND,
- user_command = str_command)
+ callback_data = ContextData(
+ command=CallbackCommand.USER_COMMAND, user_command=str_command
+ )
- await command_handler(message = message, callback_data = callback_data, **kwargs)
+ await command_handler(message=message, callback_data=callback_data, **kwargs)
@router.callback_query(ContextData.filter(F.command == CallbackCommand.USER_COMMAND))
async def command_callback(message: CallbackQuery, **kwargs):
-
- await command_handler(message = message, **kwargs)
+ await command_handler(message=message, **kwargs)
async def command_handler(message: Message | CallbackQuery, **kwargs):
-
callback_data: ContextData = kwargs.pop("callback_data")
str_command = callback_data.user_command
app: "QBotApp" = kwargs.pop("app")
@@ -70,45 +43,48 @@ async def command_handler(message: Message | CallbackQuery, **kwargs):
if not command:
return
-
+
state: FSMContext = kwargs.pop("state")
state_data = await state.get_data()
-
+
if command.register_navigation:
- clear_state(state_data = state_data)
+ clear_state(state_data=state_data)
if command.clear_navigation:
state_data.pop("navigation_stack", None)
state_data.pop("navigation_context", None)
if command.register_navigation:
- stack = save_navigation_context(callback_data = callback_data, state_data = state_data)
-
+ stack = save_navigation_context(
+ callback_data=callback_data, state_data=state_data
+ )
+
callback_context = CommandCallbackContext[app.user_class](
- message = message,
- callback_data = callback_data,
- db_session = kwargs.pop("db_session"),
- user = kwargs.pop("user"),
- app = app,
- state_data = state_data,
- state = state,
- i18n = kwargs.pop("i18n"),
- kwargs = kwargs)
+ message=message,
+ callback_data=callback_data,
+ db_session=kwargs.pop("db_session"),
+ user=kwargs.pop("user"),
+ app=app,
+ state_data=state_data,
+ state=state,
+ i18n=kwargs.pop("i18n"),
+ kwargs=kwargs,
+ )
await command.handler(callback_context)
await state.set_data(state_data)
if command.register_navigation:
-
- stack, navigation_context = get_navigation_context(state_data = state_data)
- back_callback_data = pop_navigation_context(stack = stack)
+ stack, navigation_context = get_navigation_context(state_data=state_data)
+ back_callback_data = pop_navigation_context(stack=stack)
if back_callback_data:
-
callback_context.keyboard_builder.row(
InlineKeyboardButton(
- text = (await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
- callback_data = back_callback_data.pack()))
+ text=(await Settings.get(Settings.APP_STRINGS_BACK_BTN)),
+ callback_data=back_callback_data.pack(),
+ )
+ )
send_message = get_send_message(message)
@@ -116,10 +92,11 @@ async def command_handler(message: Message | CallbackQuery, **kwargs):
message = message.message
if callback_context.message_text:
- await send_message(text = callback_context.message_text,
- reply_markup = callback_context.keyboard_builder.as_markup())
+ await send_message(
+ text=callback_context.message_text,
+ reply_markup=callback_context.keyboard_builder.as_markup(),
+ )
else:
- await message.edit_reply_markup(reply_markup = callback_context.keyboard_builder.as_markup())
-
-from ..common import get_send_message
-from ..navigation import save_navigation_context, get_navigation_context, clear_state, pop_navigation_context
\ No newline at end of file
+ await message.edit_reply_markup(
+ reply_markup=callback_context.keyboard_builder.as_markup()
+ )
diff --git a/config/__init__.py b/config/__init__.py
index 02e86ed..66f2cf0 100644
--- a/config/__init__.py
+++ b/config/__init__.py
@@ -1,4 +1,3 @@
-from babel.support import LazyProxy
from pydantic import computed_field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Literal, Self
@@ -6,11 +5,8 @@ import warnings
class Config(BaseSettings):
-
model_config = SettingsConfigDict(
- env_file = ".env",
- env_ignore_empty = True,
- extra = "ignore"
+ env_file=".env", env_ignore_empty=True, extra="ignore"
)
SECRET_KEY: str = "changethis"
@@ -35,22 +31,24 @@ class Config(BaseSettings):
def API_DOMAIN(self) -> str:
if self.ENVIRONMENT == "local":
return self.DOMAIN
- return f'api.{self.DOMAIN}'
-
- @computed_field
+ return f"api.{self.DOMAIN}"
+
+ @computed_field
@property
def API_URL(self) -> str:
if self.USE_NGROK:
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
TELEGRAM_BOT_TOKEN: str = "changethis"
ADMIN_TELEGRAM_ID: int
- USE_NGROK : bool = False
+ USE_NGROK: bool = False
NGROK_AUTH_TOKEN: str = "changethis"
NGROK_URL: str = ""
@@ -59,7 +57,7 @@ class Config(BaseSettings):
def _check_default_secret(self, var_name: str, value: str | None) -> None:
if value == "changethis":
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."
)
if self.ENVIRONMENT == "local":
@@ -76,5 +74,6 @@ class Config(BaseSettings):
self._check_default_secret("NGROK_AUTH_TOKEN", self.NGROK_AUTH_TOKEN)
return self
-
-config = Config()
\ No newline at end of file
+
+
+config = Config()
diff --git a/db/__init__.py b/db/__init__.py
index aae659a..f2a556a 100644
--- a/db/__init__.py
+++ b/db/__init__.py
@@ -4,15 +4,16 @@ from sqlalchemy.orm import sessionmaker
from ..config import config
-import logging
-logger = logging.getLogger('sqlalchemy.engine')
-logger.setLevel(logging.DEBUG)
+# import logging
+# logger = logging.getLogger('sqlalchemy.engine')
+# logger.setLevel(logging.DEBUG)
async_engine = create_async_engine(config.DATABASE_URI)
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:
- yield session
\ No newline at end of file
+ yield session
diff --git a/fsm/db_storage.py b/fsm/db_storage.py
index b614949..3e90b08 100644
--- a/fsm/db_storage.py
+++ b/fsm/db_storage.py
@@ -1,5 +1,11 @@
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 typing import Any, Dict
import ujson as json
@@ -9,76 +15,70 @@ from ..model.fsm_storage import FSMStorage
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:
key_builder = DefaultKeyBuilder()
self.key_builder = key_builder
-
async def set_state(self, key: StorageKey, state: StateType = None) -> None:
-
db_key = self.key_builder.build(key, "state")
async with async_session() as session:
- db_state = (await session.exec(
- select(FSMStorage).where(FSMStorage.key == db_key))).first()
-
+ db_state = (
+ await session.exec(select(FSMStorage).where(FSMStorage.key == db_key))
+ ).first()
+
if db_state:
if state is None:
await session.delete(db_state)
else:
db_state.value = state.state if isinstance(state, State) else state
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)
else:
return
-
+
await session.commit()
-
async def get_state(self, key: StorageKey) -> str | None:
-
db_key = self.key_builder.build(key, "state")
async with async_session() as session:
- db_state = (await session.exec(
- select(FSMStorage).where(FSMStorage.key == db_key))).first()
+ db_state = (
+ await session.exec(select(FSMStorage).where(FSMStorage.key == db_key))
+ ).first()
return db_state.value if db_state else None
-
async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None:
-
db_key = self.key_builder.build(key, "data")
async with async_session() as session:
- db_data = (await session.exec(
- select(FSMStorage).where(FSMStorage.key == db_key))).first()
-
+ db_data = (
+ await session.exec(select(FSMStorage).where(FSMStorage.key == db_key))
+ ).first()
+
if db_data:
if not data:
await session.delete(db_data)
else:
- db_data.value = json.dumps(data, ensure_ascii = False)
+ db_data.value = json.dumps(data, ensure_ascii=False)
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)
else:
return
-
+
await session.commit()
-
async def get_data(self, key: StorageKey) -> Dict[str, Any]:
-
db_key = self.key_builder.build(key, "data")
async with async_session() as session:
- db_data = (await session.exec(
- select(FSMStorage).where(FSMStorage.key == db_key))).first()
+ db_data = (
+ await session.exec(select(FSMStorage).where(FSMStorage.key == db_key))
+ ).first()
return json.loads(db_data.value) if db_data else {}
-
async def close(self):
return await super().close()
-
diff --git a/lifespan.py b/lifespan.py
index 46950fb..1f73d96 100644
--- a/lifespan.py
+++ b/lifespan.py
@@ -5,57 +5,70 @@ from logging import getLogger
logger = getLogger(__name__)
+
@asynccontextmanager
async def default_lifespan(app: QBotApp):
-
logger.debug("starting qbot app")
-
+
if app.config.USE_NGROK:
try:
from pyngrok import ngrok
from pyngrok.conf import PyngrokConfig
-
+
except ImportError:
logger.error("pyngrok is not installed")
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
commands_captions = dict[str, list[tuple[str, str]]]()
for command_name, command in app.bot_commands.items():
- if isinstance(command.caption, str):
- if "default" not in commands_captions:
- commands_captions["default"] = []
- commands_captions["default"].append((command_name, command.caption))
- for locale, description in command.caption.items():
- if locale not in commands_captions:
- commands_captions[locale] = []
- commands_captions[locale].append((command_name, description))
+ if command.show_in_bot_commands:
+ if isinstance(command.caption, str) or command.caption is None:
+ if "default" not in commands_captions:
+ commands_captions["default"] = []
+ commands_captions["default"].append(
+ (command_name, command.caption or command_name)
+ )
+ 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():
- await app.bot.set_my_commands([BotCommand(command = command[0], description=command[1]) for command in commands],
- language_code = None if locale == "default" else locale)
+ await app.bot.set_my_commands(
+ [
+ BotCommand(command=command[0], description=command[1])
+ for command in commands
+ ],
+ language_code=None if locale == "default" else locale,
+ )
+
+ await app.bot.set_webhook(
+ url=f"{app.config.API_URL}/api/telegram/webhook",
+ drop_pending_updates=True,
+ allowed_updates=["message", "callback_query", "pre_checkout_query"],
+ secret_token=app.bot_auth_token,
+ )
-
- await app.bot.set_webhook(url = f"{app.config.API_URL}/api/telegram/webhook",
- drop_pending_updates = True,
- allowed_updates = ['message', 'callback_query', 'pre_checkout_query'],
- secret_token = app.bot_auth_token)
-
logger.info("qbot app started")
-
+
if app.lifespan:
async with app.lifespan(app):
yield
else:
yield
-
+
logger.info("stopping qbot app")
await app.bot.delete_webhook()
if app.config.USE_NGROK:
ngrok.disconnect(app.config.NGROK_URL)
ngrok.kill()
- logger.info("qbot app stopped")
\ No newline at end of file
+ logger.info("qbot app stopped")
diff --git a/main.py b/main.py
index 18e05c7..ff1bbe4 100644
--- a/main.py
+++ b/main.py
@@ -1,24 +1,21 @@
-from functools import wraps
-from typing import Annotated, Callable, Any, Union, override
+from typing import Annotated, Callable, Any
from typing_extensions import Doc
from aiogram import Bot, Dispatcher
-from aiogram.filters import CommandStart
from aiogram.client.default import DefaultBotProperties
-from aiogram.types import Message, BotCommand
-from aiogram.utils.keyboard import InlineKeyboardBuilder
+from aiogram.types import Message
from aiogram.utils.callback_answer import CallbackAnswerMiddleware
from aiogram.utils.i18n import I18n
from fastapi import FastAPI
from fastapi.applications import Lifespan, AppType
-from logging import getLogger
from secrets import token_hex
from .config import Config
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.entity_metadata import EntityMetadata
-from .bot.handlers.user_handlers import Command, CommandCallbackContext
+from .model.descriptors import BotCommand
+from .router import Router
class QBotApp(FastAPI):
@@ -26,47 +23,67 @@ class QBotApp(FastAPI):
Main class for the QBot application
"""
- def __init__[UserType: UserBase](self,
- user_class: Annotated[type[UserType], Doc(
- "User class that will be used in the application"
- )] | None = None,
- config: Config | None = None,
- bot_start: Annotated[Callable[[Annotated[Callable[[Message, Any], None], Doc(
- "Default handler for the start command"
- )], Message, Any], None], Doc(
- "Handler for the start command"
- )] | None = None,
- bot_commands: list[Command] | None = None,
- lifespan: Lifespan[AppType] | None = None,
- *args,
- **kwargs):
-
+ def __init__[UserType: UserBase](
+ self,
+ user_class: (
+ Annotated[
+ type[UserType], Doc("User class that will be used in the application")
+ ]
+ | None
+ ) = None,
+ config: Config | None = None,
+ bot_start: (
+ Annotated[
+ Callable[
+ [
+ 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:
config = Config()
if user_class is None:
from .model.default_user import DefaultUser
+
user_class = DefaultUser
self.user_class = user_class
self.entity_metadata: EntityMetadata = user_class.entity_metadata
self.config = config
self.lifespan = lifespan
- self.bot = Bot(token = self.config.TELEGRAM_BOT_TOKEN, default = DefaultBotProperties(parse_mode = "HTML"))
-
- dp = Dispatcher(storage = DbStorage())
+ self.bot = Bot(
+ token=self.config.TELEGRAM_BOT_TOKEN,
+ default=DefaultBotProperties(parse_mode="HTML"),
+ )
- i18n = I18n(path = "locales", default_locale = "en", domain = "messages")
- i18n_middleware = I18nMiddleware(user_class = user_class, i18n = i18n)
+ dp = Dispatcher(storage=DbStorage())
+
+ i18n = I18n(path="locales", default_locale="en", domain="messages")
+ i18n_middleware = I18nMiddleware(user_class=user_class, i18n=i18n)
i18n_middleware.setup(dp)
- # dp.callback_query.middleware(ResetStateMiddleware())
dp.callback_query.middleware(CallbackAnswerMiddleware())
from .bot.handlers.start import router as start_router
+
dp.include_router(start_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.callback_query.middleware.register(auth)
dp.include_router(main_menu_router)
@@ -76,35 +93,17 @@ class QBotApp(FastAPI):
self.bot_auth_token = token_hex(128)
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
- super().__init__(lifespan = default_lifespan, *args, **kwargs)
+ super().__init__(lifespan=default_lifespan, *args, **kwargs)
from .api_route.telegram import router as telegram_router
- self.include_router(telegram_router, prefix = "/api/telegram", tags = ["telegram"])
- @override
- def bot_command(self, command: Command): ...
+ self.include_router(telegram_router, prefix="/api/telegram", tags=["telegram"])
- @override
- def bot_command(self, command: str, caption: str | dict[str, str] | None = None): ...
-
- def bot_command(self, command: str | Command, caption: str | dict[str, str] | None = None):
- """
- 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
\ No newline at end of file
+ def register_routers(self, *routers: Router):
+ for router in routers:
+ for command_name, command in router._commands.items():
+ self.bot_commands[command_name] = command
diff --git a/middleware/telegram/__init__.py b/middleware/telegram/__init__.py
index e89704d..978b4d9 100644
--- a/middleware/telegram/__init__.py
+++ b/middleware/telegram/__init__.py
@@ -1,3 +1,2 @@
-from .auth import AuthMiddleware
-from .i18n import I18nMiddleware
-from .reset_state import ResetStateMiddleware
\ No newline at end of file
+from .auth import AuthMiddleware as AuthMiddleware
+from .i18n import I18nMiddleware as I18nMiddleware
diff --git a/middleware/telegram/auth.py b/middleware/telegram/auth.py
index de8413f..89a23fa 100644
--- a/middleware/telegram/auth.py
+++ b/middleware/telegram/auth.py
@@ -1,6 +1,5 @@
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Message, CallbackQuery
-from aiogram.utils.i18n import gettext as _
from babel.support import LazyProxy
from typing import Any, Awaitable, Callable, Dict
@@ -8,33 +7,31 @@ from ...model.user import UserBase
class AuthMiddleware(BaseMiddleware):
-
- def __init__[UserType: UserBase](self,
- user_class: type[UserType],
- not_authenticated_msg: LazyProxy | str = "not authenticated",
- not_active_msg: LazyProxy | str = "not active"):
-
+ def __init__[UserType: UserBase](
+ self,
+ user_class: type[UserType],
+ not_authenticated_msg: LazyProxy | str = "not authenticated",
+ not_active_msg: LazyProxy | str = "not active",
+ ):
self.user_class = user_class
self.not_authenticated_msg = not_authenticated_msg
self.not_active_msg = not_active_msg
-
- async def __call__(self,
- handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
- event: TelegramObject,
- data: Dict[str, Any]) -> Any:
-
- user = await self.user_class.get(id = event.from_user.id, session = data["db_session"])
-
+ async def __call__(
+ self,
+ handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
+ event: TelegramObject,
+ data: Dict[str, Any],
+ ) -> Any:
+ user = await self.user_class.get(
+ id=event.from_user.id, session=data["db_session"]
+ )
+
if user and user.is_active:
data["user"] = user
return await handler(event, data)
-
+
if type(event) in [Message, CallbackQuery]:
if user and not user.is_active:
return await event.answer(self.not_active_msg)
return await event.answer(self.not_authenticated_msg)
-
-
-
-
\ No newline at end of file
diff --git a/middleware/telegram/i18n.py b/middleware/telegram/i18n.py
index 664e558..925f026 100644
--- a/middleware/telegram/i18n.py
+++ b/middleware/telegram/i18n.py
@@ -5,10 +5,6 @@ from ...model.user import UserBase
class I18nMiddleware(SimpleI18nMiddleware):
- """
- This middleware stores locale in the FSM storage
- """
-
def __init__[UserType: UserBase](
self,
user_class: type[UserType],
@@ -22,7 +18,7 @@ class I18nMiddleware(SimpleI18nMiddleware):
async def get_locale(self, event: TelegramObject, data: Dict[str, Any]) -> str:
db_session = data.get("db_session")
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:
return user.lang
- return await super().get_locale(event=event, data=data)
\ No newline at end of file
+ return await super().get_locale(event=event, data=data)
diff --git a/middleware/telegram/reset_state.py b/middleware/telegram/reset_state.py
index a1c9709..8801111 100644
--- a/middleware/telegram/reset_state.py
+++ b/middleware/telegram/reset_state.py
@@ -1,30 +1,26 @@
-from typing import Any, Awaitable, Callable, Dict
-from aiogram import BaseMiddleware
-from aiogram.types import TelegramObject
-from aiogram.fsm.context import FSMContext
-from aiogram.utils.i18n import gettext as _
+# from typing import Any, Awaitable, Callable, Dict
+# from aiogram import BaseMiddleware
+# from aiogram.types import TelegramObject
+# from aiogram.fsm.context import FSMContext
+# from aiogram.utils.i18n import gettext as _
-from ...bot.handlers.context import ContextData
+# from ...bot.handlers.context import ContextData
-class ResetStateMiddleware(BaseMiddleware):
- async def __call__(self,
- handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
- event: TelegramObject,
- 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
+# class ResetStateMiddleware(BaseMiddleware):
+# async def __call__(self,
+# handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
+# event: TelegramObject,
+# data: Dict[str, Any]) -> Any:
- 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
-
\ No newline at end of file
+# if not save_state:
+# state = data.get("state")
+# if isinstance(state, FSMContext):
+# await state.clear()
+
+# return await handler(event, data)
diff --git a/model/__init__.py b/model/__init__.py
index 68f59c5..50ee14d 100644
--- a/model/__init__.py
+++ b/model/__init__.py
@@ -7,9 +7,7 @@ from .bot_enum import BotEnum, EnumMember
from ..db import async_session
-
class EntityPermission(BotEnum):
-
LIST = EnumMember("list")
READ = EnumMember("read")
CREATE = EnumMember("create")
@@ -23,24 +21,23 @@ class EntityPermission(BotEnum):
def session_dep(func):
- @wraps(func)
- async def wrapper(cls, *args, **kwargs):
-
- if "session" in kwargs and kwargs["session"]:
- return await func(cls, *args, **kwargs)
+ @wraps(func)
+ async def wrapper(cls, *args, **kwargs):
+ if "session" in kwargs and kwargs["session"]:
+ return await func(cls, *args, **kwargs)
- _session = None
+ _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)
- else:
- kwargs["session"] = _session
+ 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 wrapper
\ No newline at end of file
+ else:
+ kwargs["session"] = _session
+ return await func(cls, *args, **kwargs)
+
+ return wrapper
diff --git a/model/_singleton.py b/model/_singleton.py
index 8a09d95..3776cb9 100644
--- a/model/_singleton.py
+++ b/model/_singleton.py
@@ -1,6 +1,7 @@
class Singleton(type):
_instances = {}
+
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
- return cls._instances[cls]
\ No newline at end of file
+ return cls._instances[cls]
diff --git a/model/bot_entity.py b/model/bot_entity.py
index 16ccc4d..c11feb7 100644
--- a/model/bot_entity.py
+++ b/model/bot_entity.py
@@ -1,33 +1,49 @@
-from functools import wraps
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 sqlmodel import SQLModel, BIGINT, Field, select, func, column
+
from sqlmodel.ext.asyncio.session import AsyncSession
+from sqlmodel.sql.expression import SelectOfScalar
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 . import session_dep
+if TYPE_CHECKING:
+ from .user import UserBase
+
class BotEntityMetaclass(SQLModelMetaclass):
-
__future_references__ = {}
def __new__(mcs, name, bases, namespace, **kwargs):
-
bot_fields_descriptors = {}
-
+
if bases:
- bot_entity_descriptor = bases[0].__dict__.get('bot_entity_descriptor')
- bot_fields_descriptors = {key: EntityFieldDescriptor(**value.__dict__.copy())
- for key, value in bot_entity_descriptor.fields_descriptors.items()} if bot_entity_descriptor else {}
-
- if '__annotations__' in namespace:
-
- for annotation in namespace['__annotations__']:
+ bot_entity_descriptor = bases[0].__dict__.get("bot_entity_descriptor")
+ bot_fields_descriptors = (
+ {
+ key: EntityFieldDescriptor(**value.__dict__.copy())
+ for key, value in bot_entity_descriptor.fields_descriptors.items()
+ }
+ if bot_entity_descriptor
+ else {}
+ )
+ if "__annotations__" in namespace:
+ for annotation in namespace["__annotations__"]:
if annotation in ["bot_entity_descriptor", "entity_metadata"]:
continue
@@ -36,41 +52,43 @@ class BotEntityMetaclass(SQLModelMetaclass):
if isinstance(attribute_value, RelationshipInfo):
continue
- descriptor_kwargs = {}
+ descriptor_kwargs = {}
descriptor_name = annotation
if attribute_value:
-
if isinstance(attribute_value, EntityField):
descriptor_kwargs = attribute_value.__dict__.copy()
sm_descriptor = descriptor_kwargs.pop("sm_descriptor", None)
-
+
if sm_descriptor:
namespace[annotation] = sm_descriptor
else:
- namespace.pop(annotation)
+ namespace.pop(annotation)
descriptor_name = descriptor_kwargs.pop("name") or annotation
- type_ = namespace['__annotations__'][annotation]
-
+ type_ = namespace["__annotations__"][annotation]
+
type_origin = get_origin(type_)
field_descriptor = EntityFieldDescriptor(
- name = descriptor_name,
- field_name = annotation,
- type_ = type_,
- type_base = type_,
- **descriptor_kwargs)
+ name=descriptor_name,
+ field_name=annotation,
+ type_=type_,
+ type_base=type_,
+ **descriptor_kwargs,
+ )
is_list = False
- if type_origin == list:
+ if type_origin is list:
field_descriptor.is_list = is_list = True
field_descriptor.type_base = type_ = get_args(type_)[0]
if type_origin == Union and isinstance(get_args(type_)[0], ForwardRef):
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:
field_descriptor.is_optional = True
@@ -78,12 +96,26 @@ class BotEntityMetaclass(SQLModelMetaclass):
if isinstance(type_, str):
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:
- field_descriptor.type_ = (list[entity_descriptor.type_] if is_list
- 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_)
+ field_descriptor.type_ = (
+ list[entity_descriptor.type_]
+ if is_list
+ 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
break
if type_not_found:
@@ -91,7 +123,7 @@ class BotEntityMetaclass(SQLModelMetaclass):
mcs.__future_references__[type_].append(field_descriptor)
else:
mcs.__future_references__[type_] = [field_descriptor]
-
+
bot_fields_descriptors[descriptor_name] = field_descriptor
descriptor_name = name
@@ -101,123 +133,246 @@ class BotEntityMetaclass(SQLModelMetaclass):
descriptor_kwargs: dict = entity_descriptor.__dict__.copy()
descriptor_name = descriptor_kwargs.pop("name", None)
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(
- name = descriptor_name,
- class_name = name,
- type_ = name,
- fields_descriptors = bot_fields_descriptors,
- field_sequence = descriptor_fields_sequence,
- **descriptor_kwargs)
+ name=descriptor_name,
+ class_name=name,
+ type_=name,
+ fields_descriptors=bot_fields_descriptors,
+ **descriptor_kwargs,
+ )
else:
- descriptor_fields_sequence = list(bot_fields_descriptors.keys())
- descriptor_fields_sequence.remove("id")
descriptor_name = name.lower()
namespace["bot_entity_descriptor"] = EntityDescriptor(
- name = descriptor_name,
- class_name = name,
- type_ = name,
- fields_descriptors = bot_fields_descriptors,
- field_sequence = descriptor_fields_sequence)
-
+ name=descriptor_name,
+ class_name=name,
+ type_=name,
+ fields_descriptors=bot_fields_descriptors,
+ )
+
+ 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():
field_descriptor.entity_descriptor = namespace["bot_entity_descriptor"]
if "table" not in kwargs:
kwargs["table"] = True
- if kwargs["table"] == True:
+ if kwargs["table"]:
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:
- namespace["__annotations__"]["entity_metadata"] = ClassVar[EntityMetadata]
+ namespace["__annotations__"]["entity_metadata"] = ClassVar[
+ EntityMetadata
+ ]
else:
- namespace["__annotations__"] = {"entity_metadata": ClassVar[EntityMetadata]}
-
+ namespace["__annotations__"] = {
+ "entity_metadata": ClassVar[EntityMetadata]
+ }
+
namespace["entity_metadata"] = entity_metadata
-
+
type_ = super().__new__(mcs, name, bases, namespace, **kwargs)
if name in mcs.__future_references__:
for field_descriptor in mcs.__future_references__[name]:
type_origin = get_origin(field_descriptor.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
- type_ | None if type_origin == UnionType else
- type_)
+ field_descriptor.type_base = type_
+
+ field_descriptor.type_ = (
+ list[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_)
return type_
-
-class BotEntity[CreateSchemaType: BaseModel,
- UpdateSchemaType: BaseModel](SQLModel,
- metaclass = BotEntityMetaclass,
- table = False):
-
+
+class BotEntity[CreateSchemaType: BaseModel, UpdateSchemaType: BaseModel](
+ SQLModel, metaclass=BotEntityMetaclass, table=False
+):
bot_entity_descriptor: ClassVar[EntityDescriptor]
entity_metadata: ClassVar[EntityMetadata]
- id: int = Field(
- primary_key = True,
- sa_type = BIGINT)
-
- name: str
-
+ id: int = Field(primary_key=True, sa_type=BIGINT)
@classmethod
@session_dep
- async def get(cls, *,
- session: AsyncSession | None = None,
- id: int):
-
- return await session.get(cls, id, populate_existing = True)
-
+ async def get(cls, *, session: AsyncSession | None = None, 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
@session_dep
- async def get_count(cls, *,
- session: AsyncSession | None = None,
- filter: str = None) -> int:
-
+ async def get_count(
+ cls,
+ *,
+ 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)
- if filter:
- select_statement = select_statement.where(column("name").ilike(f"%{filter}%"))
+ if static_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)
-
-
+
@classmethod
@session_dep
- async def get_multi(cls, *,
- session: AsyncSession | None = None,
- order_by = None,
- filter:str = None,
- skip: int = 0,
- limit: int = None):
-
+ async def get_multi(
+ cls,
+ *,
+ session: AsyncSession | None = None,
+ order_by=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)
if limit:
select_statement = select_statement.limit(limit)
- if filter:
- select_statement = select_statement.where(column("name").ilike(f"%{filter}%"))
+ if static_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:
select_statement = select_statement.order_by(order_by)
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
@session_dep
- async def create(cls, *,
- session: AsyncSession | None = None,
- obj_in: CreateSchemaType,
- commit: bool = False):
-
+ async def create(
+ cls,
+ *,
+ session: AsyncSession | None = None,
+ obj_in: CreateSchemaType,
+ commit: bool = False,
+ ):
if isinstance(obj_in, cls):
obj = obj_in
else:
@@ -226,16 +381,17 @@ class BotEntity[CreateSchemaType: BaseModel,
if commit:
await session.commit()
return obj
-
-
+
@classmethod
@session_dep
- async def update(cls, *,
- session: AsyncSession | None = None,
- id: int,
- obj_in: UpdateSchemaType,
- commit: bool = False):
-
+ async def update(
+ cls,
+ *,
+ session: AsyncSession | None = None,
+ id: int,
+ obj_in: UpdateSchemaType,
+ commit: bool = False,
+ ):
obj = await session.get(cls, id)
if obj:
obj_data = obj.model_dump()
@@ -248,14 +404,12 @@ class BotEntity[CreateSchemaType: BaseModel,
await session.commit()
return obj
return None
-
-
+
@classmethod
@session_dep
- async def remove(cls, *,
- session: AsyncSession | None = None,
- id: int,
- commit: bool = False):
+ async def remove(
+ cls, *, session: AsyncSession | None = None, id: int, commit: bool = False
+ ):
obj = await session.get(cls, id)
if obj:
await session.delete(obj)
@@ -263,4 +417,3 @@ class BotEntity[CreateSchemaType: BaseModel,
await session.commit()
return obj
return None
-
diff --git a/model/bot_enum.py b/model/bot_enum.py
index 274984b..a72b83e 100644
--- a/model/bot_enum.py
+++ b/model/bot_enum.py
@@ -4,36 +4,46 @@ from typing import Any, Self, overload
class BotEnumMetaclass(type):
-
def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]):
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"]
-
+
annotations = {}
for key, value in namespace.items():
- if (key.isupper() and
- not key.startswith("__") and
- not key.endswith("__")):
-
+ if key.isupper() and not key.startswith("__") and not key.endswith("__"):
if not isinstance(value, EnumMember):
value = EnumMember(value, None)
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.")
-
- if (value.value in [member.value for member in all_members.values()] and
- key not in all_members.keys()):
+ 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 key not in all_members.keys()
+ ):
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
all_members[key] = member
annotations[key] = type(member)
-
- namespace["__annotations__"] = annotations
+
+ namespace["__annotations__"] = annotations
namespace["all_members"] = all_members
type_ = super().__new__(cls, name, bases, namespace)
@@ -46,22 +56,23 @@ class BotEnumMetaclass(type):
class EnumMember(object):
+ @overload
+ def __init__(self, value: str) -> "EnumMember": ...
@overload
- def __init__(self, value: str) -> "EnumMember":...
+ def __init__(self, value: "EnumMember") -> "EnumMember": ...
@overload
- def __init__(self, value: "EnumMember") -> "EnumMember":...
+ def __init__(self, value: str, loc_obj: dict[str, str]) -> "EnumMember": ...
- @overload
- def __init__(self, value: str, loc_obj: dict[str, str]) -> "EnumMember":...
-
- def __init__(self,
- value: str = None,
- loc_obj: dict[str, str] = None,
- parent: type = None,
- name: str = None,
- casting: bool = True) -> "EnumMember":
+ def __init__(
+ self,
+ value: str = None,
+ loc_obj: dict[str, str] = None,
+ parent: type = None,
+ name: str = None,
+ casting: bool = True,
+ ) -> "EnumMember":
if not casting:
self._parent = parent
self._name = name
@@ -69,10 +80,9 @@ class EnumMember(object):
self.loc_obj = loc_obj
@overload
- def __new__(cls: Self, *args, **kwargs) -> "EnumMember":...
+ def __new__(cls: Self, *args, **kwargs) -> "EnumMember": ...
def __new__(cls, *args, casting: bool = True, **kwargs) -> "EnumMember":
-
if (cls.__name__ == "EnumMember") or not casting:
obj = super().__new__(cls)
kwargs["casting"] = False
@@ -80,56 +90,59 @@ class EnumMember(object):
return obj
if args.__len__() == 0:
return list(cls.all_members.values())[0]
- if args.__len__() == 1 and isinstance(args[0], str):
- return {member.value: member for key, member in cls.all_members.items()}[args[0]]
+ if args.__len__() == 1 and isinstance(args[0], str):
+ return {member.value: member for key, member in cls.all_members.items()}[
+ args[0]
+ ]
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:
return args[0]
-
+
def __get_pydantic_core_schema__(cls, *args, **kwargs):
return str_schema()
-
+
def __get__(self, instance, owner) -> Self:
- # return {member.value: member for key, member in owner.all_members.items()}[self.value]
- return {member.value: member for key, member in self._parent.all_members.items()}[self.value]
-
+ return {
+ member.value: member for key, member in self._parent.all_members.items()
+ }[self.value]
+
def __set__(self, instance, value):
instance.__dict__[self] = value
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):
return self.value
-
- def __eq__(self, other : Self | str) -> bool:
- if other is None:
- return False
- if isinstance(other, str):
- return self.value == other
- return self.value == other.value
-
+
+ def __eq__(self, other: Self | str) -> bool:
+ if other is None:
+ return False
+ if isinstance(other, str):
+ return self.value == other
+ return self.value == other.value
+
def __hash__(self):
return hash(self.value)
-
+
def localized(self, lang: str = None) -> str:
if self.loc_obj and len(self.loc_obj) > 0:
if lang and lang in self.loc_obj.keys():
return self.loc_obj[lang]
else:
return self.loc_obj[list(self.loc_obj.keys())[0]]
-
+
return self.value
-
-class BotEnum(EnumMember, metaclass = BotEnumMetaclass):
+class BotEnum(EnumMember, metaclass=BotEnumMetaclass):
all_members: dict[str, EnumMember]
class EnumType(TypeDecorator):
-
impl = String(256)
def __init__(self, enum_type: BotEnum):
@@ -140,8 +153,8 @@ class EnumType(TypeDecorator):
if value and isinstance(value, EnumMember):
return value.value
return None
-
+
def process_result_value(self, value, dialect):
if value:
return self._enum_type(value)
- return None
\ No newline at end of file
+ return None
diff --git a/model/default_user.py b/model/default_user.py
index a83b15d..453ae12 100644
--- a/model/default_user.py
+++ b/model/default_user.py
@@ -1,4 +1,4 @@
from .user import UserBase
-class DefaultUser(UserBase): ...
\ No newline at end of file
+class DefaultUser(UserBase): ...
diff --git a/model/descriptors.py b/model/descriptors.py
index c9bc222..7e94e90 100644
--- a/model/descriptors.py
+++ b/model/descriptors.py
@@ -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 dataclasses import dataclass, field
+from sqlmodel.ext.asyncio.session import AsyncSession
from .role import RoleBase
from . import EntityPermission
+from ..bot.handlers.context import ContextData
if TYPE_CHECKING:
from .bot_entity import BotEntity
+ from ..main import QBotApp
+ from .user import UserBase
EntityCaptionCallable = Callable[["EntityDescriptor"], str]
EntityItemCaptionCallable = Callable[["EntityDescriptor", Any], str]
EntityFieldCaptionCallable = Callable[["EntityFieldDescriptor", Any, Any], str]
-@dataclass(kw_only = True)
-class _BaseEntityFieldDescriptor():
+@dataclass
+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
caption: str | LazyProxy | EntityFieldCaptionCallable | None = None
description: str | LazyProxy | EntityFieldCaptionCallable | None = None
@@ -24,21 +89,25 @@ class _BaseEntityFieldDescriptor():
localizable: bool = False
bool_false_value: str | LazyProxy = "no"
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
-@dataclass(kw_only = True)
+@dataclass(kw_only=True)
class EntityField(_BaseEntityFieldDescriptor):
name: str | None = None
sm_descriptor: Any = None
-@dataclass(kw_only = True)
+@dataclass(kw_only=True)
class Setting(_BaseEntityFieldDescriptor):
name: str | None = None
-@dataclass(kw_only = True)
+@dataclass(kw_only=True)
class EntityFieldDescriptor(_BaseEntityFieldDescriptor):
name: str
field_name: str
@@ -52,42 +121,80 @@ class EntityFieldDescriptor(_BaseEntityFieldDescriptor):
return self.name.__hash__()
-@dataclass(kw_only = True)
+@dataclass(kw_only=True)
class _BaseEntityDescriptor:
-
icon: str = "📘"
- caption: str | LazyProxy | EntityCaptionCallable | None = None
- caption_plural: str | LazyProxy | EntityCaptionCallable | None = None
+ full_name: str | LazyProxy | EntityCaptionCallable | None = None
+ full_name_plural: 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
- field_sequence: list[str] = None
- edit_button_visible: bool = True
- edit_buttons: list[list[str | tuple[str, str | LazyProxy | EntityFieldCaptionCallable]]] = None
- permissions: dict[EntityPermission, list[RoleBase]] = field(default_factory = lambda: {
- EntityPermission.LIST: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
- EntityPermission.READ: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
- EntityPermission.CREATE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
- EntityPermission.UPDATE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
- EntityPermission.DELETE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
- EntityPermission.LIST_ALL: [RoleBase.SUPER_USER],
- EntityPermission.READ_ALL: [RoleBase.SUPER_USER],
- EntityPermission.CREATE_ALL: [RoleBase.SUPER_USER],
- EntityPermission.UPDATE_ALL: [RoleBase.SUPER_USER],
- EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER]
- })
+ ownership_fields: dict[RoleBase, str] = field(default_factory=dict[RoleBase, str])
+ permissions: dict[EntityPermission, list[RoleBase]] = field(
+ default_factory=lambda: {
+ EntityPermission.LIST: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
+ EntityPermission.READ: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
+ EntityPermission.CREATE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
+ EntityPermission.UPDATE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
+ EntityPermission.DELETE: [RoleBase.DEFAULT_USER, RoleBase.SUPER_USER],
+ EntityPermission.LIST_ALL: [RoleBase.SUPER_USER],
+ EntityPermission.READ_ALL: [RoleBase.SUPER_USER],
+ EntityPermission.CREATE_ALL: [RoleBase.SUPER_USER],
+ EntityPermission.UPDATE_ALL: [RoleBase.SUPER_USER],
+ EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER],
+ }
+ )
-@dataclass(kw_only = True)
+@dataclass(kw_only=True)
class Entity(_BaseEntityDescriptor):
-
name: str | None = None
-
+
@dataclass
class EntityDescriptor(_BaseEntityDescriptor):
-
name: str
class_name: str
type_: type["BotEntity"]
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): ...
diff --git a/model/entity_metadata.py b/model/entity_metadata.py
index 5feb309..47c9886 100644
--- a/model/entity_metadata.py
+++ b/model/entity_metadata.py
@@ -2,7 +2,6 @@ from .descriptors import EntityDescriptor
from ._singleton import Singleton
-class EntityMetadata(metaclass = Singleton):
-
+class EntityMetadata(metaclass=Singleton):
def __init__(self):
self.entity_descriptors: dict[str, EntityDescriptor] = {}
diff --git a/model/field_types.py b/model/field_types.py
deleted file mode 100644
index 29dede7..0000000
--- a/model/field_types.py
+++ /dev/null
@@ -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]
-
-
-
diff --git a/model/fsm_storage.py b/model/fsm_storage.py
index 58ad936..5148f2c 100644
--- a/model/fsm_storage.py
+++ b/model/fsm_storage.py
@@ -1,8 +1,7 @@
from sqlmodel import SQLModel, Field
-class FSMStorage(SQLModel, table = True):
-
+class FSMStorage(SQLModel, table=True):
__tablename__ = "fsm_storage"
- key: str = Field(primary_key = True)
- value: str | None = None
\ No newline at end of file
+ key: str = Field(primary_key=True)
+ value: str | None = None
diff --git a/model/language.py b/model/language.py
index 097364e..afee9ba 100644
--- a/model/language.py
+++ b/model/language.py
@@ -2,5 +2,4 @@ from .bot_enum import BotEnum, EnumMember
class LanguageBase(BotEnum):
-
- EN = EnumMember("en", {"en": "🇬🇧 english"})
\ No newline at end of file
+ EN = EnumMember("en", {"en": "🇬🇧 english"})
diff --git a/model/menu.py b/model/menu.py
deleted file mode 100644
index e37b46b..0000000
--- a/model/menu.py
+++ /dev/null
@@ -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]] = []
\ No newline at end of file
diff --git a/model/owned_bot_entity.py b/model/owned_bot_entity.py
deleted file mode 100644
index dfbc546..0000000
--- a/model/owned_bot_entity.py
+++ /dev/null
@@ -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)
\ No newline at end of file
diff --git a/model/role.py b/model/role.py
index 9f41833..38462b6 100644
--- a/model/role.py
+++ b/model/role.py
@@ -2,6 +2,5 @@ from .bot_enum import BotEnum, EnumMember
class RoleBase(BotEnum):
-
SUPER_USER = EnumMember("super_user")
- DEFAULT_USER = EnumMember("default_user")
\ No newline at end of file
+ DEFAULT_USER = EnumMember("default_user")
diff --git a/model/settings.py b/model/settings.py
index 215a5d8..1ed5ba9 100644
--- a/model/settings.py
+++ b/model/settings.py
@@ -7,131 +7,193 @@ from typing import Any, get_args, get_origin
from ..db import async_session
from .role import RoleBase
from .descriptors import EntityFieldDescriptor, Setting
-from ..utils import deserialize, serialize
+from ..utils.serialization import deserialize, serialize
import ujson as json
-class DbSettings(SQLModel, table = True):
+class DbSettings(SQLModel, table=True):
__tablename__ = "settings"
- name: str = Field(primary_key = True)
+ name: str = Field(primary_key=True)
value: str
class SettingsMetaclass(type):
-
def __new__(cls, class_name, base_classes, attributes):
-
settings_descriptors = {}
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"]:
continue
-
+
attr_value = attributes.get(annotation)
name = annotation
-
- type_ = attributes['__annotations__'][annotation]
+
+ type_ = attributes["__annotations__"][annotation]
if isinstance(attr_value, Setting):
descriptor_kwargs = attr_value.__dict__.copy()
name = descriptor_kwargs.pop("name") or annotation
attributes[annotation] = EntityFieldDescriptor(
- name = name,
- field_name = annotation,
- type_ = type_,
- type_base = type_,
- **descriptor_kwargs)
-
+ name=name,
+ field_name=annotation,
+ type_=type_,
+ type_base=type_,
+ **descriptor_kwargs,
+ )
+
else:
attributes[annotation] = EntityFieldDescriptor(
- name = annotation,
- field_name = annotation,
- type_ = type_,
- type_base = type_,
- default = attr_value)
-
+ name=annotation,
+ field_name=annotation,
+ type_=type_,
+ type_base=type_,
+ default=attr_value,
+ )
+
type_origin = get_origin(type_)
- if type_origin == list:
+ if type_origin is list:
attributes[annotation].is_list = True
attributes[annotation].type_base = type_ = get_args(type_)[0]
elif type_origin == UnionType and get_args(type_)[1] == NoneType:
attributes[annotation].is_optional = True
attributes[annotation].type_base = type_ = get_args(type_)[0]
-
+
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])
-
+
attributes["__annotations__"] = {}
attributes["_settings_descriptors"] = settings_descriptors
-
+
return super().__new__(cls, class_name, base_classes, attributes)
-class Settings(metaclass = SettingsMetaclass):
-
+class Settings(metaclass=SettingsMetaclass):
_cache: dict[str, Any] = dict[str, Any]()
_settings_descriptors: dict[str, EntityFieldDescriptor] = {}
-
- PAGE_SIZE: int = Setting(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_GREETING_P_NAME: str = Setting(name = "AS_GREETING", default = "Hello, {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_FORBIDDEN: str = Setting(name = "AS_FORBIDDEN", default = "Forbidden", 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_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)
+ PAGE_SIZE: int = Setting(
+ 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_GREETING_P_NAME: str = Setting(
+ name="AS_GREETING", default="Hello, {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_FORBIDDEN: str = Setting(
+ name="AS_FORBIDDEN", default="Forbidden", 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_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(
- name = "AS_CONFIRM_DEL",
- default = "Are you sure you want to delete \"{name}\"?",
- 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_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)
+ name="AS_CONFIRM_DEL",
+ default='Are you sure you want to delete "{name}"?',
+ 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_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(
- name = "AS_FIELDEDIT_PROMPT",
- default = "Enter new value for \"{name}\" (current value: {value})",
- is_visible = False)
+ name="AS_FIELDEDIT_PROMPT",
+ default='Enter new value for "{name}" (current value: {value})',
+ is_visible=False,
+ )
APP_STRINGS_FIELD_CREATE_PROMPT_TEMPLATE_P_NAME: str = Setting(
- name = "AS_FIELDCREATE_PROMPT",
- default = "Enter new value for \"{name}\"",
- is_visible = False)
+ name="AS_FIELDCREATE_PROMPT",
+ default='Enter new value for "{name}"',
+ is_visible=False,
+ )
APP_STRINGS_STRING_EDITOR_LOCALE_TEMPLATE_P_NAME: str = Setting(
- name = "AS_STREDIT_LOC_TEMPLATE",
- default = "string for \"{name}\"",
- is_visible = False)
- APP_STRINGS_VIEW_FILTER_EDIT_PROMPT: str = Setting(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)
+ name="AS_STREDIT_LOC_TEMPLATE", default='string for "{name}"', is_visible=False
+ )
+ APP_STRINGS_VIEW_FILTER_EDIT_PROMPT: str = Setting(
+ 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
+ )
-
@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
if name not in cls._cache.keys():
@@ -144,73 +206,79 @@ class Settings(metaclass = SettingsMetaclass):
locale = get_i18n().current_locale
try:
obj = json.loads(ret_val)
- except:
+ except Exception:
return ret_val
return obj.get(locale, obj[list(obj.keys())[0]])
return ret_val
-
@classmethod
async def load_param(cls, param: EntityFieldDescriptor) -> Any:
-
async with async_session() as session:
- db_setting = (await session.exec(
- select(DbSettings)
- .where(DbSettings.name == param.field_name))).first()
-
- if db_setting:
- return await deserialize(session = session,
- type_ = param.type_,
- 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_())
-
+ db_setting = (
+ await session.exec(
+ select(DbSettings).where(DbSettings.name == param.field_name)
+ )
+ ).first()
+ if db_setting:
+ return await deserialize(
+ session=session, type_=param.type_, value=db_setting.value
+ )
+
+ 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
async def load_params(cls):
-
async with async_session() as session:
db_settings = (await session.exec(select(DbSettings))).all()
for db_setting in db_settings:
if db_setting.name in cls.__dict__:
- setting = cls.__dict__[db_setting.name]
- cls._cache[db_setting.name] = await deserialize(session = session,
- type_ = setting.type_,
- value = db_setting.value,
- default = setting.default)
-
+ setting = cls.__dict__[db_setting.name] # type: EntityFieldDescriptor
+ cls._cache[db_setting.name] = await deserialize(
+ session=session,
+ type_=setting.type_,
+ value=db_setting.value,
+ default=setting.default,
+ )
+
cls._loaded = True
-
-
+
@classmethod
async def set_param(cls, param: str | EntityFieldDescriptor, value) -> None:
if isinstance(param, str):
- param = cls._settings_descriptors[param]
+ param = cls._settings_descriptors[param]
ser_value = serialize(value, param)
async with async_session() as session:
- db_setting = (await session.exec(
- select(DbSettings)
- .where(DbSettings.name == param.field_name))).first()
+ db_setting = (
+ await session.exec(
+ select(DbSettings).where(DbSettings.name == param.field_name)
+ )
+ ).first()
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)
session.add(db_setting)
await session.commit()
cls._cache[param.field_name] = value
-
@classmethod
def list_params(cls) -> dict[str, EntityFieldDescriptor]:
return cls._settings_descriptors
-
-
+
@classmethod
async def get_params(cls) -> dict[EntityFieldDescriptor, Any]:
-
params = cls.list_params()
- return {param: await cls.get(param, all_locales = True) for _, param in params.items()}
\ No newline at end of file
+ return {
+ param: await cls.get(param, all_locales=True) for _, param in params.items()
+ }
diff --git a/model/user.py b/model/user.py
index 889e802..9880f31 100644
--- a/model/user.py
+++ b/model/user.py
@@ -10,11 +10,12 @@ from .fsm_storage import FSMStorage as FSMStorage
from .view_setting import ViewSetting as ViewSetting
-class UserBase(BotEntity, table = False):
-
+class UserBase(BotEntity, table=False):
__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
-
- roles: list[RoleBase] = Field(sa_type=ARRAY(EnumType(RoleBase)), default = [RoleBase.DEFAULT_USER])
\ No newline at end of file
+
+ roles: list[RoleBase] = Field(
+ sa_type=ARRAY(EnumType(RoleBase)), default=[RoleBase.DEFAULT_USER]
+ )
diff --git a/model/view_setting.py b/model/view_setting.py
index 8ecaec7..8f99ac7 100644
--- a/model/view_setting.py
+++ b/model/view_setting.py
@@ -4,36 +4,36 @@ from sqlalchemy.ext.asyncio.session import AsyncSession
from . import session_dep
-class ViewSetting(SQLModel, table = True):
-
+class ViewSetting(SQLModel, table=True):
__tablename__ = "view_setting"
- user_id: int = Field(sa_type = BIGINT, primary_key = True, foreign_key="user.id", ondelete="CASCADE")
- entity_name: str = Field(primary_key = True)
- filter: str | None = None
+ user_id: int = Field(
+ sa_type=BIGINT, primary_key=True, foreign_key="user.id", ondelete="CASCADE"
+ )
+ entity_name: str = Field(primary_key=True)
+ filter: str | None = None
-
@classmethod
@session_dep
- async def get_filter(cls, *,
- session: AsyncSession | None = None,
- user_id: int,
- entity_name: str):
-
+ async def get_filter(
+ cls, *, session: AsyncSession | None = None, user_id: int, entity_name: str
+ ):
setting = await session.get(cls, (user_id, entity_name))
return setting.filter if setting else None
-
+
@classmethod
@session_dep
- async def set_filter(cls, *,
- session: AsyncSession | None = None,
- user_id: int,
- entity_name: str,
- filter: str):
-
- setting = await session.get(cls, (user_id, entity_name))
- if setting:
- setting.filter = filter
- else:
- setting = cls(user_id = user_id, entity_name = entity_name, filter = filter)
- session.add(setting)
- await session.commit()
\ No newline at end of file
+ async def set_filter(
+ cls,
+ *,
+ session: AsyncSession | None = None,
+ user_id: int,
+ entity_name: str,
+ filter: str,
+ ):
+ setting = await session.get(cls, (user_id, entity_name))
+ if setting:
+ setting.filter = filter
+ else:
+ setting = cls(user_id=user_id, entity_name=entity_name, filter=filter)
+ session.add(setting)
+ await session.commit()
diff --git a/router.py b/router.py
new file mode 100644
index 0000000..2aa4a9b
--- /dev/null
+++ b/router.py
@@ -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
diff --git a/utils/main.py b/utils/main.py
new file mode 100644
index 0000000..250487e
--- /dev/null
+++ b/utils/main.py
@@ -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
diff --git a/utils/__init__.py b/utils/serialization.py
similarity index 63%
rename from utils/__init__.py
rename to utils/serialization.py
index e23e0b5..ce1d6d2 100644
--- a/utils/__init__.py
+++ b/utils/serialization.py
@@ -1,18 +1,14 @@
-from datetime import datetime
+from datetime import datetime, time
from decimal import Decimal
-from types import NoneType, UnionType
from sqlmodel import select, column
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
from ..model.bot_entity import BotEntity
from ..model.bot_enum import BotEnum
-from ..model.descriptors import EntityFieldDescriptor, EntityDescriptor
-from ..model import EntityPermission
-
-if TYPE_CHECKING:
- from ..model.user import UserBase
+from ..model.descriptors import EntityFieldDescriptor
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
if type_origin in [UnionType, Union]:
args = get_args(type_)
- if args[1] == NoneType:
+ if args[1] is NoneType:
type_ = args[0]
if value is None:
return None
is_optional = True
- if get_origin(type_) == list:
+ if get_origin(type_) is list:
arg_type = None
args = get_args(type_)
if args:
@@ -34,7 +30,9 @@ async def deserialize[T](session: AsyncSession, type_: type[T], value: str = Non
if arg_type:
if issubclass(arg_type, BotEntity):
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:
ret.append(item)
return ret
@@ -52,55 +50,43 @@ async def deserialize[T](session: AsyncSession, type_: type[T], value: str = Non
if is_optional and not value:
return None
return type_(value)
- elif type_ == datetime:
+ elif type_ is time:
if is_optional and not value:
return None
- return datetime.fromisoformat(value)
- elif type_ == bool:
+ return time.fromisoformat(value.replace("-", ":"))
+ 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"
- elif type_ == Decimal:
+ elif type_ is Decimal:
if is_optional and not value:
return None
return Decimal(value)
-
+
if is_optional and not value:
return None
return type_(value)
def serialize(value: Any, field_descriptor: EntityFieldDescriptor) -> str:
-
if value is None:
return ""
type_ = field_descriptor.type_base
-
+
if field_descriptor.is_list:
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):
- 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:
- return json.dumps(value, ensure_ascii = False)
+ return json.dumps(value, ensure_ascii=False)
elif issubclass(type_, BotEntity):
return str(value.id) if value else ""
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]])
\ No newline at end of file