Compare commits
2 Commits
02aec23b84
...
fe0380f9f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe0380f9f3 | ||
|
|
f0db2b2830 |
@@ -31,7 +31,11 @@ async def telegram_webhook(
|
|||||||
return Response(status_code=400)
|
return Response(status_code=400)
|
||||||
try:
|
try:
|
||||||
await app.dp.feed_webhook_update(
|
await app.dp.feed_webhook_update(
|
||||||
app.bot, update, db_session=db_session, app=app
|
app.bot,
|
||||||
|
update,
|
||||||
|
db_session=db_session,
|
||||||
|
app=app,
|
||||||
|
**(request.state if request.state else {}),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Error processing update", exc_info=True)
|
logger.error("Error processing update", exc_info=True)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from babel.support import LazyProxy
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from ....model.descriptors import FieldDescriptor
|
from ....model.descriptors import FieldDescriptor
|
||||||
|
from ....model.user import UserBase
|
||||||
from ..context import ContextData, CallbackCommand
|
from ..context import ContextData, CallbackCommand
|
||||||
from ....utils.main import get_send_message
|
from ....utils.main import get_send_message
|
||||||
from .wrapper import wrap_editor
|
from .wrapper import wrap_editor
|
||||||
@@ -20,6 +21,7 @@ async def bool_editor(
|
|||||||
edit_prompt: str,
|
edit_prompt: str,
|
||||||
field_descriptor: FieldDescriptor,
|
field_descriptor: FieldDescriptor,
|
||||||
callback_data: ContextData,
|
callback_data: ContextData,
|
||||||
|
user: UserBase,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
keyboard_builder = InlineKeyboardBuilder()
|
keyboard_builder = InlineKeyboardBuilder()
|
||||||
@@ -70,6 +72,7 @@ async def bool_editor(
|
|||||||
field_descriptor=field_descriptor,
|
field_descriptor=field_descriptor,
|
||||||
callback_data=callback_data,
|
callback_data=callback_data,
|
||||||
state_data=state_data,
|
state_data=state_data,
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
state: FSMContext = kwargs["state"]
|
state: FSMContext = kwargs["state"]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from ....model.descriptors import FieldDescriptor
|
from ....model.descriptors import FieldDescriptor
|
||||||
from ....model.settings import Settings
|
from ....model.settings import Settings
|
||||||
|
from ....model.user import UserBase
|
||||||
from ..context import ContextData, CallbackCommand
|
from ..context import ContextData, CallbackCommand
|
||||||
from ....utils.main import get_send_message, get_field_descriptor
|
from ....utils.main import get_send_message, get_field_descriptor
|
||||||
from .wrapper import wrap_editor
|
from .wrapper import wrap_editor
|
||||||
@@ -49,6 +50,7 @@ async def time_picker(
|
|||||||
callback_data: ContextData,
|
callback_data: ContextData,
|
||||||
current_value: datetime | time,
|
current_value: datetime | time,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
|
user: UserBase,
|
||||||
edit_prompt: str | None = None,
|
edit_prompt: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -162,6 +164,7 @@ async def time_picker(
|
|||||||
field_descriptor=field_descriptor,
|
field_descriptor=field_descriptor,
|
||||||
callback_data=callback_data,
|
callback_data=callback_data,
|
||||||
state_data=state_data,
|
state_data=state_data,
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
await state.set_data(state_data)
|
await state.set_data(state_data)
|
||||||
@@ -179,6 +182,7 @@ async def date_picker(
|
|||||||
callback_data: ContextData,
|
callback_data: ContextData,
|
||||||
current_value: datetime,
|
current_value: datetime,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
|
user: UserBase,
|
||||||
edit_prompt: str | None = None,
|
edit_prompt: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -273,6 +277,7 @@ async def date_picker(
|
|||||||
field_descriptor=field_descriptor,
|
field_descriptor=field_descriptor,
|
||||||
callback_data=callback_data,
|
callback_data=callback_data,
|
||||||
state_data=state_data,
|
state_data=state_data,
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
await state.set_data(state_data)
|
await state.set_data(state_data)
|
||||||
@@ -292,6 +297,7 @@ async def date_picker_year(
|
|||||||
callback_data: ContextData,
|
callback_data: ContextData,
|
||||||
app: "QBotApp",
|
app: "QBotApp",
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
|
user: UserBase,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
start_date = datetime.strptime(callback_data.data, "%Y-%m-%d %H-%M")
|
start_date = datetime.strptime(callback_data.data, "%Y-%m-%d %H-%M")
|
||||||
@@ -365,6 +371,7 @@ async def date_picker_year(
|
|||||||
field_descriptor=field_descriptor,
|
field_descriptor=field_descriptor,
|
||||||
callback_data=callback_data,
|
callback_data=callback_data,
|
||||||
state_data=state_data,
|
state_data=state_data,
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
await query.message.edit_reply_markup(reply_markup=keyboard_builder.as_markup())
|
await query.message.edit_reply_markup(reply_markup=keyboard_builder.as_markup())
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ async def render_entity_picker(
|
|||||||
field_descriptor=field_descriptor,
|
field_descriptor=field_descriptor,
|
||||||
callback_data=callback_data,
|
callback_data=callback_data,
|
||||||
state_data=state_data,
|
state_data=state_data,
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
await state.set_data(state_data)
|
await state.set_data(state_data)
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ async def field_editor(message: Message | CallbackQuery, **kwargs):
|
|||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
stack, context = get_navigation_context(state_data=state_data)
|
stack, context = get_navigation_context(state_data=state_data)
|
||||||
|
|
||||||
|
kwargs.update({"callback_data": context})
|
||||||
|
|
||||||
return await entity_item(
|
return await entity_item(
|
||||||
query=message, navigation_stack=stack, **kwargs
|
query=message, navigation_stack=stack, **kwargs
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from ....utils.main import (
|
|||||||
clear_state,
|
clear_state,
|
||||||
get_entity_descriptor,
|
get_entity_descriptor,
|
||||||
get_field_descriptor,
|
get_field_descriptor,
|
||||||
|
build_field_sequence,
|
||||||
)
|
)
|
||||||
from ....utils.serialization import deserialize
|
from ....utils.serialization import deserialize
|
||||||
from ..common.routing import route_callback
|
from ..common.routing import route_callback
|
||||||
@@ -168,7 +169,15 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
|
|||||||
form_name, entity_descriptor.default_form
|
form_name, entity_descriptor.default_form
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if form.edit_field_sequence:
|
||||||
field_sequence = form.edit_field_sequence
|
field_sequence = form.edit_field_sequence
|
||||||
|
else:
|
||||||
|
field_sequence = build_field_sequence(
|
||||||
|
entity_descriptor=entity_descriptor,
|
||||||
|
user=user,
|
||||||
|
callback_data=callback_data,
|
||||||
|
)
|
||||||
|
|
||||||
current_index = (
|
current_index = (
|
||||||
field_sequence.index(callback_data.field_name)
|
field_sequence.index(callback_data.field_name)
|
||||||
if callback_data.context
|
if callback_data.context
|
||||||
@@ -251,10 +260,14 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
|
|||||||
|
|
||||||
# What if user has several roles and each role has its own ownership field? Should we allow creation even
|
# What if user has several roles and each role has its own ownership field? Should we allow creation even
|
||||||
# if user has no CREATE_ALL permission
|
# if user has no CREATE_ALL permission
|
||||||
|
user_permissions = get_user_permissions(user, entity_descriptor)
|
||||||
|
|
||||||
# for role in user.roles:
|
for role in user.roles:
|
||||||
# if role in entity_descriptor.ownership_fields and not EntityPermission.CREATE_ALL in user_permissions:
|
if (
|
||||||
# entity_data[entity_descriptor.ownership_fields[role]] = user.id
|
role in entity_descriptor.ownership_fields
|
||||||
|
and EntityPermission.CREATE_ALL not in user_permissions
|
||||||
|
):
|
||||||
|
entity_data[entity_descriptor.ownership_fields[role]] = user.id
|
||||||
|
|
||||||
deser_entity_data = {
|
deser_entity_data = {
|
||||||
key: await deserialize(
|
key: await deserialize(
|
||||||
@@ -284,9 +297,19 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
|
|||||||
|
|
||||||
if entity_descriptor.on_created:
|
if entity_descriptor.on_created:
|
||||||
if iscoroutinefunction(entity_descriptor.on_created):
|
if iscoroutinefunction(entity_descriptor.on_created):
|
||||||
await entity_descriptor.on_created(new_entity, EntityEventContext(db_session=db_session, app=app))
|
await entity_descriptor.on_created(
|
||||||
|
new_entity,
|
||||||
|
EntityEventContext(
|
||||||
|
db_session=db_session, app=app, message=message
|
||||||
|
),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
entity_descriptor.on_created(new_entity, EntityEventContext(db_session=db_session, app=app))
|
entity_descriptor.on_created(
|
||||||
|
new_entity,
|
||||||
|
EntityEventContext(
|
||||||
|
db_session=db_session, app=app, message=message
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
form_name = (
|
form_name = (
|
||||||
callback_data.form_params.split("&")[0]
|
callback_data.form_params.split("&")[0]
|
||||||
@@ -336,9 +359,19 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
|
|||||||
|
|
||||||
if entity_descriptor.on_updated:
|
if entity_descriptor.on_updated:
|
||||||
if iscoroutinefunction(entity_descriptor.on_updated):
|
if iscoroutinefunction(entity_descriptor.on_updated):
|
||||||
await entity_descriptor.on_updated(entity, EntityEventContext(db_session=db_session, app=app))
|
await entity_descriptor.on_updated(
|
||||||
|
entity,
|
||||||
|
EntityEventContext(
|
||||||
|
db_session=db_session, app=app, message=message
|
||||||
|
),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
entity_descriptor.on_updated(entity, EntityEventContext(db_session=db_session, app=app))
|
entity_descriptor.on_updated(
|
||||||
|
entity,
|
||||||
|
EntityEventContext(
|
||||||
|
db_session=db_session, app=app, message=message
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
elif callback_data.context == CommandContext.COMMAND_FORM:
|
elif callback_data.context == CommandContext.COMMAND_FORM:
|
||||||
clear_state(state_data=state_data)
|
clear_state(state_data=state_data)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import Any
|
|||||||
from ....model.descriptors import FieldDescriptor
|
from ....model.descriptors import FieldDescriptor
|
||||||
from ....model.language import LanguageBase
|
from ....model.language import LanguageBase
|
||||||
from ....model.settings import Settings
|
from ....model.settings import Settings
|
||||||
|
from ....model.user import UserBase
|
||||||
from ....utils.main import get_send_message, get_local_text
|
from ....utils.main import get_send_message, get_local_text
|
||||||
from ....utils.serialization import serialize
|
from ....utils.serialization import serialize
|
||||||
from ..context import ContextData, CallbackCommand
|
from ..context import ContextData, CallbackCommand
|
||||||
@@ -25,6 +26,7 @@ async def string_editor(
|
|||||||
current_value: Any,
|
current_value: Any,
|
||||||
edit_prompt: str,
|
edit_prompt: str,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
|
user: UserBase,
|
||||||
locale_index: int = 0,
|
locale_index: int = 0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -90,6 +92,7 @@ async def string_editor(
|
|||||||
field_descriptor=field_descriptor,
|
field_descriptor=field_descriptor,
|
||||||
callback_data=callback_data,
|
callback_data=callback_data,
|
||||||
state_data=state_data,
|
state_data=state_data,
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
await state.set_data(state_data)
|
await state.set_data(state_data)
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|||||||
|
|
||||||
from ....model.settings import Settings
|
from ....model.settings import Settings
|
||||||
from ....model.descriptors import FieldDescriptor
|
from ....model.descriptors import FieldDescriptor
|
||||||
|
from ....model.user import UserBase
|
||||||
from ..context import ContextData, CallbackCommand, CommandContext
|
from ..context import ContextData, CallbackCommand, CommandContext
|
||||||
from ....utils.navigation import get_navigation_context, pop_navigation_context
|
from ....utils.navigation import get_navigation_context, pop_navigation_context
|
||||||
|
from ....utils.main import build_field_sequence
|
||||||
|
|
||||||
|
|
||||||
async def wrap_editor(
|
async def wrap_editor(
|
||||||
@@ -12,6 +14,7 @@ async def wrap_editor(
|
|||||||
field_descriptor: FieldDescriptor,
|
field_descriptor: FieldDescriptor,
|
||||||
callback_data: ContextData,
|
callback_data: ContextData,
|
||||||
state_data: dict,
|
state_data: dict,
|
||||||
|
user: UserBase,
|
||||||
):
|
):
|
||||||
if callback_data.context in [
|
if callback_data.context in [
|
||||||
CommandContext.ENTITY_CREATE,
|
CommandContext.ENTITY_CREATE,
|
||||||
@@ -36,7 +39,14 @@ async def wrap_editor(
|
|||||||
form = field_descriptor.entity_descriptor.forms.get(
|
form = field_descriptor.entity_descriptor.forms.get(
|
||||||
form_name, field_descriptor.entity_descriptor.default_form
|
form_name, field_descriptor.entity_descriptor.default_form
|
||||||
)
|
)
|
||||||
|
if form.edit_field_sequence:
|
||||||
field_sequence = form.edit_field_sequence
|
field_sequence = form.edit_field_sequence
|
||||||
|
else:
|
||||||
|
field_sequence = build_field_sequence(
|
||||||
|
entity_descriptor=field_descriptor.entity_descriptor,
|
||||||
|
user=user,
|
||||||
|
callback_data=callback_data,
|
||||||
|
)
|
||||||
field_index = (
|
field_index = (
|
||||||
field_sequence.index(field_descriptor.name)
|
field_sequence.index(field_descriptor.name)
|
||||||
if callback_data.context
|
if callback_data.context
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from inspect import iscoroutinefunction
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
@@ -6,7 +7,12 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from ....model.descriptors import FieldEditButton, CommandButton, InlineButton
|
from ....model.descriptors import (
|
||||||
|
FieldEditButton,
|
||||||
|
CommandButton,
|
||||||
|
InlineButton,
|
||||||
|
EntityEventContext,
|
||||||
|
)
|
||||||
from ....model.settings import Settings
|
from ....model.settings import Settings
|
||||||
from ....model.user import UserBase
|
from ....model.user import UserBase
|
||||||
from ....model import EntityPermission
|
from ....model import EntityPermission
|
||||||
@@ -17,6 +23,8 @@ from ....utils.main import (
|
|||||||
get_value_repr,
|
get_value_repr,
|
||||||
get_callable_str,
|
get_callable_str,
|
||||||
get_entity_descriptor,
|
get_entity_descriptor,
|
||||||
|
build_field_sequence,
|
||||||
|
get_user_permissions,
|
||||||
)
|
)
|
||||||
from ..context import ContextData, CallbackCommand, CommandContext
|
from ..context import ContextData, CallbackCommand, CommandContext
|
||||||
from ....utils.navigation import (
|
from ....utils.navigation import (
|
||||||
@@ -89,10 +97,11 @@ async def entity_item(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if form.form_buttons:
|
if form.form_buttons:
|
||||||
|
context = EntityEventContext(db_session=db_session, app=app, message=query)
|
||||||
for edit_buttons_row in form.form_buttons:
|
for edit_buttons_row in form.form_buttons:
|
||||||
btn_row = []
|
btn_row = []
|
||||||
for button in edit_buttons_row:
|
for button in edit_buttons_row:
|
||||||
if button.visibility and not button.visibility(entity_item):
|
if button.visibility and not button.visibility(entity_item, context):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(button, FieldEditButton) and can_edit:
|
if isinstance(button, FieldEditButton) and can_edit:
|
||||||
@@ -157,7 +166,10 @@ async def entity_item(
|
|||||||
if isinstance(button.command, ContextData):
|
if isinstance(button.command, ContextData):
|
||||||
btn_cdata = button.command
|
btn_cdata = button.command
|
||||||
elif callable(button.command):
|
elif callable(button.command):
|
||||||
btn_cdata = button.command(callback_data, entity_item)
|
if iscoroutinefunction(button.command):
|
||||||
|
btn_cdata = await button.command(entity_item, context)
|
||||||
|
else:
|
||||||
|
btn_cdata = button.command(entity_item, context)
|
||||||
elif isinstance(button.command, str):
|
elif isinstance(button.command, str):
|
||||||
btn_cdata = ContextData(
|
btn_cdata = ContextData(
|
||||||
command=CallbackCommand.USER_COMMAND,
|
command=CallbackCommand.USER_COMMAND,
|
||||||
@@ -175,13 +187,26 @@ async def entity_item(
|
|||||||
if isinstance(button.inline_button, InlineKeyboardButton):
|
if isinstance(button.inline_button, InlineKeyboardButton):
|
||||||
btn_row.append(button.inline_button)
|
btn_row.append(button.inline_button)
|
||||||
elif callable(button.inline_button):
|
elif callable(button.inline_button):
|
||||||
btn_row.append(button.inline_button(entity_item))
|
if iscoroutinefunction(button.inline_button):
|
||||||
|
btn_row.append(
|
||||||
|
await button.inline_button(entity_item, context)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
btn_row.append(button.inline_button(entity_item, context))
|
||||||
|
|
||||||
if btn_row:
|
if btn_row:
|
||||||
keyboard_builder.row(*btn_row)
|
keyboard_builder.row(*btn_row)
|
||||||
|
|
||||||
edit_delete_row = []
|
edit_delete_row = []
|
||||||
if can_edit and form.show_edit_button:
|
if can_edit and form.show_edit_button:
|
||||||
|
if form.edit_field_sequence:
|
||||||
|
field_sequence = form.edit_field_sequence
|
||||||
|
else:
|
||||||
|
field_sequence = build_field_sequence(
|
||||||
|
entity_descriptor=entity_descriptor,
|
||||||
|
user=user,
|
||||||
|
callback_data=callback_data,
|
||||||
|
)
|
||||||
edit_delete_row.append(
|
edit_delete_row.append(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=(await Settings.get(Settings.APP_STRINGS_EDIT_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_EDIT_BTN)),
|
||||||
@@ -191,7 +216,7 @@ async def entity_item(
|
|||||||
entity_name=entity_descriptor.name,
|
entity_name=entity_descriptor.name,
|
||||||
entity_id=str(entity_item.id),
|
entity_id=str(entity_item.id),
|
||||||
form_params=callback_data.form_params,
|
form_params=callback_data.form_params,
|
||||||
field_name=form.edit_field_sequence[0],
|
field_name=field_sequence[0],
|
||||||
).pack(),
|
).pack(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -238,8 +263,30 @@ async def entity_item(
|
|||||||
|
|
||||||
item_text = f"<b><u><i>{entity_caption or entity_descriptor.name}:</i></u></b> <b>{entity_item_repr}</b>"
|
item_text = f"<b><u><i>{entity_caption or entity_descriptor.name}:</i></u></b> <b>{entity_item_repr}</b>"
|
||||||
|
|
||||||
|
user_permissions = get_user_permissions(user, entity_descriptor)
|
||||||
|
|
||||||
for field_descriptor in entity_descriptor.fields_descriptors.values():
|
for field_descriptor in entity_descriptor.fields_descriptors.values():
|
||||||
if field_descriptor.is_visible:
|
if (
|
||||||
|
field_descriptor.is_visible is not None
|
||||||
|
and not field_descriptor.is_visible
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
skip = False
|
||||||
|
|
||||||
|
for own_field in entity_descriptor.ownership_fields.items():
|
||||||
|
if (
|
||||||
|
own_field[1].rstrip("_id")
|
||||||
|
== field_descriptor.field_name.rstrip("_id")
|
||||||
|
and own_field[0] in user.roles
|
||||||
|
and EntityPermission.READ_ALL not in user_permissions
|
||||||
|
):
|
||||||
|
skip = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if skip:
|
||||||
|
continue
|
||||||
|
|
||||||
field_caption = await get_callable_str(
|
field_caption = await get_callable_str(
|
||||||
field_descriptor.caption, field_descriptor, entity_item
|
field_descriptor.caption, field_descriptor, entity_item
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,17 +50,21 @@ async def entity_delete_callback(query: CallbackQuery, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if callback_data.data == "yes":
|
if callback_data.data == "yes":
|
||||||
|
|
||||||
entity = await entity_descriptor.type_.remove(
|
entity = await entity_descriptor.type_.remove(
|
||||||
session=db_session, id=int(callback_data.entity_id), commit=True
|
session=db_session, id=int(callback_data.entity_id), commit=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if entity_descriptor.on_deleted:
|
if entity_descriptor.on_deleted:
|
||||||
if iscoroutinefunction(entity_descriptor.on_created):
|
if iscoroutinefunction(entity_descriptor.on_created):
|
||||||
await entity_descriptor.on_deleted(entity, EntityEventContext(db_session=db_session, app=app))
|
await entity_descriptor.on_deleted(
|
||||||
|
entity,
|
||||||
|
EntityEventContext(db_session=db_session, app=app, message=query),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
entity_descriptor.on_deleted(entity, EntityEventContext(db_session=db_session, app=app))
|
entity_descriptor.on_deleted(
|
||||||
|
entity,
|
||||||
|
EntityEventContext(db_session=db_session, app=app, message=query),
|
||||||
|
)
|
||||||
|
|
||||||
await route_callback(message=query, **kwargs)
|
await route_callback(message=query, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from ....utils.main import (
|
|||||||
clear_state,
|
clear_state,
|
||||||
get_entity_descriptor,
|
get_entity_descriptor,
|
||||||
get_callable_str,
|
get_callable_str,
|
||||||
|
build_field_sequence,
|
||||||
)
|
)
|
||||||
from ....utils.serialization import deserialize
|
from ....utils.serialization import deserialize
|
||||||
from ..context import ContextData, CallbackCommand, CommandContext
|
from ..context import ContextData, CallbackCommand, CommandContext
|
||||||
@@ -116,6 +117,14 @@ async def entity_list(
|
|||||||
EntityPermission.CREATE in user_permissions
|
EntityPermission.CREATE in user_permissions
|
||||||
or EntityPermission.CREATE_ALL in user_permissions
|
or EntityPermission.CREATE_ALL in user_permissions
|
||||||
) and form_list.show_add_new_button:
|
) and form_list.show_add_new_button:
|
||||||
|
if form_item.edit_field_sequence:
|
||||||
|
field_sequence = form_item.edit_field_sequence
|
||||||
|
else:
|
||||||
|
field_sequence = build_field_sequence(
|
||||||
|
entity_descriptor=entity_descriptor,
|
||||||
|
user=user,
|
||||||
|
callback_data=callback_data,
|
||||||
|
)
|
||||||
keyboard_builder.row(
|
keyboard_builder.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=(await Settings.get(Settings.APP_STRINGS_ADD_BTN)),
|
text=(await Settings.get(Settings.APP_STRINGS_ADD_BTN)),
|
||||||
@@ -123,7 +132,7 @@ async def entity_list(
|
|||||||
command=CallbackCommand.FIELD_EDITOR,
|
command=CallbackCommand.FIELD_EDITOR,
|
||||||
context=CommandContext.ENTITY_CREATE,
|
context=CommandContext.ENTITY_CREATE,
|
||||||
entity_name=entity_descriptor.name,
|
entity_name=entity_descriptor.name,
|
||||||
field_name=form_item.edit_field_sequence[0],
|
field_name=field_sequence[0],
|
||||||
form_params=form_list.item_form,
|
form_params=form_list.item_form,
|
||||||
).pack(),
|
).pack(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ async def start(message: Message, **kwargs):
|
|||||||
app: QBotApp = kwargs["app"]
|
app: QBotApp = kwargs["app"]
|
||||||
|
|
||||||
if app.start_handler:
|
if app.start_handler:
|
||||||
await app.start_handler(
|
await app.start_handler(default_start_handler, message, **kwargs)
|
||||||
default_start_handler, message, **kwargs
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await default_start_handler(message, **kwargs)
|
await default_start_handler(message, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -24,26 +24,11 @@ class Config(BaseSettings):
|
|||||||
def DATABASE_URI(self) -> str:
|
def DATABASE_URI(self) -> str:
|
||||||
return f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
return f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||||
|
|
||||||
DOMAIN: str = "localhost"
|
|
||||||
|
|
||||||
@computed_field
|
|
||||||
@property
|
|
||||||
def API_DOMAIN(self) -> str:
|
|
||||||
if self.ENVIRONMENT == "local":
|
|
||||||
return self.DOMAIN
|
|
||||||
return f"{self.DOMAIN}"
|
|
||||||
|
|
||||||
@computed_field
|
|
||||||
@property
|
|
||||||
def API_URL(self) -> str:
|
|
||||||
if self.USE_NGROK:
|
|
||||||
return self.NGROK_URL
|
|
||||||
return (
|
|
||||||
f"https://{self.API_DOMAIN}"
|
|
||||||
)
|
|
||||||
|
|
||||||
API_PORT: int = 8000
|
API_PORT: int = 8000
|
||||||
|
|
||||||
|
TELEGRAM_WEBHOOK_URL: str = "http://localhost:8000"
|
||||||
|
TELEGRAM_BOT_SERVER: str = "https://api.telegram.org"
|
||||||
|
TELEGRAM_BOT_SERVER_IS_LOCAL: bool = False
|
||||||
TELEGRAM_BOT_TOKEN: str = "changethis"
|
TELEGRAM_BOT_TOKEN: str = "changethis"
|
||||||
|
|
||||||
ADMIN_TELEGRAM_ID: int
|
ADMIN_TELEGRAM_ID: int
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Callable, Any
|
from typing import Callable, Any
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.client.session.aiohttp import AiohttpSession
|
||||||
|
from aiogram.client.telegram import TelegramAPIServer
|
||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
from aiogram.types import Message, BotCommand as AiogramBotCommand
|
from aiogram.types import Message, BotCommand as AiogramBotCommand
|
||||||
from aiogram.utils.callback_answer import CallbackAnswerMiddleware
|
from aiogram.utils.callback_answer import CallbackAnswerMiddleware
|
||||||
@@ -21,12 +23,12 @@ from .router import Router
|
|||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def default_lifespan(app: "QBotApp"):
|
async def default_lifespan(app: "QBotApp"):
|
||||||
logger.debug("starting qbot app")
|
logger.debug("starting qbot app")
|
||||||
|
|
||||||
if app.lifespan_bot_init:
|
if app.lifespan_bot_init:
|
||||||
|
|
||||||
if app.config.USE_NGROK:
|
if app.config.USE_NGROK:
|
||||||
app.ngrok_init()
|
app.ngrok_init()
|
||||||
|
|
||||||
@@ -35,8 +37,8 @@ async def default_lifespan(app: "QBotApp"):
|
|||||||
logger.info("qbot app started")
|
logger.info("qbot app started")
|
||||||
|
|
||||||
if app.lifespan:
|
if app.lifespan:
|
||||||
async with app.lifespan(app):
|
async with app.lifespan(app) as state:
|
||||||
yield
|
yield state
|
||||||
else:
|
else:
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@@ -88,8 +90,14 @@ class QBotApp[UserType: UserBase](FastAPI):
|
|||||||
self.entity_metadata: EntityMetadata = user_class.entity_metadata
|
self.entity_metadata: EntityMetadata = user_class.entity_metadata
|
||||||
self.config = config
|
self.config = config
|
||||||
self.lifespan = lifespan
|
self.lifespan = lifespan
|
||||||
|
api_server = TelegramAPIServer.from_base(
|
||||||
|
self.config.TELEGRAM_BOT_SERVER,
|
||||||
|
is_local=self.config.TELEGRAM_BOT_SERVER_IS_LOCAL,
|
||||||
|
)
|
||||||
|
session = AiohttpSession(api=api_server)
|
||||||
self.bot = Bot(
|
self.bot = Bot(
|
||||||
token=self.config.TELEGRAM_BOT_TOKEN,
|
token=self.config.TELEGRAM_BOT_TOKEN,
|
||||||
|
session=session,
|
||||||
default=DefaultBotProperties(parse_mode="HTML"),
|
default=DefaultBotProperties(parse_mode="HTML"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,18 +132,16 @@ class QBotApp[UserType: UserBase](FastAPI):
|
|||||||
|
|
||||||
from .api_route.telegram import router as telegram_router
|
from .api_route.telegram import router as telegram_router
|
||||||
|
|
||||||
self.include_router(telegram_router, prefix="/api/telegram", tags=["telegram"])
|
self.include_router(telegram_router, prefix="/telegram", tags=["telegram"])
|
||||||
self.root_router = Router()
|
self.root_router = Router()
|
||||||
self.root_router._commands = self.bot_commands
|
self.root_router._commands = self.bot_commands
|
||||||
self.command = self.root_router.command
|
self.command = self.root_router.command
|
||||||
|
|
||||||
|
|
||||||
def register_routers(self, *routers: Router):
|
def register_routers(self, *routers: Router):
|
||||||
for router in routers:
|
for router in routers:
|
||||||
for command_name, command in router._commands.items():
|
for command_name, command in router._commands.items():
|
||||||
self.bot_commands[command_name] = command
|
self.bot_commands[command_name] = command
|
||||||
|
|
||||||
|
|
||||||
def ngrok_init(self):
|
def ngrok_init(self):
|
||||||
try:
|
try:
|
||||||
from pyngrok import ngrok
|
from pyngrok import ngrok
|
||||||
@@ -151,7 +157,6 @@ class QBotApp[UserType: UserBase](FastAPI):
|
|||||||
)
|
)
|
||||||
self.config.NGROK_URL = tunnel.public_url
|
self.config.NGROK_URL = tunnel.public_url
|
||||||
|
|
||||||
|
|
||||||
def ngrok_stop(self):
|
def ngrok_stop(self):
|
||||||
try:
|
try:
|
||||||
from pyngrok import ngrok
|
from pyngrok import ngrok
|
||||||
@@ -163,10 +168,7 @@ class QBotApp[UserType: UserBase](FastAPI):
|
|||||||
ngrok.disconnect(self.config.NGROK_URL)
|
ngrok.disconnect(self.config.NGROK_URL)
|
||||||
ngrok.kill()
|
ngrok.kill()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def bot_init(self):
|
async def bot_init(self):
|
||||||
|
|
||||||
commands_captions = dict[str, list[tuple[str, str]]]()
|
commands_captions = dict[str, list[tuple[str, str]]]()
|
||||||
|
|
||||||
for command_name, command in self.bot_commands.items():
|
for command_name, command in self.bot_commands.items():
|
||||||
@@ -183,6 +185,13 @@ class QBotApp[UserType: UserBase](FastAPI):
|
|||||||
commands_captions[locale] = []
|
commands_captions[locale] = []
|
||||||
commands_captions[locale].append((command_name, description))
|
commands_captions[locale].append((command_name, description))
|
||||||
|
|
||||||
|
await self.bot.set_webhook(
|
||||||
|
url=f"{self.config.TELEGRAM_WEBHOOK_URL}/telegram/webhook",
|
||||||
|
drop_pending_updates=True,
|
||||||
|
allowed_updates=self.allowed_updates,
|
||||||
|
secret_token=self.bot_auth_token,
|
||||||
|
)
|
||||||
|
|
||||||
for locale, commands in commands_captions.items():
|
for locale, commands in commands_captions.items():
|
||||||
await self.bot.set_my_commands(
|
await self.bot.set_my_commands(
|
||||||
[
|
[
|
||||||
@@ -192,14 +201,7 @@ class QBotApp[UserType: UserBase](FastAPI):
|
|||||||
language_code=None if locale == "default" else locale,
|
language_code=None if locale == "default" else locale,
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.bot.set_webhook(
|
|
||||||
url=f"{self.config.API_URL}/api/telegram/webhook",
|
|
||||||
drop_pending_updates=True,
|
|
||||||
allowed_updates=self.allowed_updates,
|
|
||||||
secret_token=self.bot_auth_token,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def bot_close(self):
|
async def bot_close(self):
|
||||||
await self.bot.delete_webhook()
|
await self.bot.delete_webhook()
|
||||||
|
await self.bot.log_out()
|
||||||
|
await self.bot.close()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import (
|
|||||||
dataclass_transform,
|
dataclass_transform,
|
||||||
)
|
)
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from pydantic_core import PydanticUndefined
|
||||||
from sqlmodel import SQLModel, BigInteger, Field, select, func, column, col
|
from sqlmodel import SQLModel, BigInteger, Field, select, func, column, col
|
||||||
from sqlmodel.main import FieldInfo
|
from sqlmodel.main import FieldInfo
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
@@ -64,7 +65,33 @@ class BotEntityMetaclass(SQLModelMetaclass):
|
|||||||
if attribute_value:
|
if attribute_value:
|
||||||
if isinstance(attribute_value, EntityField):
|
if isinstance(attribute_value, EntityField):
|
||||||
descriptor_kwargs = attribute_value.__dict__.copy()
|
descriptor_kwargs = attribute_value.__dict__.copy()
|
||||||
sm_descriptor = descriptor_kwargs.pop("sm_descriptor", None)
|
sm_descriptor = descriptor_kwargs.pop("sm_descriptor", None) # type: FieldInfo
|
||||||
|
|
||||||
|
if sm_descriptor:
|
||||||
|
if (
|
||||||
|
attribute_value.default is not None
|
||||||
|
and sm_descriptor.default is PydanticUndefined
|
||||||
|
):
|
||||||
|
sm_descriptor.default = attribute_value.default
|
||||||
|
if (
|
||||||
|
attribute_value.default_factory is not None
|
||||||
|
and sm_descriptor.default_factory is PydanticUndefined
|
||||||
|
):
|
||||||
|
sm_descriptor.default_factory = (
|
||||||
|
attribute_value.default_factory
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if (
|
||||||
|
attribute_value.default is not None
|
||||||
|
or attribute_value.default_factory is not None
|
||||||
|
):
|
||||||
|
sm_descriptor = Field()
|
||||||
|
if attribute_value.default is not None:
|
||||||
|
sm_descriptor.default = attribute_value.default
|
||||||
|
if attribute_value.default_factory is not None:
|
||||||
|
sm_descriptor.default_factory = (
|
||||||
|
attribute_value.default_factory
|
||||||
|
)
|
||||||
|
|
||||||
if sm_descriptor:
|
if sm_descriptor:
|
||||||
namespace[annotation] = sm_descriptor
|
namespace[annotation] = sm_descriptor
|
||||||
@@ -157,23 +184,6 @@ class BotEntityMetaclass(SQLModelMetaclass):
|
|||||||
fields_descriptors=bot_fields_descriptors,
|
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():
|
for field_descriptor in bot_fields_descriptors.values():
|
||||||
field_descriptor.entity_descriptor = namespace["bot_entity_descriptor"]
|
field_descriptor.entity_descriptor = namespace["bot_entity_descriptor"]
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class _BaseFieldDescriptor:
|
|||||||
description: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
description: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
||||||
edit_prompt: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
edit_prompt: str | LazyProxy | EntityFieldCaptionCallable | None = None
|
||||||
caption_value: EntityFieldCaptionCallable | None = None
|
caption_value: EntityFieldCaptionCallable | None = None
|
||||||
is_visible: bool = True
|
is_visible: bool | None = None
|
||||||
localizable: bool = False
|
localizable: bool = False
|
||||||
bool_false_value: str | LazyProxy = "no"
|
bool_false_value: str | LazyProxy = "no"
|
||||||
bool_true_value: str | LazyProxy = "yes"
|
bool_true_value: str | LazyProxy = "yes"
|
||||||
@@ -102,6 +102,7 @@ class _BaseFieldDescriptor:
|
|||||||
ep_child_field: str | None = None
|
ep_child_field: str | None = None
|
||||||
dt_type: Literal["date", "datetime"] = "date"
|
dt_type: Literal["date", "datetime"] = "date"
|
||||||
default: Any = None
|
default: Any = None
|
||||||
|
default_factory: Callable[[], Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
@@ -204,6 +205,7 @@ class CommandCallbackContext[UT: UserBase]:
|
|||||||
class EntityEventContext:
|
class EntityEventContext:
|
||||||
db_session: AsyncSession
|
db_session: AsyncSession
|
||||||
app: "QBotApp"
|
app: "QBotApp"
|
||||||
|
message: Message | CallbackQuery | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
|
|||||||
@@ -227,7 +227,9 @@ class Settings(metaclass=SettingsMetaclass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
param.default
|
param.default_factory()
|
||||||
|
if param.default_factory
|
||||||
|
else param.default
|
||||||
if param.default
|
if param.default
|
||||||
else (
|
else (
|
||||||
[]
|
[]
|
||||||
@@ -249,7 +251,6 @@ class Settings(metaclass=SettingsMetaclass):
|
|||||||
session=session,
|
session=session,
|
||||||
type_=setting.type_,
|
type_=setting.type_,
|
||||||
value=db_setting.value,
|
value=db_setting.value,
|
||||||
default=setting.default,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cls._loaded = True
|
cls._loaded = True
|
||||||
|
|||||||
@@ -222,3 +222,40 @@ def get_field_descriptor(
|
|||||||
if entity_descriptor:
|
if entity_descriptor:
|
||||||
return entity_descriptor.fields_descriptors.get(callback_data.field_name)
|
return entity_descriptor.fields_descriptors.get(callback_data.field_name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_field_sequence(
|
||||||
|
entity_descriptor: EntityDescriptor, user: "UserBase", callback_data: ContextData
|
||||||
|
):
|
||||||
|
field_sequence = list[str]()
|
||||||
|
# exclude ownership fields from edit if user has no CREATE_ALL/UPDATE_ALL permission
|
||||||
|
user_permissions = get_user_permissions(user, entity_descriptor)
|
||||||
|
for fd in entity_descriptor.fields_descriptors.values():
|
||||||
|
if not (
|
||||||
|
fd.is_optional
|
||||||
|
or fd.field_name == "id"
|
||||||
|
or fd.field_name[:-3] == "_id"
|
||||||
|
or fd.default is not None
|
||||||
|
):
|
||||||
|
skip = False
|
||||||
|
for own_field in entity_descriptor.ownership_fields.items():
|
||||||
|
if (
|
||||||
|
own_field[1].rstrip("_id") == fd.field_name.rstrip("_id")
|
||||||
|
and own_field[0] in user.roles
|
||||||
|
and (
|
||||||
|
(
|
||||||
|
EntityPermission.CREATE_ALL not in user_permissions
|
||||||
|
and callback_data.context == CommandContext.ENTITY_CREATE
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
EntityPermission.UPDATE_ALL not in user_permissions
|
||||||
|
and callback_data.context == CommandContext.ENTITY_EDIT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
skip = True
|
||||||
|
break
|
||||||
|
if not skip:
|
||||||
|
field_sequence.append(fd.field_name)
|
||||||
|
|
||||||
|
return field_sequence
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from typing import Optional
|
|||||||
|
|
||||||
|
|
||||||
class User(UserBase):
|
class User(UserBase):
|
||||||
|
|
||||||
bot_entity_descriptor = Entity(
|
bot_entity_descriptor = Entity(
|
||||||
icon="👤",
|
icon="👤",
|
||||||
full_name="User",
|
full_name="User",
|
||||||
@@ -56,7 +55,6 @@ class User(UserBase):
|
|||||||
|
|
||||||
|
|
||||||
class Entity(BotEntity):
|
class Entity(BotEntity):
|
||||||
|
|
||||||
bot_entity_descriptor = Entity(
|
bot_entity_descriptor = Entity(
|
||||||
icon="📦",
|
icon="📦",
|
||||||
full_name="Entity",
|
full_name="Entity",
|
||||||
|
|||||||
Reference in New Issue
Block a user