Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3208721d9e | ||
|
|
b7211368cc | ||
|
|
ca374cdea0 |
No files matched your search
@@ -1,86 +0,0 @@
|
|||||||
name: Build Docs
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
changes:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Set job outputs to values from filter step
|
|
||||||
outputs:
|
|
||||||
docs: ${{ steps.filter.outputs.docs }}
|
|
||||||
steps:
|
|
||||||
# - name: Base requirements
|
|
||||||
# run: |
|
|
||||||
# apk update && apk add --no-cache nodejs
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
# For pull requests it's not necessary to checkout the code but for the main branch it is
|
|
||||||
- uses: dorny/paths-filter@v3
|
|
||||||
id: filter
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
docs:
|
|
||||||
- README.md
|
|
||||||
- docs/**
|
|
||||||
- requirements-docs.txt
|
|
||||||
- pyproject.toml
|
|
||||||
- mkdocs.yml
|
|
||||||
- Dockerfile.docs-site
|
|
||||||
- docker-compose.docs-site.yml
|
|
||||||
- .gitea/workflows/build-docs.yml
|
|
||||||
- .gitea/workflows/deploy-docs.yml
|
|
||||||
|
|
||||||
build-docs:
|
|
||||||
needs:
|
|
||||||
- changes
|
|
||||||
if: ${{ needs.changes.outputs.docs == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: catthehacker/ubuntu:act-22.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.13.2"
|
|
||||||
- name: Setup uv
|
|
||||||
uses: astral-sh/setup-uv@v5
|
|
||||||
with:
|
|
||||||
version: "0.6.3"
|
|
||||||
enable-cache: true
|
|
||||||
cache-dependency-glob: |
|
|
||||||
requirements**.txt
|
|
||||||
pyproject.toml
|
|
||||||
- name: Install docs extras
|
|
||||||
run: |
|
|
||||||
uv venv
|
|
||||||
uv pip install mkdocs-material
|
|
||||||
- name: Build Docs
|
|
||||||
run: .venv/bin/mkdocs build
|
|
||||||
- name: Build Docker Image
|
|
||||||
run: |
|
|
||||||
docker build -f Dockerfile.docs-site -t ${{ vars.STACK_NAME }} .
|
|
||||||
docker tag ${{ vars.STACK_NAME }}:latest registry.botforge.biz/${{ vars.STACK_NAME }}
|
|
||||||
docker login registry.botforge.biz -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASSWORD }}
|
|
||||||
docker push registry.botforge.biz/${{ vars.STACK_NAME }}
|
|
||||||
|
|
||||||
deploy-docs:
|
|
||||||
needs:
|
|
||||||
- build-docs
|
|
||||||
runs-on: staging
|
|
||||||
env:
|
|
||||||
STACK_NAME: ${{ vars.STACK_NAME }}
|
|
||||||
DOMAIN: ${{ vars.DOMAIN }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Copy to working directory
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/${{ vars.STACK_NAME }}
|
|
||||||
cp docker-compose.docs-site.yml ~/${{ vars.STACK_NAME }}/docker-compose.yml
|
|
||||||
- name: Start Docs Site
|
|
||||||
run: |
|
|
||||||
cd ~/${{ vars.STACK_NAME }}
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
+3
-7
@@ -1,10 +1,6 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
|
/backend/.venv
|
||||||
.env
|
.env
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
.DS_Store
|
/backend/logs
|
||||||
.venv
|
.DS_Store
|
||||||
.python-version
|
|
||||||
uv.lock
|
|
||||||
dist
|
|
||||||
*.egg-info
|
|
||||||
site
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.13
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
FROM nginx
|
|
||||||
|
|
||||||
WORKDIR /usr/share/nginx/html
|
|
||||||
|
|
||||||
RUN rm -rf ./*
|
|
||||||
COPY ./site/ ./
|
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 Alexander Kalinovsky
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<p align="center">
|
|
||||||
<a href="https://qbot.botforge.biz"><img src="https://qbot.botforge.biz/img/qbot.svg" alt="QBot"></a>
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
|
||||||
<em>Telegram Bots Rapid Application Development (RAD) Framework.</em>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
**QBot** is a library for fast development of Telegram bots and mini-apps following the **RAD (Rapid Application Development)** principle in a **declarative style**.
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
- **Automatic CRUD Interface Generation** – Manage objects effortlessly without manual UI coding.
|
|
||||||
- **Built-in Field Editors** – Includes text inputs, date/time pickers, boolean switches, enums, and entity selectors.
|
|
||||||
- **Advanced Pagination & Filtering** – Easily organize and navigate large datasets.
|
|
||||||
- **Authentication & Authorization** – Role-based access control for secure interactions.
|
|
||||||
- **Context Preservation** – Store navigation stacks and user interaction states in the database.
|
|
||||||
- **Internationalization Support** – Localizable UI and string fields for multilingual bots.
|
|
||||||
|
|
||||||
**QBot** powered by **[FastAPI](https://fastapi.tiangolo.com)**, **[SQLModel](https://sqlmodel.tiangolo.com)** & **[aiogram](https://aiogram.dev)** – Leverage the full capabilities of these frameworks for high performance and flexibility.
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
- **Faster Development** – Automate repetitive tasks and build bots in record time.
|
|
||||||
- **Highly Modular** – Easily extend and customize functionality.
|
|
||||||
- **Less Code Duplication** – Focus on core features while QBot handles the rest.
|
|
||||||
- **Enterprise-Grade Structure** – Scalable, maintainable, and optimized for real-world usage.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
```python
|
|
||||||
class AppEntity(BotEntity):
|
|
||||||
"""
|
|
||||||
BotEntity - business entity. Based on SQLModel, which provides ORM functionality,
|
|
||||||
basic CRUD functions and additional metadata, describing entities view and behaviour
|
|
||||||
"""
|
|
||||||
bot_entity_descriptor = Entity( # metadata attribute
|
|
||||||
name = "bot_entity", # Entity - descriptor for entity metadata
|
|
||||||
...
|
|
||||||
)
|
|
||||||
|
|
||||||
name: str # entity field with default sqlmodel's FieldInfo descriptor
|
|
||||||
# and default qbot's field descriptor
|
|
||||||
|
|
||||||
description: str | None = Field( # field with sqlmodel's descriptor
|
|
||||||
sa_type = String, index = True) # and default qbot's descriptor
|
|
||||||
|
|
||||||
age: int = EntityField( # field with qbot's descriptor
|
|
||||||
caption = "Age",
|
|
||||||
)
|
|
||||||
|
|
||||||
user_id: int | None = EntityField( # using both descriptors
|
|
||||||
sm_descriptor=Field(
|
|
||||||
sa_type=BigInteger,
|
|
||||||
foreign_key="user.id",
|
|
||||||
ondelete="RESTRICT",
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
is_visible=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
user: Optional[User] = EntityField( # using Relationship as sqlmodel descriptor
|
|
||||||
sm_descriptor=Relationship(
|
|
||||||
back_populates="entities",
|
|
||||||
sa_relationship_kwargs={
|
|
||||||
"lazy": "selectin",
|
|
||||||
"foreign_keys": "Entity.user_id",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
caption="User",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
app = QBotApp() # bot application based on FastAPI application
|
|
||||||
# providing Telegram API webhook handler
|
|
||||||
|
|
||||||
@app.command( # decorator for bot commands definition
|
|
||||||
name="menu",
|
|
||||||
caption="Main menu",
|
|
||||||
show_in_bot_commands=True, # shows command in bot's main menu
|
|
||||||
clear_navigation=True, # resets navigation stack between bot dialogues
|
|
||||||
)
|
|
||||||
async def menu(context: CommandCallbackContext):
|
|
||||||
context.message_text = "Main menu" # message text
|
|
||||||
context.keyboard_builder.row( # and buttons will be shown to user
|
|
||||||
InlineKeyboardButton(
|
|
||||||
text="Entities",
|
|
||||||
callback_data=ContextData( # callback query dataclass representing
|
|
||||||
command=CallbackCommand.ENTITY_LIST, # navigation context and its parameters
|
|
||||||
entity_name="bot_entity"
|
|
||||||
).pack(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
## Result
|
|
||||||
|
|
||||||
<iframe width="100%" height="691" src="https://www.youtube.com/embed/ptTnoppkYfM" title="QBot Framework – The Open-Source RAD Tool for Telegram Bots" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
|
||||||
|
|
||||||
Here you can see the result - [YouTube Video with Bot](https://www.youtube.com/shorts/ptTnoppkYfM)
|
|
||||||
|
|
||||||
|
|
||||||
@@ -12,10 +12,8 @@ from .model.descriptors import (
|
|||||||
EntityField as EntityField,
|
EntityField as EntityField,
|
||||||
EntityForm as EntityForm,
|
EntityForm as EntityForm,
|
||||||
EntityList as EntityList,
|
EntityList as EntityList,
|
||||||
Filter as Filter,
|
|
||||||
EntityPermission as EntityPermission,
|
EntityPermission as EntityPermission,
|
||||||
CommandCallbackContext as CommandCallbackContext,
|
CommandCallbackContext as CommandCallbackContext,
|
||||||
EntityEventContext as EntityEventContext,
|
|
||||||
CommandButton as CommandButton,
|
CommandButton as CommandButton,
|
||||||
FieldEditButton as FieldEditButton,
|
FieldEditButton as FieldEditButton,
|
||||||
InlineButton as InlineButton,
|
InlineButton as InlineButton,
|
||||||
@@ -10,6 +10,7 @@ from logging import getLogger
|
|||||||
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
logger.setLevel("DEBUG")
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@@ -31,11 +32,7 @@ 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,
|
app.bot, update, db_session=db_session, app=app
|
||||||
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)
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -6,7 +6,6 @@ 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
|
||||||
@@ -21,7 +20,6 @@ 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()
|
||||||
@@ -72,7 +70,6 @@ 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"]
|
||||||
File renamed without changes.
@@ -8,7 +8,6 @@ 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
|
||||||
@@ -50,7 +49,6 @@ 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,
|
||||||
):
|
):
|
||||||
@@ -164,7 +162,6 @@ 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)
|
||||||
@@ -182,7 +179,6 @@ 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,
|
||||||
):
|
):
|
||||||
@@ -277,7 +273,6 @@ 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)
|
||||||
@@ -297,7 +292,6 @@ 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")
|
||||||
@@ -371,7 +365,6 @@ 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,7 +289,6 @@ 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,8 +107,6 @@ 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
|
||||||
)
|
)
|
||||||
+5
-52
@@ -1,4 +1,3 @@
|
|||||||
from inspect import iscoroutinefunction
|
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.types import Message, CallbackQuery
|
from aiogram.types import Message, CallbackQuery
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
@@ -13,7 +12,7 @@ from ..user_handlers.main import cammand_handler
|
|||||||
from ....model import EntityPermission
|
from ....model import EntityPermission
|
||||||
from ....model.user import UserBase
|
from ....model.user import UserBase
|
||||||
from ....model.settings import Settings
|
from ....model.settings import Settings
|
||||||
from ....model.descriptors import EntityEventContext, FieldDescriptor
|
from ....model.descriptors import FieldDescriptor
|
||||||
from ....model.language import LanguageBase
|
from ....model.language import LanguageBase
|
||||||
from ....auth import authorize_command
|
from ....auth import authorize_command
|
||||||
from ....utils.main import (
|
from ....utils.main import (
|
||||||
@@ -22,7 +21,6 @@ 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
|
||||||
@@ -169,15 +167,7 @@ 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
|
||||||
@@ -260,14 +250,10 @@ 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 (
|
# if role in entity_descriptor.ownership_fields and not EntityPermission.CREATE_ALL in user_permissions:
|
||||||
role in entity_descriptor.ownership_fields
|
# entity_data[entity_descriptor.ownership_fields[role]] = user.id
|
||||||
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(
|
||||||
@@ -295,22 +281,6 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
|
|||||||
commit=True,
|
commit=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if entity_descriptor.on_created:
|
|
||||||
if iscoroutinefunction(entity_descriptor.on_created):
|
|
||||||
await entity_descriptor.on_created(
|
|
||||||
new_entity,
|
|
||||||
EntityEventContext(
|
|
||||||
db_session=db_session, app=app, message=message
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
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]
|
||||||
if callback_data.form_params
|
if callback_data.form_params
|
||||||
@@ -355,23 +325,6 @@ async def process_field_edit_callback(message: Message | CallbackQuery, **kwargs
|
|||||||
setattr(entity, key, value)
|
setattr(entity, key, value)
|
||||||
|
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
await db_session.refresh(entity)
|
|
||||||
|
|
||||||
if entity_descriptor.on_updated:
|
|
||||||
if iscoroutinefunction(entity_descriptor.on_updated):
|
|
||||||
await entity_descriptor.on_updated(
|
|
||||||
entity,
|
|
||||||
EntityEventContext(
|
|
||||||
db_session=db_session, app=app, message=message
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
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,7 +8,6 @@ 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
|
||||||
@@ -26,7 +25,6 @@ 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,
|
||||||
):
|
):
|
||||||
@@ -92,7 +90,6 @@ 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,10 +3,8 @@ 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(
|
||||||
@@ -14,7 +12,6 @@ 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,
|
||||||
@@ -39,14 +36,7 @@ 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,4 +1,3 @@
|
|||||||
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
|
||||||
@@ -7,12 +6,7 @@ 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 (
|
from ....model.descriptors import FieldEditButton, CommandButton, InlineButton
|
||||||
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
|
||||||
@@ -23,8 +17,6 @@ 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 (
|
||||||
@@ -97,11 +89,10 @@ 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, context):
|
if button.visibility and not button.visibility(entity_item):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(button, FieldEditButton) and can_edit:
|
if isinstance(button, FieldEditButton) and can_edit:
|
||||||
@@ -166,10 +157,7 @@ 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):
|
||||||
if iscoroutinefunction(button.command):
|
btn_cdata = button.command(callback_data, entity_item)
|
||||||
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,
|
||||||
@@ -187,26 +175,13 @@ 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):
|
||||||
if iscoroutinefunction(button.inline_button):
|
btn_row.append(button.inline_button(entity_item))
|
||||||
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)),
|
||||||
@@ -216,7 +191,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=field_sequence[0],
|
field_name=form.edit_field_sequence[0],
|
||||||
).pack(),
|
).pack(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -263,47 +238,25 @@ 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 (
|
if field_descriptor.is_visible:
|
||||||
field_descriptor.is_visible is not None
|
field_caption = await get_callable_str(
|
||||||
and not field_descriptor.is_visible
|
field_descriptor.caption, field_descriptor, entity_item
|
||||||
):
|
|
||||||
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_descriptor.caption, field_descriptor, entity_item
|
|
||||||
)
|
|
||||||
if field_descriptor.caption_value:
|
|
||||||
value = await get_callable_str(
|
|
||||||
field_descriptor.caption_value,
|
|
||||||
field_descriptor,
|
|
||||||
entity_item,
|
|
||||||
getattr(entity_item, field_descriptor.field_name),
|
|
||||||
)
|
)
|
||||||
else:
|
if field_descriptor.caption_value:
|
||||||
value = await get_value_repr(
|
value = await get_callable_str(
|
||||||
value=getattr(entity_item, field_descriptor.field_name),
|
field_descriptor.caption_value,
|
||||||
field_descriptor=field_descriptor,
|
field_descriptor,
|
||||||
locale=user.lang,
|
entity_item,
|
||||||
)
|
getattr(entity_item, field_descriptor.field_name),
|
||||||
item_text += f"\n{field_caption or field_descriptor.name}:{f' <b>{value}</b>' if value else ''}"
|
)
|
||||||
|
else:
|
||||||
|
value = await get_value_repr(
|
||||||
|
value=getattr(entity_item, field_descriptor.field_name),
|
||||||
|
field_descriptor=field_descriptor,
|
||||||
|
locale=user.lang,
|
||||||
|
)
|
||||||
|
item_text += f"\n{field_caption or field_descriptor.name}:{f' <b>{value}</b>' if value else ''}"
|
||||||
|
|
||||||
context = pop_navigation_context(navigation_stack)
|
context = pop_navigation_context(navigation_stack)
|
||||||
if context:
|
if context:
|
||||||
+1
-16
@@ -1,4 +1,3 @@
|
|||||||
from inspect import iscoroutinefunction
|
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
||||||
@@ -6,8 +5,6 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from qbot.model.descriptors import EntityEventContext
|
|
||||||
|
|
||||||
from ..context import ContextData, CallbackCommand
|
from ..context import ContextData, CallbackCommand
|
||||||
from ....model.user import UserBase
|
from ....model.user import UserBase
|
||||||
from ....model.settings import Settings
|
from ....model.settings import Settings
|
||||||
@@ -50,22 +47,10 @@ async def entity_delete_callback(query: CallbackQuery, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if callback_data.data == "yes":
|
if callback_data.data == "yes":
|
||||||
entity = await entity_descriptor.type_.remove(
|
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 iscoroutinefunction(entity_descriptor.on_created):
|
|
||||||
await entity_descriptor.on_deleted(
|
|
||||||
entity,
|
|
||||||
EntityEventContext(db_session=db_session, app=app, message=query),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
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)
|
||||||
|
|
||||||
elif not callback_data.data:
|
elif not callback_data.data:
|
||||||
@@ -18,7 +18,6 @@ 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
|
||||||
@@ -117,14 +116,6 @@ 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)),
|
||||||
@@ -132,7 +123,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=field_sequence[0],
|
field_name=form_item.edit_field_sequence[0],
|
||||||
form_params=form_list.item_form,
|
form_params=form_list.item_form,
|
||||||
).pack(),
|
).pack(),
|
||||||
)
|
)
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -21,7 +21,9 @@ 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(default_start_handler, message, **kwargs)
|
await app.start_handler(
|
||||||
|
default_start_handler, message, **kwargs
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await default_start_handler(message, **kwargs)
|
await default_start_handler(message, **kwargs)
|
||||||
|
|
||||||
File renamed without changes.
@@ -24,11 +24,26 @@ 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"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}"
|
||||||
|
)
|
||||||
|
|
||||||
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
|
||||||
File renamed without changes.
@@ -1,20 +0,0 @@
|
|||||||
services:
|
|
||||||
qbot-docs-site:
|
|
||||||
image: 'registry.botforge.biz/${STACK_NAME?Variable not set}:${TAG-latest}'
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- traefik-public
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.docker.network=traefik-public
|
|
||||||
- traefik.constraint-label=traefik-public
|
|
||||||
- traefik.http.services.${STACK_NAME?Variable not set}.loadbalancer.server.port=80
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}.rule=Host(`${DOMAIN?Variable not set}`)
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}.entrypoints=websecure
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}.tls=true
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}.tls.certresolver=le
|
|
||||||
- traefik.http.routers.${STACK_NAME?Variable not set}.service=${STACK_NAME?Variable not set}
|
|
||||||
|
|
||||||
networks:
|
|
||||||
traefik-public:
|
|
||||||
external: true
|
|
||||||
Whitespace-only changes.
@@ -1,65 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="1500"
|
|
||||||
height="500"
|
|
||||||
viewBox="0 0 1500 500"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
xml:space="preserve"
|
|
||||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
|
||||||
sodipodi:docname="qbot.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="0.52452521"
|
|
||||||
inkscape:cx="704.4466"
|
|
||||||
inkscape:cy="297.41182"
|
|
||||||
inkscape:window-width="1608"
|
|
||||||
inkscape:window-height="1007"
|
|
||||||
inkscape:window-x="8"
|
|
||||||
inkscape:window-y="52"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="layer1" /><defs
|
|
||||||
id="defs1"><rect
|
|
||||||
x="80.81143"
|
|
||||||
y="115.09507"
|
|
||||||
width="901.16989"
|
|
||||||
height="411.40365"
|
|
||||||
id="rect3" /><rect
|
|
||||||
x="90.606755"
|
|
||||||
y="102.85091"
|
|
||||||
width="416.30131"
|
|
||||||
height="306.1039"
|
|
||||||
id="rect2" /></defs><g
|
|
||||||
inkscape:label="Слой 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"><g
|
|
||||||
id="g18"
|
|
||||||
transform="translate(118.93657)"><path
|
|
||||||
style="font-size:266.667px;font-family:'Arial Rounded MT Bold';-inkscape-font-specification:'Arial Rounded MT Bold, Normal';fill:#404040"
|
|
||||||
d="m 685.86904,314.58341 q 8.46355,5.72918 18.4896,11.19793 10.02606,5.33854 13.28127,8.59377 3.25521,3.125 3.25521,8.98437 0,4.16668 -3.90625,8.33335 -3.77605,4.16668 -9.24481,4.16668 -4.42708,0 -10.8073,-2.8646 -6.25001,-2.86459 -14.84377,-8.33334 -8.46355,-5.46876 -18.61981,-12.76044 -18.88023,9.63543 -46.35423,9.63543 -22.26565,0 -39.974,-7.03126 -17.57815,-7.16146 -29.55733,-20.44273 -11.97918,-13.41147 -18.09898,-31.77087 -5.98959,-18.3594 -5.98959,-39.97401 0,-22.00523 6.25001,-40.36463 6.38021,-18.3594 18.35939,-31.25004 11.97918,-12.89064 29.16671,-19.66148 17.18752,-6.90105 39.06254,-6.90105 29.68754,0 50.91152,12.10939 21.3542,11.97918 32.29171,34.24483 10.93751,22.13545 10.93751,52.0834 0,45.44276 -24.6094,72.0053 z m -30.33858,-21.09377 q 8.07293,-9.2448 11.84897,-21.87503 3.90626,-12.63022 3.90626,-29.29691 0,-20.96358 -6.77084,-36.32817 -6.77085,-15.3646 -19.40107,-23.17711 -12.50002,-7.94273 -28.77608,-7.94273 -11.58855,0 -21.4844,4.42709 -9.76563,4.29688 -16.9271,12.63022 -7.03126,8.33335 -11.19793,21.3542 -4.03647,12.89064 -4.03647,29.0365 0,32.94274 15.36461,50.78131 15.3646,17.70836 38.80213,17.70836 9.63543,0 19.79169,-4.03647 -6.1198,-4.55729 -15.3646,-9.11459 -9.1146,-4.5573 -12.50002,-7.03126 -3.38542,-2.47396 -3.38542,-7.03126 0,-3.90625 3.25521,-6.90105 3.25521,-2.99479 7.16147,-2.99479 11.84897,0 39.71359,19.79169 z m 169.53146,44.79172 h -58.3334 q -12.63023,0 -18.09898,-5.59898 -5.33855,-5.72917 -5.33855,-18.09897 V 171.09366 q 0,-12.63023 5.46876,-18.09898 5.59896,-5.59897 17.96877,-5.59897 h 61.84903 q 13.67189,0 23.69795,1.69271 10.02605,1.69271 17.96877,6.51043 6.77084,4.03646 11.97918,10.28647 5.20834,6.1198 7.94272,13.67189 2.73438,7.42188 2.73438,15.75523 0,28.64586 -28.64587,41.92713 37.63025,11.97918 37.63025,46.61464 0,16.01564 -8.20313,28.90629 -8.20314,12.76043 -22.13545,18.88023 -8.72396,3.64583 -20.0521,5.20834 -11.32814,1.43229 -26.43233,1.43229 z m -2.86458,-84.76574 h -40.23443 v 55.72924 h 41.53651 q 39.19276,0 39.19276,-28.25524 0,-14.45315 -10.15627,-20.96357 -10.15626,-6.51043 -30.33857,-6.51043 z m -40.23443,-77.08343 v 49.34902 h 35.41671 q 14.45314,0 22.26565,-2.73438 7.94272,-2.73437 12.10939,-10.41667 3.25522,-5.46876 3.25522,-12.2396 0,-14.45315 -10.28647,-19.14065 -10.28648,-4.81772 -31.38025,-4.81772 z m 283.98469,92.83866 q 0,15.88544 -4.9479,29.29691 -4.9479,13.41148 -14.3229,23.0469 -9.375,9.63543 -22.3959,14.84377 -13.0208,5.07813 -29.29688,5.07813 -16.14586,0 -29.0365,-5.20834 -12.89064,-5.20834 -22.39586,-14.84377 -9.37501,-9.76563 -14.32293,-22.91669 -4.81772,-13.28127 -4.81772,-29.29691 0,-16.14585 4.94793,-29.55733 4.94792,-13.41147 14.19272,-22.91669 9.2448,-9.50522 22.39586,-14.58335 13.15106,-5.20834 29.0365,-5.20834 16.14588,0 29.29688,5.20834 13.1511,5.20834 22.5261,14.84376 9.375,9.63543 14.1927,22.9167 4.9479,13.28126 4.9479,29.29691 z m -35.6771,0 q 0,-21.74482 -9.6354,-33.85421 -9.5052,-12.10939 -25.65108,-12.10939 -10.41668,0 -18.3594,5.46876 -7.94272,5.33855 -12.2396,15.88544 -4.29688,10.54689 -4.29688,24.60941 0,13.93231 4.16667,24.34899 4.29688,10.41668 12.10939,16.01564 7.94272,5.46876 18.61982,5.46876 16.14588,0 25.65108,-12.10939 9.6354,-12.2396 9.6354,-33.72401 z m 65.6251,-69.14071 h 3.9062 v -21.35419 q 0,-8.59376 0.3906,-13.41148 0.5209,-4.94792 2.6042,-8.46355 2.0833,-3.64584 5.9896,-5.85938 3.9063,-2.34375 8.724,-2.34375 6.7708,0 12.2396,5.07813 3.6458,3.38542 4.5573,8.33334 1.0416,4.81771 1.0416,13.8021 v 24.21878 h 13.0209 q 7.5521,0 11.4583,3.64584 4.0365,3.51563 4.0365,9.11459 0,7.16147 -5.7292,10.02606 -5.5989,2.86458 -16.1458,2.86458 h -6.6407 v 65.36467 q 0,8.33334 0.5209,12.89064 0.651,4.42709 3.125,7.29167 2.6041,2.73438 8.3333,2.73438 3.125,0 8.4636,-1.04167 5.3385,-1.17187 8.3333,-1.17187 4.2969,0 7.6823,3.51563 3.5156,3.38542 3.5156,8.46355 0,8.59376 -9.375,13.15106 -9.375,4.55729 -26.9531,4.55729 -16.6667,0 -25.2605,-5.59896 -8.5937,-5.59897 -11.3281,-15.49481 -2.6042,-9.89585 -2.6042,-26.43233 v -68.22924 h -4.6875 q -7.6823,0 -11.7188,-3.64584 -4.0364,-3.64583 -4.0364,-9.2448 0,-5.59896 4.1667,-9.11459 4.2968,-3.64584 12.3698,-3.64585 z"
|
|
||||||
id="text17"
|
|
||||||
aria-label="QBot" /><g
|
|
||||||
id="g17"
|
|
||||||
transform="matrix(1.195598,0,0,1.195598,-52.1047,-71.699942)"><path
|
|
||||||
id="path17"
|
|
||||||
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#a587c9;fill-opacity:1"
|
|
||||||
d="m 272.44727,142.82422 c -6.05914,0.0153 -11.03743,2.03697 -14.95508,6.0293 -3.98025,4.51096 -6.09057,11.66026 -6.47657,21.24218 v 3.72461 c -0.0471,-0.0706 -0.0973,-0.13474 -0.14453,-0.20508 v 52.01758 18.20703 57.11524 c 0.0477,-0.0476 0.0968,-0.0871 0.14453,-0.13477 v 64.16602 c 2e-5,8.38844 1.13695,15.11813 3.29102,20.36523 0.73739,1.28016 1.58937,2.4395 2.56445,3.46875 3.62458,3.92663 8.21734,6.0553 13.75782,6.42383 0.5993,0.0286 1.18235,0.0711 1.8125,0.0723 6.05823,-0.0157 11.0359,-2.03742 14.95312,-6.0293 3.98025,-4.51096 6.09057,-11.66026 6.47656,-21.24219 v -3.72461 c 0.0471,0.0706 0.0974,0.13474 0.14453,0.20508 v -52.01953 -18.20508 -57.11523 c -0.0477,0.0476 -0.0968,0.0871 -0.14453,0.13476 V 173.1543 c 0,-8.38703 -1.13767,-15.11654 -3.29101,-20.36328 -0.73758,-1.28076 -1.58899,-2.44105 -2.56446,-3.47071 -3.6877,-3.99502 -8.37983,-6.1256 -14.05078,-6.4375 -0.50595,-0.0202 -0.99001,-0.0576 -1.51757,-0.0586 z" /><path
|
|
||||||
d="m 250.87109,300.95508 v -57.11524 c -0.79426,8.72199 -2.70097,16.33161 -5.73632,22.8125 -4.1111,8.77777 -9.55427,15.33464 -16.33204,19.66797 -6.77777,4.22221 -14.22287,6.33203 -22.33398,6.33203 -7.44443,0 -14.38954,-2.10982 -20.83398,-6.33203 -6.44444,-4.33333 -11.66602,-10.8902 -15.66602,-19.66797 -3.88888,-8.88888 -5.83398,-19.4438 -5.83398,-31.66601 0,-18.55553 4.05686,-32.88889 12.16796,-43 8.1111,-10.1111 18.27779,-15.16602 30.5,-15.16602 6,0 11.66668,1.22158 17,3.66602 5.33333,2.33333 10.05492,5.94509 14.16602,10.83398 4.11111,4.77777 7.33269,10.83269 9.66602,18.16602 1.65444,4.96334 2.70164,10.36877 3.23632,16.14648 v -52.01758 c -6.86066,-10.22654 -15.08687,-17.82908 -24.6875,-22.79492 -9.66665,-5.11111 -20.61177,-7.66601 -32.83398,-7.66601 -14.44443,0 -27.61113,3.60982 -39.5,10.83203 -11.77777,7.22221 -20.99936,17.77909 -27.66602,31.66797 -6.66666,13.77776 -10,30.33269 -10,49.66601 0,18.88887 3.27844,35.33204 9.83399,49.33203 6.66666,13.88887 15.77648,24.55556 27.33203,32 11.66665,7.44444 24.55687,11.16797 38.66797,11.16797 11.55555,0 22.05361,-2.27843 31.49804,-6.83398 9.50365,-4.64131 18.62118,-11.32337 27.35547,-20.03125 z"
|
|
||||||
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#fabf9c;fill-opacity:1"
|
|
||||||
id="path13" /><path
|
|
||||||
d="m 294.01562,237.18555 v 57.11523 c 0.79416,-8.72281 2.70075,-16.33307 5.73633,-22.81445 4.1111,-8.77777 9.55426,-15.33269 16.33203,-19.66602 6.77777,-4.22221 14.22288,-6.33398 22.33399,-6.33398 7.44443,0 14.38954,2.11177 20.83398,6.33398 6.44444,4.33333 11.66602,10.88825 15.66602,19.66602 3.88888,8.88888 5.83398,19.4438 5.83398,31.66601 0,18.55553 -4.05687,32.88889 -12.16797,43 -8.1111,10.1111 -18.27779,15.16797 -30.5,15.16797 -6,0 -11.66668,-1.22353 -17,-3.66797 -5.33333,-2.33333 -10.05491,-5.94314 -14.16601,-10.83203 -4.11111,-4.77777 -7.33269,-10.83464 -9.66602,-18.16797 -1.6545,-4.96351 -2.70166,-10.36855 -3.23633,-16.14648 v 52.01953 c 6.86067,10.22655 15.08688,17.82909 24.6875,22.79492 9.66665,5.11111 20.61178,7.66602 32.83399,7.66602 14.44443,0 27.61113,-3.61178 39.5,-10.83399 11.77777,-7.22221 20.99936,-17.77713 27.66601,-31.66601 6.66667,-13.77776 10,-30.3327 10,-49.66602 10e-6,-18.88887 -3.27648,-35.33399 -9.83203,-49.33398 -6.66666,-13.88887 -15.77843,-24.55556 -27.33398,-32 -11.66665,-7.44444 -24.55492,-11.16602 -38.66602,-11.16602 -11.55555,0 -22.05557,2.27844 -31.5,6.83399 -9.50364,4.64131 -18.62118,11.32337 -27.35547,20.03125 z"
|
|
||||||
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#a7a7fa;fill-opacity:1"
|
|
||||||
id="path15" /></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,59 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="500"
|
|
||||||
height="500"
|
|
||||||
viewBox="0 0 500 500"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
xml:space="preserve"
|
|
||||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
|
||||||
sodipodi:docname="qbot_1_1.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="0.52452521"
|
|
||||||
inkscape:cx="346.02722"
|
|
||||||
inkscape:cy="171.58374"
|
|
||||||
inkscape:window-width="1608"
|
|
||||||
inkscape:window-height="1007"
|
|
||||||
inkscape:window-x="8"
|
|
||||||
inkscape:window-y="52"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="layer1" /><defs
|
|
||||||
id="defs1"><rect
|
|
||||||
x="80.811432"
|
|
||||||
y="115.09507"
|
|
||||||
width="901.16986"
|
|
||||||
height="411.40366"
|
|
||||||
id="rect3" /><rect
|
|
||||||
x="90.606758"
|
|
||||||
y="102.85091"
|
|
||||||
width="416.3013"
|
|
||||||
height="306.10391"
|
|
||||||
id="rect2" /></defs><g
|
|
||||||
inkscape:label="Слой 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"><g
|
|
||||||
id="g17"
|
|
||||||
transform="matrix(1.3786092,0,0,1.3786092,-125.59292,-120.94283)"><path
|
|
||||||
id="path17"
|
|
||||||
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#a587c9;fill-opacity:1"
|
|
||||||
d="m 272.44727,142.82422 c -6.05914,0.0153 -11.03743,2.03697 -14.95508,6.0293 -3.98025,4.51096 -6.09057,11.66026 -6.47657,21.24218 v 3.72461 c -0.0471,-0.0706 -0.0973,-0.13474 -0.14453,-0.20508 v 52.01758 18.20703 57.11524 c 0.0477,-0.0476 0.0968,-0.0871 0.14453,-0.13477 v 64.16602 c 2e-5,8.38844 1.13695,15.11813 3.29102,20.36523 0.73739,1.28016 1.58937,2.4395 2.56445,3.46875 3.62458,3.92663 8.21734,6.0553 13.75782,6.42383 0.5993,0.0286 1.18235,0.0711 1.8125,0.0723 6.05823,-0.0157 11.0359,-2.03742 14.95312,-6.0293 3.98025,-4.51096 6.09057,-11.66026 6.47656,-21.24219 v -3.72461 c 0.0471,0.0706 0.0974,0.13474 0.14453,0.20508 v -52.01953 -18.20508 -57.11523 c -0.0477,0.0476 -0.0968,0.0871 -0.14453,0.13476 V 173.1543 c 0,-8.38703 -1.13767,-15.11654 -3.29101,-20.36328 -0.73758,-1.28076 -1.58899,-2.44105 -2.56446,-3.47071 -3.6877,-3.99502 -8.37983,-6.1256 -14.05078,-6.4375 -0.50595,-0.0202 -0.99001,-0.0576 -1.51757,-0.0586 z" /><path
|
|
||||||
d="m 250.87109,300.95508 v -57.11524 c -0.79426,8.72199 -2.70097,16.33161 -5.73632,22.8125 -4.1111,8.77777 -9.55427,15.33464 -16.33204,19.66797 -6.77777,4.22221 -14.22287,6.33203 -22.33398,6.33203 -7.44443,0 -14.38954,-2.10982 -20.83398,-6.33203 -6.44444,-4.33333 -11.66602,-10.8902 -15.66602,-19.66797 -3.88888,-8.88888 -5.83398,-19.4438 -5.83398,-31.66601 0,-18.55553 4.05686,-32.88889 12.16796,-43 8.1111,-10.1111 18.27779,-15.16602 30.5,-15.16602 6,0 11.66668,1.22158 17,3.66602 5.33333,2.33333 10.05492,5.94509 14.16602,10.83398 4.11111,4.77777 7.33269,10.83269 9.66602,18.16602 1.65444,4.96334 2.70164,10.36877 3.23632,16.14648 v -52.01758 c -6.86066,-10.22654 -15.08687,-17.82908 -24.6875,-22.79492 -9.66665,-5.11111 -20.61177,-7.66601 -32.83398,-7.66601 -14.44443,0 -27.61113,3.60982 -39.5,10.83203 -11.77777,7.22221 -20.99936,17.77909 -27.66602,31.66797 -6.66666,13.77776 -10,30.33269 -10,49.66601 0,18.88887 3.27844,35.33204 9.83399,49.33203 6.66666,13.88887 15.77648,24.55556 27.33203,32 11.66665,7.44444 24.55687,11.16797 38.66797,11.16797 11.55555,0 22.05361,-2.27843 31.49804,-6.83398 9.50365,-4.64131 18.62118,-11.32337 27.35547,-20.03125 z"
|
|
||||||
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#fabf9c;fill-opacity:1"
|
|
||||||
id="path13" /><path
|
|
||||||
d="m 294.01562,237.18555 v 57.11523 c 0.79416,-8.72281 2.70075,-16.33307 5.73633,-22.81445 4.1111,-8.77777 9.55426,-15.33269 16.33203,-19.66602 6.77777,-4.22221 14.22288,-6.33398 22.33399,-6.33398 7.44443,0 14.38954,2.11177 20.83398,6.33398 6.44444,4.33333 11.66602,10.88825 15.66602,19.66602 3.88888,8.88888 5.83398,19.4438 5.83398,31.66601 0,18.55553 -4.05687,32.88889 -12.16797,43 -8.1111,10.1111 -18.27779,15.16797 -30.5,15.16797 -6,0 -11.66668,-1.22353 -17,-3.66797 -5.33333,-2.33333 -10.05491,-5.94314 -14.16601,-10.83203 -4.11111,-4.77777 -7.33269,-10.83464 -9.66602,-18.16797 -1.6545,-4.96351 -2.70166,-10.36855 -3.23633,-16.14648 v 52.01953 c 6.86067,10.22655 15.08688,17.82909 24.6875,22.79492 9.66665,5.11111 20.61178,7.66602 32.83399,7.66602 14.44443,0 27.61113,-3.61178 39.5,-10.83399 11.77777,-7.22221 20.99936,-17.77713 27.66601,-31.66601 6.66667,-13.77776 10,-30.3327 10,-49.66602 10e-6,-18.88887 -3.27648,-35.33399 -9.83203,-49.33398 -6.66666,-13.88887 -15.77843,-24.55556 -27.33398,-32 -11.66665,-7.44444 -24.55492,-11.16602 -38.66602,-11.16602 -11.55555,0 -22.05557,2.27844 -31.5,6.83399 -9.50364,4.64131 -18.62118,11.32337 -27.35547,20.03125 z"
|
|
||||||
style="-inkscape-font-specification:'Arial Rounded MT Bold, ';opacity:1;fill:#a7a7fa;fill-opacity:1"
|
|
||||||
id="path15" /></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.1 KiB |
-101
@@ -1,101 +0,0 @@
|
|||||||
<style>
|
|
||||||
h1 { display: none; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://qbot.botforge.biz"><img src="img/qbot.svg" alt="QBot"></a>
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
|
||||||
<em>Telegram Bots Rapid Application Development (RAD) Framework.</em>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
**QBot** is a library for fast development of Telegram bots and mini-apps following the **RAD (Rapid Application Development)** principle in a **declarative style**.
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
- **Automatic CRUD Interface Generation** – Manage objects effortlessly without manual UI coding.
|
|
||||||
- **Built-in Field Editors** – Includes text inputs, date/time pickers, boolean switches, enums, and entity selectors.
|
|
||||||
- **Advanced Pagination & Filtering** – Easily organize and navigate large datasets.
|
|
||||||
- **Authentication & Authorization** – Role-based access control for secure interactions.
|
|
||||||
- **Context Preservation** – Store navigation stacks and user interaction states in the database.
|
|
||||||
- **Internationalization Support** – Localizable UI and string fields for multilingual bots.
|
|
||||||
|
|
||||||
**QBot** powered by **[FastAPI](https://fastapi.tiangolo.com)**, **[SQLModel](https://sqlmodel.tiangolo.com)** & **[aiogram](https://aiogram.dev)** – Leverage the full capabilities of these frameworks for high performance and flexibility.
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
- **Faster Development** – Automate repetitive tasks and build bots in record time.
|
|
||||||
- **Highly Modular** – Easily extend and customize functionality.
|
|
||||||
- **Less Code Duplication** – Focus on core features while QBot handles the rest.
|
|
||||||
- **Enterprise-Grade Structure** – Scalable, maintainable, and optimized for real-world usage.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
```python
|
|
||||||
class AppEntity(BotEntity):
|
|
||||||
"""
|
|
||||||
BotEntity - business entity. Based on SQLModel, which provides ORM functionality,
|
|
||||||
basic CRUD functions and additional metadata, describing entities view and behaviour
|
|
||||||
"""
|
|
||||||
bot_entity_descriptor = Entity( # metadata attribute
|
|
||||||
name = "bot_entity", # Entity - descriptor for entity metadata
|
|
||||||
...
|
|
||||||
)
|
|
||||||
|
|
||||||
name: str # entity field with default sqlmodel's FieldInfo descriptor
|
|
||||||
# and default qbot's field descriptor
|
|
||||||
|
|
||||||
description: str | None = Field( # field with sqlmodel's descriptor
|
|
||||||
sa_type = String, index = True) # and default qbot's descriptor
|
|
||||||
|
|
||||||
age: int = EntityField( # field with qbot's descriptor
|
|
||||||
caption = "Age",
|
|
||||||
)
|
|
||||||
|
|
||||||
user_id: int | None = EntityField( # using both descriptors
|
|
||||||
sm_descriptor=Field(
|
|
||||||
sa_type=BigInteger,
|
|
||||||
foreign_key="user.id",
|
|
||||||
ondelete="RESTRICT",
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
is_visible=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
user: Optional[User] = EntityField( # using Relationship as sqlmodel descriptor
|
|
||||||
sm_descriptor=Relationship(
|
|
||||||
back_populates="entities",
|
|
||||||
sa_relationship_kwargs={
|
|
||||||
"lazy": "selectin",
|
|
||||||
"foreign_keys": "Entity.user_id",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
caption="User",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
app = QBotApp() # bot application based on FastAPI application
|
|
||||||
# providing Telegram API webhook handler
|
|
||||||
|
|
||||||
@app.command( # decorator for bot commands definition
|
|
||||||
name="menu",
|
|
||||||
caption="Main menu",
|
|
||||||
show_in_bot_commands=True, # shows command in bot's main menu
|
|
||||||
clear_navigation=True, # resets navigation stack between bot dialogues
|
|
||||||
)
|
|
||||||
async def menu(context: CommandCallbackContext):
|
|
||||||
context.message_text = "Main menu" # message text
|
|
||||||
context.keyboard_builder.row( # and buttons will be shown to user
|
|
||||||
InlineKeyboardButton(
|
|
||||||
text="Entities",
|
|
||||||
callback_data=ContextData( # callback query dataclass representing
|
|
||||||
command=CallbackCommand.ENTITY_LIST, # navigation context and its parameters
|
|
||||||
entity_name="bot_entity"
|
|
||||||
).pack(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
## Result
|
|
||||||
|
|
||||||
<iframe width="100%" height="691" src="https://www.youtube.com/embed/ptTnoppkYfM" title="QBot Framework – The Open-Source RAD Tool for Telegram Bots" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
|
||||||
|
|
||||||
forced_change_
|
|
||||||
File renamed without changes.
File renamed without changes.
+21
-23
@@ -1,8 +1,6 @@
|
|||||||
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
|
||||||
@@ -23,12 +21,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()
|
||||||
|
|
||||||
@@ -37,8 +35,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) as state:
|
async with app.lifespan(app):
|
||||||
yield state
|
yield
|
||||||
else:
|
else:
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@@ -46,10 +44,10 @@ async def default_lifespan(app: "QBotApp"):
|
|||||||
|
|
||||||
if app.lifespan_bot_init:
|
if app.lifespan_bot_init:
|
||||||
await app.bot_close()
|
await app.bot_close()
|
||||||
|
|
||||||
if app.config.USE_NGROK:
|
if app.config.USE_NGROK:
|
||||||
app.ngrok_stop()
|
app.ngrok_stop()
|
||||||
|
|
||||||
logger.info("qbot app stopped")
|
logger.info("qbot app stopped")
|
||||||
|
|
||||||
|
|
||||||
@@ -90,14 +88,8 @@ 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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,16 +124,18 @@ 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="/telegram", tags=["telegram"])
|
self.include_router(telegram_router, prefix="/api/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
|
||||||
@@ -156,6 +150,7 @@ class QBotApp[UserType: UserBase](FastAPI):
|
|||||||
pyngrok_config=PyngrokConfig(auth_token=self.config.NGROK_AUTH_TOKEN),
|
pyngrok_config=PyngrokConfig(auth_token=self.config.NGROK_AUTH_TOKEN),
|
||||||
)
|
)
|
||||||
self.config.NGROK_URL = tunnel.public_url
|
self.config.NGROK_URL = tunnel.public_url
|
||||||
|
|
||||||
|
|
||||||
def ngrok_stop(self):
|
def ngrok_stop(self):
|
||||||
try:
|
try:
|
||||||
@@ -168,7 +163,10 @@ 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():
|
||||||
@@ -185,13 +183,6 @@ 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(
|
||||||
[
|
[
|
||||||
@@ -201,7 +192,14 @@ 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()
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
-14
@@ -1,14 +0,0 @@
|
|||||||
site_name: QBot Framework
|
|
||||||
site_url: https://qbot.botforge.biz
|
|
||||||
theme:
|
|
||||||
name: material
|
|
||||||
palette:
|
|
||||||
primary: #fabf9c
|
|
||||||
accent: #a7a7fa
|
|
||||||
font:
|
|
||||||
text: 'Roboto'
|
|
||||||
code: 'Roboto Mono'
|
|
||||||
logo: 'img/qbot_1_1.svg'
|
|
||||||
favicon: 'img/qbot_1_1.svg'
|
|
||||||
repo_name: botforge/qbot
|
|
||||||
repo_url: https://git.botforge.biz/botforge/qbot
|
|
||||||
File renamed without changes.
File renamed without changes.
@@ -12,7 +12,6 @@ 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
|
||||||
@@ -65,33 +64,7 @@ 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) # type: FieldInfo
|
sm_descriptor = descriptor_kwargs.pop("sm_descriptor", None)
|
||||||
|
|
||||||
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
|
||||||
@@ -184,6 +157,23 @@ 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"]
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ class EnumType(TypeDecorator):
|
|||||||
impl = AutoString
|
impl = AutoString
|
||||||
cache_ok = True
|
cache_ok = True
|
||||||
|
|
||||||
def __init__(self, enum_type):
|
def __init__(self, enum_type: BotEnum):
|
||||||
self._enum_type = enum_type
|
self._enum_type = enum_type
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
File renamed without changes.
@@ -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 | None = None
|
is_visible: bool = True
|
||||||
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,7 +102,6 @@ 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)
|
||||||
@@ -164,9 +163,6 @@ class _BaseEntityDescriptor:
|
|||||||
EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER],
|
EntityPermission.DELETE_ALL: [RoleBase.SUPER_USER],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
on_created: Callable[["BotEntity", "EntityEventContext"], None] | None = None
|
|
||||||
on_deleted: Callable[["BotEntity", "EntityEventContext"], None] | None = None
|
|
||||||
on_updated: Callable[["BotEntity", "EntityEventContext"], None] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
@@ -201,13 +197,6 @@ class CommandCallbackContext[UT: UserBase]:
|
|||||||
kwargs: dict[str, Any] = field(default_factory=dict)
|
kwargs: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class EntityEventContext:
|
|
||||||
db_session: AsyncSession
|
|
||||||
app: "QBotApp"
|
|
||||||
message: Message | CallbackQuery | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
class BotCommand:
|
class BotCommand:
|
||||||
name: str
|
name: str
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -227,9 +227,7 @@ class Settings(metaclass=SettingsMetaclass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
param.default_factory()
|
param.default
|
||||||
if param.default_factory
|
|
||||||
else param.default
|
|
||||||
if param.default
|
if param.default
|
||||||
else (
|
else (
|
||||||
[]
|
[]
|
||||||
@@ -251,6 +249,7 @@ 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
|
||||||
File renamed without changes.
File renamed without changes.
@@ -1,29 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "quickbot"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "QBot - Rapid Application Development Framework for Telegram Bots"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.12"
|
|
||||||
classifiers = [
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Operating System :: OS Independent",
|
|
||||||
"Development Status :: 3 - Alpha",
|
|
||||||
]
|
|
||||||
authors = [
|
|
||||||
{ name = "Alexander Kalinovsky", email = "ak@botforge.biz" },
|
|
||||||
]
|
|
||||||
license = { file = "LICENSE" }
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
"aiogram>=3.17.0",
|
|
||||||
"babel>=2.17.0",
|
|
||||||
"fastapi[standard]>=0.115.8",
|
|
||||||
"greenlet>=3.1.1",
|
|
||||||
"mkdocs-material>=9.6.5",
|
|
||||||
"pydantic-settings>=2.7.1",
|
|
||||||
"pyngrok>=7.2.3",
|
|
||||||
"pytest>=8.3.4",
|
|
||||||
"ruff>=0.9.6",
|
|
||||||
"sqlmodel>=0.0.22",
|
|
||||||
"ujson>=5.10.0",
|
|
||||||
]
|
|
||||||
File renamed without changes.
-139
@@ -1,139 +0,0 @@
|
|||||||
from qbot import (
|
|
||||||
QBotApp,
|
|
||||||
BotEntity,
|
|
||||||
Entity,
|
|
||||||
EntityForm,
|
|
||||||
EntityList,
|
|
||||||
FieldEditButton,
|
|
||||||
EntityField,
|
|
||||||
CommandCallbackContext,
|
|
||||||
ContextData,
|
|
||||||
CallbackCommand,
|
|
||||||
)
|
|
||||||
from qbot.model.user import UserBase
|
|
||||||
from qbot.model.descriptors import Filter
|
|
||||||
from qbot.model.role import RoleBase
|
|
||||||
|
|
||||||
from aiogram.types import InlineKeyboardButton
|
|
||||||
from datetime import datetime
|
|
||||||
from sqlmodel import BigInteger, Field, Relationship
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class User(UserBase):
|
|
||||||
bot_entity_descriptor = Entity(
|
|
||||||
icon="👤",
|
|
||||||
full_name="User",
|
|
||||||
full_name_plural="Users",
|
|
||||||
item_repr=lambda d, e: e.name,
|
|
||||||
default_list=EntityList(
|
|
||||||
show_add_new_button=False,
|
|
||||||
static_filters=[Filter("roles", "contains", value=RoleBase.SUPER_USER)],
|
|
||||||
filtering=True,
|
|
||||||
filtering_fields=["name"],
|
|
||||||
),
|
|
||||||
default_form=EntityForm(
|
|
||||||
show_delete_button=False,
|
|
||||||
show_edit_button=False,
|
|
||||||
form_buttons=[
|
|
||||||
[
|
|
||||||
FieldEditButton("name"),
|
|
||||||
FieldEditButton("is_active"),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
FieldEditButton("lang"),
|
|
||||||
FieldEditButton("roles"),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
item_repr=lambda d, e: f"{e.name}\n{
|
|
||||||
'is active' if e.is_active else 'is not active'
|
|
||||||
}\nlang: {e.lang.localized()}\nroles: [{
|
|
||||||
', '.join([r.localized() for r in e.roles]) if e.roles else 'none'
|
|
||||||
}]",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Entity(BotEntity):
|
|
||||||
bot_entity_descriptor = Entity(
|
|
||||||
icon="📦",
|
|
||||||
full_name="Entity",
|
|
||||||
full_name_plural="Entities",
|
|
||||||
item_repr=lambda d, e: e.name,
|
|
||||||
default_list=EntityList(
|
|
||||||
filtering=True,
|
|
||||||
filtering_fields=["name"],
|
|
||||||
order_by="name",
|
|
||||||
),
|
|
||||||
default_form=EntityForm(
|
|
||||||
form_buttons=[
|
|
||||||
[
|
|
||||||
FieldEditButton("name"),
|
|
||||||
FieldEditButton("user"),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
FieldEditButton("creation_dt"),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
id: int = EntityField(
|
|
||||||
caption="ID",
|
|
||||||
sm_descriptor=Field(
|
|
||||||
primary_key=True,
|
|
||||||
),
|
|
||||||
is_visible=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
name: str = EntityField(
|
|
||||||
caption="Name",
|
|
||||||
)
|
|
||||||
|
|
||||||
creation_dt: datetime = EntityField(
|
|
||||||
caption="Creation date",
|
|
||||||
dt_type="datetime",
|
|
||||||
)
|
|
||||||
|
|
||||||
user_id: int | None = EntityField(
|
|
||||||
sm_descriptor=Field(
|
|
||||||
sa_type=BigInteger,
|
|
||||||
foreign_key="user.id",
|
|
||||||
ondelete="RESTRICT",
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
is_visible=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
user: Optional[User] = EntityField(
|
|
||||||
sm_descriptor=Relationship(
|
|
||||||
sa_relationship_kwargs={
|
|
||||||
"lazy": "selectin",
|
|
||||||
"foreign_keys": "Entity.user_id",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
caption="User",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
app = QBotApp(
|
|
||||||
user_class=User,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command(
|
|
||||||
name="menu",
|
|
||||||
caption="Main menu",
|
|
||||||
show_in_bot_commands=True,
|
|
||||||
clear_navigation=True,
|
|
||||||
)
|
|
||||||
async def menu(context: CommandCallbackContext):
|
|
||||||
context.message_text = "Main menu"
|
|
||||||
context.keyboard_builder.row(
|
|
||||||
InlineKeyboardButton(
|
|
||||||
text="Entities",
|
|
||||||
callback_data=ContextData(
|
|
||||||
command=CallbackCommand.MENU_ENTRY_ENTITIES,
|
|
||||||
).pack(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Whitespace-only changes.
@@ -222,40 +222,3 @@ 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
|
|
||||||
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user