diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 820f6c5..1dc4877 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -30,17 +30,8 @@ jobs:
- name: Install dependencies
run: make develop
- - name: Run flake8
- run: make flake
-
- - name: Run black
- run: make black
-
- - name: Run bandit
- run: make bandit
-
- - name: Run mypy
- run: make mypy
+ - name: Run CI linters
+ run: make lint-ci
test:
name: Run service tests with pytest
diff --git a/.gitignore b/.gitignore
index 68bc17f..195ec50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -157,4 +157,5 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+.idea/
+.vscode
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2eeff45..e8cacea 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -22,59 +22,18 @@ repos:
types: [python]
- id: trailing-whitespace
- - repo: https://github.com/pre-commit/pygrep-hooks
- rev: v1.10.0
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.1.7
hooks:
- - id: python-check-blanket-noqa
- - id: python-check-mock-methods
- - id: python-no-eval
- - id: python-no-log-warn
- - id: python-use-type-annotations
- - id: text-unicode-replacement-char
-
- - repo: https://github.com/asottile/pyupgrade
- rev: v3.15.0
- hooks:
- - id: pyupgrade
- args: [--py311-plus]
-
- - repo: https://github.com/pycqa/autoflake
- rev: v2.2.1
- hooks:
- - id: autoflake
- args:
- - --in-place
- - --remove-all-unused-imports
- - --remove-unused-variables
- - --ignore-init-module-imports
-
- - repo: https://github.com/psf/black
- rev: 23.10.1
- hooks:
- - id: black
- language_version: python3.11
-
- - repo: https://github.com/pycqa/bandit
- rev: 1.7.5
- hooks:
- - id: bandit
- args:
- - --aggregate=file
- - -iii
- - -ll
- require_serial: true
- - repo: https://github.com/pycqa/isort
- rev: 5.12.0
- hooks:
- - id: isort
- name: isort (python)
- args: [--profile black]
+ - id: ruff
+ args: [--fix]
+ - id: ruff-format
- repo: local
hooks:
- id: mypy
name: mypy
- entry: mypy ./inclusive_dance_bot --config-file ./pyproject.toml
+ entry: mypy ./idb --config-file ./pyproject.toml
language: python
language_version: python3.11
require_serial: true
diff --git a/Makefile b/Makefile
index 5c385a9..90e8a8b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-PROJECT_PATH = ./inclusive_dance_bot/
+PROJECT_PATH = ./idb/
TEST_PATH = ./tests/
HELP_FUN = \
@@ -11,13 +11,13 @@ help: ##@Help Show this help
@echo -e "Usage: make [target] ...\n"
@perl -e '$(HELP_FUN)' $(MAKEFILE_LIST)
-lint-ci: flake black bandit mypy ##@Linting Run all linters in CI
+lint-ci: flake ruff bandit mypy ##@Linting Run all linters in CI
flake: ##@Linting Run flake8
.venv/bin/flake8 --max-line-length 88 --format=default $(PROJECT_PATH) 2>&1 | tee flake8.txt
-black: ##@Linting Run black
- .venv/bin/black $(PROJECT_PATH) --check
+ruff: ##@Linting Run ruff
+ .venv/bin/ruff check $(PROJECT_PATH)
bandit: ##@Linting Run bandit
.venv/bin/bandit -r -ll -iii $(PROJECT_PATH) -f json -o ./bandit.json
diff --git a/README.md b/README.md
index 2de33dd..7dcb494 100644
--- a/README.md
+++ b/README.md
@@ -29,21 +29,13 @@ Environment variables are used to configure the bot for connecting to Telegram,
```bash
-TELEGRAM_BOT_TOKEN # your bot token
-TELEGRAM_BOT_ADMIN_IDS # Superadmin IDs, who can appoint other admins
+APP_TELEGRAM_BOT_TOKEN # your bot token
+APP_TELEGRAM_BOT_ADMIN_IDS # Superadmin IDs, who can appoint other admins
-DEBUG # Flag for debugging (using in sqlalchemy engine for echo)
+APP_DEBUG # Flag for debugging (using in sqlalchemy engine for echo)
-POSTGRES_HOST # database host
-POSTGRES_PORT # database port
-POSTGRES_USER # database user
-POSTGRES_PASSWORD # database password
-POSTGRES_DB # database name
-
-REDIS_HOST # redis host
-REDIS_PORT # redis port
-REDIS_PASSWORD # redis password
-REDIS_DB # redis db
+APP_PG_DSN # DSN of your postgresql database
+APP_REDIS_DSN # DSN of your redis storage
```
### Docker
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 14a61fe..0ee75a2 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -15,27 +15,17 @@ services:
redis:
image: redis
restart: unless-stopped
- command: redis-server --requirepass ${REDIS_PASSWORD}
+ command: redis-server --requirepass $REDIS_PASSWORD
bot:
image: andytakker/inclusive_dance_bot:latest
restart: unless-stopped
command: /wait-for-it.sh -t 15 -h db -p 5432 -- /app/start.sh
environment:
- POSTGRES_HOST: db
- POSTGRES_PORT: 5432
- POSTGRES_USER: $POSTGRES_USER
- POSTGRES_PASSWORD: $POSTGRES_PASSWORD
- POSTGRES_DB: $POSTGRES_DB
-
- PG_URL: postgresql+asyncpg://$POSTGRES_USER:$POSTGRES_PASSWORD@db:5432/$POSTGRES_DB
-
- REDIS_HOST: redis
- REDIS_PORT: 6379
- REDIS_PASSWORD: $REDIS_PASSWORD
- REDIS_DB: $REDIS_DB
+ APP_REDIS_DSN: redis://:$REDIS_PASSWORD@redis:6379/1
+ APP_PG_DSN: postgresql+asyncpg://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres:5432/$POSTGRES_DB
- TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
+ APP_TELEGRAM_BOT_TOKEN: $TELEGRAM_BOT_TOKEN
volumes:
postgres_data:
diff --git a/inclusive_dance_bot/__init__.py b/idb/__init__.py
similarity index 100%
rename from inclusive_dance_bot/__init__.py
rename to idb/__init__.py
diff --git a/inclusive_dance_bot/__main__.py b/idb/__main__.py
similarity index 72%
rename from inclusive_dance_bot/__main__.py
rename to idb/__main__.py
index f87e1e5..e484f15 100644
--- a/inclusive_dance_bot/__main__.py
+++ b/idb/__main__.py
@@ -3,10 +3,10 @@
from aiomisc import Service, entrypoint
from aiomisc_log import basic_config
-from inclusive_dance_bot.arguments import get_parser
-from inclusive_dance_bot.deps import config_deps
-from inclusive_dance_bot.services.bot import AiogramBotService
-from inclusive_dance_bot.services.periodic import PeriodicMailingService
+from idb.arguments import get_parser
+from idb.bot.services.bot import AiogramBotService
+from idb.bot.services.periodic import PeriodicMailingService
+from idb.deps import config_deps
log = logging.getLogger(__name__)
@@ -14,9 +14,10 @@
def main() -> None:
parser = get_parser()
arguments = parser.parse_args()
+
basic_config(
- log_format=arguments.log_level,
- level=arguments.log_format,
+ log_format=arguments.log_format,
+ level=arguments.log_level,
)
config_deps(arguments=arguments)
services: list[Service] = [
diff --git a/inclusive_dance_bot/arguments.py b/idb/arguments.py
similarity index 87%
rename from inclusive_dance_bot/arguments.py
rename to idb/arguments.py
index 8db0fd3..bc73b51 100644
--- a/inclusive_dance_bot/arguments.py
+++ b/idb/arguments.py
@@ -1,6 +1,7 @@
import argparse
import json
+from aiogram.enums import ParseMode
from aiomisc_log import LogFormat, LogLevel
from configargparse import ArgumentParser
@@ -13,7 +14,7 @@ def int_list(s: str) -> list[int]:
if not all(map(lambda x: isinstance(x, int), value)):
raise ValueError
return value
- except Exception:
+ except (json.JSONDecodeError, ValueError):
raise ValueError("This is not list of integers")
@@ -42,6 +43,12 @@ def get_parser() -> ArgumentParser:
group = parser.add_argument_group("Telegram Bot options")
group.add_argument("--telegram-bot-token", required=True)
group.add_argument("--telegram-bot-admin-ids", type=int_list, default=[])
+ group.add_argument(
+ "--telegram-parse-mode",
+ type=ParseMode,
+ choices=tuple(ParseMode._member_names_),
+ default=ParseMode.HTML,
+ )
group.add_argument("--telegram-periodic-interval", type=int, default=5 * 60)
group.add_argument("--telegram-mailing-gap", type=int, default=2 * 60)
diff --git a/inclusive_dance_bot/bot/__init__.py b/idb/bot/__init__.py
similarity index 100%
rename from inclusive_dance_bot/bot/__init__.py
rename to idb/bot/__init__.py
diff --git a/inclusive_dance_bot/bot/dialogs/admins/main_menu/read/__init__.py b/idb/bot/dialogs/__init__.py
similarity index 100%
rename from inclusive_dance_bot/bot/dialogs/admins/main_menu/read/__init__.py
rename to idb/bot/dialogs/__init__.py
diff --git a/inclusive_dance_bot/bot/dialogs/users/windows/__init__.py b/idb/bot/dialogs/admins/__init__.py
similarity index 100%
rename from inclusive_dance_bot/bot/dialogs/users/windows/__init__.py
rename to idb/bot/dialogs/admins/__init__.py
diff --git a/inclusive_dance_bot/bot/middlewares/__init__.py b/idb/bot/dialogs/admins/feedbacks/__init__.py
similarity index 100%
rename from inclusive_dance_bot/bot/middlewares/__init__.py
rename to idb/bot/dialogs/admins/feedbacks/__init__.py
diff --git a/inclusive_dance_bot/db/__init__.py b/idb/bot/dialogs/admins/feedbacks/answer/__init__.py
similarity index 100%
rename from inclusive_dance_bot/db/__init__.py
rename to idb/bot/dialogs/admins/feedbacks/answer/__init__.py
diff --git a/idb/bot/dialogs/admins/feedbacks/answer/dialog.py b/idb/bot/dialogs/admins/feedbacks/answer/dialog.py
new file mode 100644
index 0000000..6f8df0a
--- /dev/null
+++ b/idb/bot/dialogs/admins/feedbacks/answer/dialog.py
@@ -0,0 +1,13 @@
+from aiogram_dialog import Dialog
+
+from idb.bot.dialogs.admins.feedbacks.answer.windows.confirm import (
+ window as confirm_window,
+)
+from idb.bot.dialogs.admins.feedbacks.answer.windows.input_message import (
+ window as input_message_window,
+)
+
+dialog = Dialog(
+ input_message_window,
+ confirm_window,
+)
diff --git a/inclusive_dance_bot/db/migrations/versions/__init__.py b/idb/bot/dialogs/admins/feedbacks/answer/windows/__init__.py
similarity index 100%
rename from inclusive_dance_bot/db/migrations/versions/__init__.py
rename to idb/bot/dialogs/admins/feedbacks/answer/windows/__init__.py
diff --git a/idb/bot/dialogs/admins/feedbacks/answer/windows/confirm.py b/idb/bot/dialogs/admins/feedbacks/answer/windows/confirm.py
new file mode 100644
index 0000000..883003a
--- /dev/null
+++ b/idb/bot/dialogs/admins/feedbacks/answer/windows/confirm.py
@@ -0,0 +1,44 @@
+from datetime import UTC, datetime
+
+from aiogram.types import CallbackQuery
+from aiogram_dialog import DialogManager, Window
+from aiogram_dialog.widgets.kbd import Button, Row
+from aiogram_dialog.widgets.text import Const
+
+from idb.bot.dialogs.admins.states import FeedbackAnswerSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.db.uow import UnitOfWork
+from idb.logic.answer import create_feedback_answer
+from idb.logic.feedback import update_answered_feedback
+
+
+async def on_click(
+ c: CallbackQuery, button: Button, dialog_manager: DialogManager
+) -> None:
+ uow: UnitOfWork = dialog_manager.middleware_data["uow"]
+ feedback_id = int(dialog_manager.start_data["feedback_id"])
+ text = str(dialog_manager.current_context().widget_data["input_answer"])
+ answer = await create_feedback_answer(
+ uow=uow,
+ feedback_id=feedback_id,
+ text=text,
+ from_user_id=dialog_manager.event.from_user.id, # type: ignore[union-attr]
+ )
+
+ await c.bot.send_message( # type: ignore[union-attr]
+ chat_id=answer.to_user_id,
+ text=answer.text,
+ )
+ await update_answered_feedback(
+ uow=uow,
+ feedback_id=feedback_id,
+ dt=datetime.now(UTC),
+ )
+ await dialog_manager.done()
+
+
+window = Window(
+ Const("Отправить сообщение?"),
+ Row(CANCEL, Button(text=Const("📨 Да"), id="send_message", on_click=on_click)),
+ state=FeedbackAnswerSG.confirm,
+)
diff --git a/idb/bot/dialogs/admins/feedbacks/answer/windows/input_message.py b/idb/bot/dialogs/admins/feedbacks/answer/windows/input_message.py
new file mode 100644
index 0000000..569fe76
--- /dev/null
+++ b/idb/bot/dialogs/admins/feedbacks/answer/windows/input_message.py
@@ -0,0 +1,27 @@
+from aiogram.types import Message
+from aiogram_dialog import DialogManager, Window
+from aiogram_dialog.widgets.input import ManagedTextInput, TextInput
+from aiogram_dialog.widgets.text import Const
+
+from idb.bot.dialogs.admins.states import FeedbackAnswerSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+
+
+async def on_success_next(
+ message: Message,
+ widget: ManagedTextInput[str],
+ dialog_manager: DialogManager,
+ value: str,
+) -> None:
+ await dialog_manager.next()
+
+
+window = Window(
+ Const("Введите сообщение"),
+ TextInput(
+ id="input_answer",
+ on_success=on_success_next, # type: ignore[arg-type]
+ ),
+ CANCEL,
+ state=FeedbackAnswerSG.input_message,
+)
diff --git a/idb/bot/dialogs/admins/feedbacks/read/__init__.py b/idb/bot/dialogs/admins/feedbacks/read/__init__.py
new file mode 100644
index 0000000..be8aed0
--- /dev/null
+++ b/idb/bot/dialogs/admins/feedbacks/read/__init__.py
@@ -0,0 +1,15 @@
+from aiogram_dialog import Dialog
+
+from idb.bot.dialogs.admins.feedbacks.read import (
+ item,
+ menu,
+ new,
+ viewed,
+)
+
+dialog = Dialog(
+ menu.window,
+ new.window,
+ viewed.window,
+ item.window,
+)
diff --git a/idb/bot/dialogs/admins/feedbacks/read/item.py b/idb/bot/dialogs/admins/feedbacks/read/item.py
new file mode 100644
index 0000000..e797b28
--- /dev/null
+++ b/idb/bot/dialogs/admins/feedbacks/read/item.py
@@ -0,0 +1,83 @@
+from typing import Any
+
+from aiogram.types import CallbackQuery
+from aiogram_dialog import DialogManager, Window
+from aiogram_dialog.widgets.kbd import Button
+from aiogram_dialog.widgets.text import Const, Jinja
+
+from idb.bot.dialogs.admins.states import (
+ FeedbackAnswerSG,
+ FeedbackItemsSG,
+)
+from idb.bot.dialogs.utils.start_with_data import start_with_data
+from idb.db.uow import UnitOfWork
+from idb.generals.enums import FEEDBACK_TYPE_MAPPING, FeedbackStatus
+
+FEEDBACK_TEMPLATE = """
+Тема: {{ feedback.title }}
+Тип: {{ feedback_type }}
+Отправлено: {{ feedback.created_at|as_local_fmt }}
+Просмотрено: {{ feedback.viewed_at|as_local_fmt }}
+
+{{ feedback.text }}
+
+{% if answers %}
+
+Ответы:
+
+{% for answer in answers %}
+Отправлено: {{ answer.created_at|as_local_fmt }}
+{{ answer.text }}
+=======
+
+{% endfor %}
+
+{% endif %}
+"""
+
+
+async def get_feedback_data(
+ uow: UnitOfWork, dialog_manager: DialogManager, **kwargs: Any
+) -> dict[str, Any]:
+ feedback_id = dialog_manager.dialog_data["feedback_id"]
+ feedback = await uow.feedbacks.read_by_id(feedback_id)
+ return {
+ "feedback": feedback,
+ "answers": await uow.answer.history(feedback_id=feedback_id),
+ "feedback_type": FEEDBACK_TYPE_MAPPING[feedback.type],
+ }
+
+
+async def back(
+ c: CallbackQuery,
+ widget: Button,
+ dialog_manager: DialogManager,
+) -> None:
+ if dialog_manager.dialog_data["back"] == FeedbackStatus.NEW:
+ await dialog_manager.switch_to(FeedbackItemsSG.new)
+ elif dialog_manager.dialog_data["back"] == FeedbackStatus.ARCHIVED:
+ await dialog_manager.switch_to(FeedbackItemsSG.archived)
+
+
+def _when(data: dict, widget: Button, dialog_manager: DialogManager) -> bool:
+ feedback = data.get("feedback")
+ if feedback is None:
+ return False
+ return feedback.is_answered
+
+
+window = Window(
+ Const("Обратная связь"),
+ Jinja(FEEDBACK_TEMPLATE),
+ Button(
+ Const("✏️ Написать пользователю"),
+ id="start_answer",
+ on_click=start_with_data(
+ state=FeedbackAnswerSG.input_message,
+ field="feedback_id",
+ ),
+ ),
+ Button(Const("⬅️ Назад"), id="back", on_click=back),
+ state=FeedbackItemsSG.item,
+ getter=get_feedback_data,
+)
diff --git a/idb/bot/dialogs/admins/feedbacks/read/menu.py b/idb/bot/dialogs/admins/feedbacks/read/menu.py
new file mode 100644
index 0000000..0e52a6e
--- /dev/null
+++ b/idb/bot/dialogs/admins/feedbacks/read/menu.py
@@ -0,0 +1,36 @@
+from typing import Any
+
+from aiogram_dialog import Window
+from aiogram_dialog.widgets.kbd import SwitchTo
+from aiogram_dialog.widgets.text import Const, Format
+
+from idb.bot.dialogs.admins.states import FeedbackItemsSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.db.uow import UnitOfWork
+
+
+async def get_data(uow: UnitOfWork, **kwargs: Any) -> dict[str, Any]:
+ archive_feedback_count = await uow.feedbacks.archive_count()
+ new_feedback_count = await uow.feedbacks.new_count()
+ return {
+ "archive_feedback_count": archive_feedback_count,
+ "new_feedback_count": new_feedback_count,
+ }
+
+
+window = Window(
+ Const("Обратная связь"),
+ SwitchTo(
+ text=Format("🆕 Новые сообщения ({new_feedback_count})"),
+ state=FeedbackItemsSG.new,
+ id="new_feedbacks",
+ ),
+ SwitchTo(
+ text=Format("👁 Просмотренные сообщения ({archive_feedback_count})"),
+ state=FeedbackItemsSG.archived,
+ id="archive_feedbacks",
+ ),
+ CANCEL,
+ state=FeedbackItemsSG.menu,
+ getter=get_data,
+)
diff --git a/idb/bot/dialogs/admins/feedbacks/read/new.py b/idb/bot/dialogs/admins/feedbacks/read/new.py
new file mode 100644
index 0000000..0f2b846
--- /dev/null
+++ b/idb/bot/dialogs/admins/feedbacks/read/new.py
@@ -0,0 +1,64 @@
+from datetime import UTC, datetime
+from typing import Any
+
+from aiogram.types import CallbackQuery
+from aiogram_dialog import DialogManager, Window
+from aiogram_dialog.widgets.kbd import Button, ScrollingGroup, Select, SwitchTo
+from aiogram_dialog.widgets.text import Const, Format, Jinja, List
+
+from idb.bot.dialogs.admins.states import FeedbackItemsSG
+from idb.bot.dialogs.utils.sync_scroll import sync_scroll
+from idb.db.uow import UnitOfWork
+from idb.generals.enums import FeedbackStatus
+from idb.logic.feedback import set_feedback_as_viewed
+
+SCROLL_KBD_ID = "feedback_scroll_id"
+SCROLL_MESSAGE_ID = "feedback_message_scroll_id"
+
+
+async def get_feedbacks_list_data(uow: UnitOfWork, **kwargs: Any) -> dict[str, Any]:
+ return {"feedbacks": await uow.feedbacks.new_items()}
+
+
+async def on_click(
+ c: CallbackQuery,
+ widget: Button,
+ dialog_manager: DialogManager,
+ feedback_id: int,
+) -> None:
+ dialog_manager.dialog_data["feedback_id"] = feedback_id
+ dialog_manager.dialog_data["back"] = FeedbackStatus.NEW
+ uow: UnitOfWork = dialog_manager.middleware_data["uow"]
+ dt = datetime.now(tz=UTC)
+ await set_feedback_as_viewed(uow=uow, feedback_id=feedback_id, dt=dt)
+ await c.answer("Сообщение отмечено прочитанным", show_alert=True)
+ await dialog_manager.switch_to(FeedbackItemsSG.item)
+
+
+window = Window(
+ Const("Новые сообщения\n"),
+ List(
+ Jinja("[{{pos}}] {{ item.created_at|as_local_fmt }} {{item.title}}"),
+ items="feedbacks",
+ id=SCROLL_MESSAGE_ID,
+ page_size=10,
+ ),
+ ScrollingGroup(
+ Select(
+ Format("{pos}"),
+ id="s_feedback",
+ item_id_getter=lambda x: x.id,
+ items="feedbacks",
+ on_click=on_click, # type: ignore[arg-type]
+ type_factory=int,
+ ),
+ id=SCROLL_KBD_ID,
+ width=5,
+ height=2,
+ hide_on_single_page=True,
+ on_page_changed=sync_scroll(SCROLL_MESSAGE_ID),
+ ),
+ SwitchTo(Const("⬅️ Назад"), id="back", state=FeedbackItemsSG.menu),
+ state=FeedbackItemsSG.new,
+ getter=get_feedbacks_list_data,
+)
diff --git a/idb/bot/dialogs/admins/feedbacks/read/viewed.py b/idb/bot/dialogs/admins/feedbacks/read/viewed.py
new file mode 100644
index 0000000..c90059f
--- /dev/null
+++ b/idb/bot/dialogs/admins/feedbacks/read/viewed.py
@@ -0,0 +1,67 @@
+from typing import Any
+
+from aiogram.types import CallbackQuery
+from aiogram_dialog import DialogManager, Window
+from aiogram_dialog.widgets.kbd import Button, ScrollingGroup, Select, SwitchTo
+from aiogram_dialog.widgets.text import Const, Format, Jinja, List
+
+from idb.bot.dialogs.admins.states import FeedbackItemsSG
+from idb.bot.dialogs.utils.sync_scroll import sync_scroll
+from idb.db.uow import UnitOfWork
+from idb.generals.enums import FeedbackStatus
+
+SCROLL_KBD_ID = "feedback_scroll_id"
+SCROLL_MESSAGE_ID = "feedback_message_scroll_id"
+
+
+async def get_feedbacks_list_data(uow: UnitOfWork, **kwargs: Any) -> dict[str, Any]:
+ return {"feedbacks": await uow.feedbacks.viewed_items()}
+
+
+async def open_feedback(
+ c: CallbackQuery,
+ widget: Button,
+ dialog_manager: DialogManager,
+ feedback_id: int,
+) -> None:
+ dialog_manager.dialog_data["feedback_id"] = feedback_id
+ dialog_manager.dialog_data["back"] = FeedbackStatus.ARCHIVED
+ await dialog_manager.switch_to(FeedbackItemsSG.item)
+
+
+HEAD_MESSAGE = """
+Просмотренные сообщения
+❌ - не ответили
+✅ - ответили
+"""
+
+window = Window(
+ Const(HEAD_MESSAGE),
+ List(
+ Jinja(
+ "[{{ pos }}] {{ item.created_at|as_local_fmt }} {{ item.title }} "
+ "{% if item.is_answered %}✅{% else %}❌{% endif %}"
+ ),
+ items="feedbacks",
+ id=SCROLL_MESSAGE_ID,
+ page_size=10,
+ ),
+ ScrollingGroup(
+ Select(
+ Format("{pos}"),
+ id="s_feedback",
+ item_id_getter=lambda x: x.id,
+ items="feedbacks",
+ on_click=open_feedback, # type: ignore[arg-type]
+ type_factory=int,
+ ),
+ id=SCROLL_KBD_ID,
+ width=5,
+ height=2,
+ hide_on_single_page=True,
+ on_page_changed=sync_scroll(SCROLL_MESSAGE_ID),
+ ),
+ SwitchTo(Const("⬅️ Назад"), id="back", state=FeedbackItemsSG.menu),
+ state=FeedbackItemsSG.archived,
+ getter=get_feedbacks_list_data,
+)
diff --git a/idb/bot/dialogs/admins/feedbacks/router.py b/idb/bot/dialogs/admins/feedbacks/router.py
new file mode 100644
index 0000000..e2b7f75
--- /dev/null
+++ b/idb/bot/dialogs/admins/feedbacks/router.py
@@ -0,0 +1,12 @@
+from aiogram import Router
+
+from idb.bot.dialogs.admins.feedbacks import read
+from idb.bot.dialogs.admins.feedbacks.answer.dialog import (
+ dialog as answer_dialog,
+)
+
+router = Router()
+router.include_routers(
+ read.dialog,
+ answer_dialog,
+)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/__init__.py b/idb/bot/dialogs/admins/mailings/__init__.py
similarity index 61%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/__init__.py
rename to idb/bot/dialogs/admins/mailings/__init__.py
index 1cc5667..3f622cb 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/__init__.py
+++ b/idb/bot/dialogs/admins/mailings/__init__.py
@@ -1,6 +1,6 @@
from aiogram import Router
-from inclusive_dance_bot.bot.dialogs.admins.mailings import cancel, create, read
+from idb.bot.dialogs.admins.mailings import cancel, create, read
router = Router()
router.include_routers(
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/delete/__init__.py b/idb/bot/dialogs/admins/mailings/cancel/__init__.py
similarity index 51%
rename from inclusive_dance_bot/bot/dialogs/admins/url/delete/__init__.py
rename to idb/bot/dialogs/admins/mailings/cancel/__init__.py
index b041188..5e4e159 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/delete/__init__.py
+++ b/idb/bot/dialogs/admins/mailings/cancel/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.admins.url.delete import confirm
+from idb.bot.dialogs.admins.mailings.cancel import confirm
dialog = Dialog(
confirm.window,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/cancel/confirm.py b/idb/bot/dialogs/admins/mailings/cancel/confirm.py
similarity index 74%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/cancel/confirm.py
rename to idb/bot/dialogs/admins/mailings/cancel/confirm.py
index 5ef84a7..446adf3 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/cancel/confirm.py
+++ b/idb/bot/dialogs/admins/mailings/cancel/confirm.py
@@ -3,11 +3,11 @@
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CancelMailingSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.enums import MailingStatus
-from inclusive_dance_bot.logic.mailing import update_mailing_by_id
+from idb.bot.dialogs.admins.states import CancelMailingSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.db.uow import UnitOfWork
+from idb.generals.enums import MailingStatus
+from idb.logic.mailing import update_mailing_by_id
async def on_click(
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/__init__.py b/idb/bot/dialogs/admins/mailings/create/__init__.py
similarity index 84%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/create/__init__.py
rename to idb/bot/dialogs/admins/mailings/create/__init__.py
index 7d75c13..f9138a1 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/__init__.py
+++ b/idb/bot/dialogs/admins/mailings/create/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.admins.mailings.create import (
+from idb.bot.dialogs.admins.mailings.create import (
choose_user_types,
confirm,
input_content,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/choose_user_types.py b/idb/bot/dialogs/admins/mailings/create/choose_user_types.py
similarity index 77%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/create/choose_user_types.py
rename to idb/bot/dialogs/admins/mailings/create/choose_user_types.py
index f742a3f..49b078d 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/choose_user_types.py
+++ b/idb/bot/dialogs/admins/mailings/create/choose_user_types.py
@@ -5,15 +5,13 @@
from aiogram_dialog.widgets.kbd import Button, Multiselect, Next, ScrollingGroup
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateMailingSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.logic.storage import Storage
+from idb.bot.dialogs.admins.states import CreateMailingSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.utils.cache import AbstractBotCache
-async def get_user_types_data(
- dialog_manager: DialogManager, storage: Storage, **kwargs: Any
-) -> dict[str, Any]:
- user_types = await storage.get_user_types()
+async def get_user_types_data(cache: AbstractBotCache, **kwargs: Any) -> dict[str, Any]:
+ user_types = await cache.get_user_types()
return {
"user_types": user_types.values(),
}
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/confirm.py b/idb/bot/dialogs/admins/mailings/create/confirm.py
similarity index 86%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/create/confirm.py
rename to idb/bot/dialogs/admins/mailings/create/confirm.py
index 7f17dd6..0b4ffcf 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/confirm.py
+++ b/idb/bot/dialogs/admins/mailings/create/confirm.py
@@ -7,11 +7,11 @@
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateMailingSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.mailing import save_mailing
-from inclusive_dance_bot.logic.storage import Storage
+from idb.bot.dialogs.admins.states import CreateMailingSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.db.uow import UnitOfWork
+from idb.logic.mailing import save_mailing
+from idb.utils.cache import AbstractBotCache
def parse_dt(
@@ -48,11 +48,11 @@ async def on_click(
async def get_mailing_data(
- dialog_manager: DialogManager, storage: Storage, **kwargs: Any
+ dialog_manager: DialogManager, cache: AbstractBotCache, **kwargs: Any
) -> dict[str, Any]:
user_types = filter(
lambda ut: ut.id in dialog_manager.dialog_data["user_types"],
- (await storage.get_user_types()).values(),
+ (await cache.get_user_types()).values(),
)
t = dialog_manager.dialog_data.get("time")
d = dialog_manager.dialog_data.get("date")
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_content.py b/idb/bot/dialogs/admins/mailings/create/input_content.py
similarity index 78%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_content.py
rename to idb/bot/dialogs/admins/mailings/create/input_content.py
index 534e2f2..aeea263 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_content.py
+++ b/idb/bot/dialogs/admins/mailings/create/input_content.py
@@ -3,9 +3,9 @@
from aiogram_dialog.widgets.input import ManagedTextInput, TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateMailingSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.bot.dialogs.utils.validators import validate_length
+from idb.bot.dialogs.admins.states import CreateMailingSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.bot.dialogs.utils.validators import validate_length
MESSAGE = "Введите сообщение\n\nОграничение - 3072 символов"
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_date.py b/idb/bot/dialogs/admins/mailings/create/input_date.py
similarity index 78%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_date.py
rename to idb/bot/dialogs/admins/mailings/create/input_date.py
index c2f3696..b1f9105 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_date.py
+++ b/idb/bot/dialogs/admins/mailings/create/input_date.py
@@ -5,9 +5,9 @@
from aiogram_dialog.widgets.input import ManagedTextInput, TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateMailingSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.bot.dialogs.utils.validators import validate_date
+from idb.bot.dialogs.admins.states import CreateMailingSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.bot.dialogs.utils.validators import validate_date
MESSAGE = "Введите дату отправки сообщения\n\nФормат: ДД.ММ.ГГГГ"
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_is_immediately.py b/idb/bot/dialogs/admins/mailings/create/input_is_immediately.py
similarity index 76%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_is_immediately.py
rename to idb/bot/dialogs/admins/mailings/create/input_is_immediately.py
index ee0f3ca..526e233 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_is_immediately.py
+++ b/idb/bot/dialogs/admins/mailings/create/input_is_immediately.py
@@ -2,8 +2,8 @@
from aiogram_dialog.widgets.kbd import Next, Row, SwitchTo
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateMailingSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
+from idb.bot.dialogs.admins.states import CreateMailingSG
+from idb.bot.dialogs.utils.buttons import BACK
window = Window(
Const("Когда отправить?"),
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_time.py b/idb/bot/dialogs/admins/mailings/create/input_time.py
similarity index 79%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_time.py
rename to idb/bot/dialogs/admins/mailings/create/input_time.py
index ffa1838..837abf4 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_time.py
+++ b/idb/bot/dialogs/admins/mailings/create/input_time.py
@@ -5,9 +5,9 @@
from aiogram_dialog.widgets.input import ManagedTextInput, TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateMailingSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.bot.dialogs.utils.validators import validate_time
+from idb.bot.dialogs.admins.states import CreateMailingSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.bot.dialogs.utils.validators import validate_time
MESSAGE = "Введите время отправки сообщения по Москве\n\nФормат: ЧЧ:ММ"
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_title.py b/idb/bot/dialogs/admins/mailings/create/input_title.py
similarity index 78%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_title.py
rename to idb/bot/dialogs/admins/mailings/create/input_title.py
index 6eb152b..5321c41 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/create/input_title.py
+++ b/idb/bot/dialogs/admins/mailings/create/input_title.py
@@ -3,9 +3,9 @@
from aiogram_dialog.widgets.input import ManagedTextInput, TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateMailingSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.bot.dialogs.utils.validators import validate_length
+from idb.bot.dialogs.admins.states import CreateMailingSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.bot.dialogs.utils.validators import validate_length
MESSAGE = "Введите тему сообщения.\n\nОграничение - 512 символов"
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/read/__init__.py b/idb/bot/dialogs/admins/mailings/read/__init__.py
similarity index 56%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/read/__init__.py
rename to idb/bot/dialogs/admins/mailings/read/__init__.py
index f4ad2ac..123863d 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/read/__init__.py
+++ b/idb/bot/dialogs/admins/mailings/read/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.admins.mailings.read import item, items, menu
+from idb.bot.dialogs.admins.mailings.read import item, items, menu
dialog = Dialog(
menu.window,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/read/item.py b/idb/bot/dialogs/admins/mailings/read/item.py
similarity index 84%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/read/item.py
rename to idb/bot/dialogs/admins/mailings/read/item.py
index 4509b22..acd7389 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/read/item.py
+++ b/idb/bot/dialogs/admins/mailings/read/item.py
@@ -5,11 +5,11 @@
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import CancelMailingSG, MailingsSG
-from inclusive_dance_bot.bot.dialogs.utils import start_with_data
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.enums import MailingStatus
+from idb.bot.dialogs.admins.states import CancelMailingSG, MailingsSG
+from idb.bot.dialogs.utils import start_with_data
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.db.uow import UnitOfWork
+from idb.generals.enums import MailingStatus
TEMPLATE_MESSAGE = """
Рассылка
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/read/items.py b/idb/bot/dialogs/admins/mailings/read/items.py
similarity index 87%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/read/items.py
rename to idb/bot/dialogs/admins/mailings/read/items.py
index bd5aa16..0efadd9 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/read/items.py
+++ b/idb/bot/dialogs/admins/mailings/read/items.py
@@ -6,10 +6,10 @@
from aiogram_dialog.widgets.kbd import Button, ScrollingGroup, Select
from aiogram_dialog.widgets.text import Const, Format, List
-from inclusive_dance_bot.bot.dialogs.admins.states import MailingsSG
-from inclusive_dance_bot.bot.dialogs.utils import sync_scroll
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.db.uow.main import UnitOfWork
+from idb.bot.dialogs.admins.states import MailingsSG
+from idb.bot.dialogs.utils import sync_scroll
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.db.uow import UnitOfWork
MAILING_TEMPLATE = """\
[{pos}] {item.title}
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/read/menu.py b/idb/bot/dialogs/admins/mailings/read/menu.py
similarity index 87%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/read/menu.py
rename to idb/bot/dialogs/admins/mailings/read/menu.py
index 8fbbf64..a5a078d 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/read/menu.py
+++ b/idb/bot/dialogs/admins/mailings/read/menu.py
@@ -6,8 +6,8 @@
from aiogram_dialog.widgets.kbd import Button, Next, Start
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateMailingSG, MailingsSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
+from idb.bot.dialogs.admins.states import CreateMailingSG, MailingsSG
+from idb.bot.dialogs.utils.buttons import CANCEL
def next_with_data(data: dict[str, Any]) -> Callable:
diff --git a/inclusive_dance_bot/db/repositories/__init__.py b/idb/bot/dialogs/admins/main_menu/__init__.py
similarity index 100%
rename from inclusive_dance_bot/db/repositories/__init__.py
rename to idb/bot/dialogs/admins/main_menu/__init__.py
diff --git a/idb/bot/dialogs/admins/main_menu/dialog.py b/idb/bot/dialogs/admins/main_menu/dialog.py
new file mode 100644
index 0000000..a846cb2
--- /dev/null
+++ b/idb/bot/dialogs/admins/main_menu/dialog.py
@@ -0,0 +1,5 @@
+from aiogram_dialog import Dialog
+
+from idb.bot.dialogs.admins.main_menu.windows import menu
+
+dialog = Dialog(menu.window)
diff --git a/inclusive_dance_bot/db/uow/__init__.py b/idb/bot/dialogs/admins/main_menu/windows/__init__.py
similarity index 100%
rename from inclusive_dance_bot/db/uow/__init__.py
rename to idb/bot/dialogs/admins/main_menu/windows/__init__.py
diff --git a/idb/bot/dialogs/admins/main_menu/windows/menu.py b/idb/bot/dialogs/admins/main_menu/windows/menu.py
new file mode 100644
index 0000000..41ff67f
--- /dev/null
+++ b/idb/bot/dialogs/admins/main_menu/windows/menu.py
@@ -0,0 +1,76 @@
+from typing import Any
+
+from aiogram_dialog import DialogManager, Window
+from aiogram_dialog.widgets.kbd import Start
+from aiogram_dialog.widgets.text import Const, Format
+
+from idb.bot.dialogs.admins.states import (
+ AdminMainMenuSG,
+ AdminSubmenuSG,
+ FeedbackItemsSG,
+ MailingsSG,
+ ManageAdminSG,
+ ReadUrlSG,
+)
+from idb.db.uow import UnitOfWork
+from idb.generals.models.user import BotUser
+
+MESSAGE_TEMPLATE = """
+В боте зарегистрировано {total_users_count} пользователей.
+
+Получено всего {total_feedbacks_count} ОС.
+
+Запланировано/отправлено всего {total_mailings_count} рассылок.
+"""
+
+
+def when_(data: dict, widget: Any, dialog_manager: DialogManager) -> bool:
+ user: BotUser = data["middleware_data"]["user"]
+ return user.is_superuser
+
+
+async def get_user_stat(
+ dialog_manager: DialogManager, uow: UnitOfWork, **kwargs: Any
+) -> dict[str, Any]:
+ total_users_count = await uow.users.total_count()
+ total_feedbacks_count = await uow.feedbacks.total_count()
+ total_mailings_count = await uow.mailings.total_count()
+ return {
+ "total_users_count": total_users_count,
+ "total_feedbacks_count": total_feedbacks_count,
+ "total_mailings_count": total_mailings_count,
+ }
+
+
+window = Window(
+ Const("Меню администратора"),
+ Format(MESSAGE_TEMPLATE),
+ Start(
+ id="feedbacks",
+ text=Const("🖋 Обратная связь от пользователей"),
+ state=FeedbackItemsSG.menu,
+ ),
+ Start(
+ id="mailings",
+ text=Const("📬 Рассылки"),
+ state=MailingsSG.menu,
+ ),
+ Start(
+ id="manage_submenu_id",
+ text=Const("📑 Управление подменю"),
+ state=AdminSubmenuSG.items,
+ ),
+ Start(
+ id="manage_url_id",
+ text=Const("🔗 Управление ссылками"),
+ state=ReadUrlSG.items,
+ ),
+ Start(
+ id="manage_admin_id",
+ text=Const("👮♀️ Управление администраторами"),
+ state=ManageAdminSG.items,
+ when=when_,
+ ),
+ state=AdminMainMenuSG.menu,
+ getter=get_user_stat,
+)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/__init__.py b/idb/bot/dialogs/admins/manage_admins/__init__.py
similarity index 60%
rename from inclusive_dance_bot/bot/dialogs/admins/manage_admins/__init__.py
rename to idb/bot/dialogs/admins/manage_admins/__init__.py
index 5dd57a1..94b0a71 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/__init__.py
+++ b/idb/bot/dialogs/admins/manage_admins/__init__.py
@@ -1,6 +1,6 @@
from aiogram import Router
-from inclusive_dance_bot.bot.dialogs.admins.manage_admins import add, delete, read
+from idb.bot.dialogs.admins.manage_admins import add, delete, read
router = Router()
router.include_routers(
diff --git a/idb/bot/dialogs/admins/manage_admins/add/__init__.py b/idb/bot/dialogs/admins/manage_admins/add/__init__.py
new file mode 100644
index 0000000..82f6df0
--- /dev/null
+++ b/idb/bot/dialogs/admins/manage_admins/add/__init__.py
@@ -0,0 +1,7 @@
+from aiogram_dialog import Dialog
+
+from idb.bot.dialogs.admins.manage_admins.add import input_username
+
+dialog = Dialog(
+ input_username.window,
+)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/add/input_username.py b/idb/bot/dialogs/admins/manage_admins/add/input_username.py
similarity index 77%
rename from inclusive_dance_bot/bot/dialogs/admins/manage_admins/add/input_username.py
rename to idb/bot/dialogs/admins/manage_admins/add/input_username.py
index 43a3626..834f95e 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/add/input_username.py
+++ b/idb/bot/dialogs/admins/manage_admins/add/input_username.py
@@ -3,12 +3,12 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import AddAdminSG
-from inclusive_dance_bot.bot.dialogs.messages import ADD_ADMIN_MESSAGE
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.exceptions import UserNotFoundError
-from inclusive_dance_bot.logic.user import add_user_to_admins
+from idb.bot.dialogs.admins.states import AddAdminSG
+from idb.bot.dialogs.messages import ADD_ADMIN_MESSAGE
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.db.uow import UnitOfWork
+from idb.exceptions import UserNotFoundError
+from idb.logic.users import add_user_to_admins
async def on_success(
diff --git a/idb/bot/dialogs/admins/manage_admins/delete/__init__.py b/idb/bot/dialogs/admins/manage_admins/delete/__init__.py
new file mode 100644
index 0000000..425d341
--- /dev/null
+++ b/idb/bot/dialogs/admins/manage_admins/delete/__init__.py
@@ -0,0 +1,5 @@
+from aiogram_dialog import Dialog
+
+from idb.bot.dialogs.admins.manage_admins.delete import confirm
+
+dialog = Dialog(confirm.window)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/delete/confirm.py b/idb/bot/dialogs/admins/manage_admins/delete/confirm.py
similarity index 81%
rename from inclusive_dance_bot/bot/dialogs/admins/manage_admins/delete/confirm.py
rename to idb/bot/dialogs/admins/manage_admins/delete/confirm.py
index 01fb1e0..3d8f4b3 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/delete/confirm.py
+++ b/idb/bot/dialogs/admins/manage_admins/delete/confirm.py
@@ -5,11 +5,11 @@
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import DeleteAdminSG
-from inclusive_dance_bot.bot.dialogs.users.states import MainMenuSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.user import delete_from_admins
+from idb.bot.dialogs.admins.states import DeleteAdminSG
+from idb.bot.dialogs.users.states import MainMenuSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.db.uow import UnitOfWork
+from idb.logic.users import delete_from_admins
async def on_click(
diff --git a/idb/bot/dialogs/admins/manage_admins/read/__init__.py b/idb/bot/dialogs/admins/manage_admins/read/__init__.py
new file mode 100644
index 0000000..afc55fe
--- /dev/null
+++ b/idb/bot/dialogs/admins/manage_admins/read/__init__.py
@@ -0,0 +1,7 @@
+from aiogram_dialog import Dialog
+
+from idb.bot.dialogs.admins.manage_admins.read import items
+
+dialog = Dialog(
+ items.window,
+)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/read/items.py b/idb/bot/dialogs/admins/manage_admins/read/items.py
similarity index 84%
rename from inclusive_dance_bot/bot/dialogs/admins/manage_admins/read/items.py
rename to idb/bot/dialogs/admins/manage_admins/read/items.py
index 09ce1db..d2fba28 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/read/items.py
+++ b/idb/bot/dialogs/admins/manage_admins/read/items.py
@@ -5,17 +5,18 @@
from aiogram_dialog.widgets.kbd import Button, ScrollingGroup, Select, Start
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import (
+from idb.bot.dialogs.admins.states import (
AddAdminSG,
DeleteAdminSG,
ManageAdminSG,
)
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.db.uow.main import UnitOfWork
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.db.uow import UnitOfWork
async def get_admins(uow: UnitOfWork, **kwargs: Any) -> dict[str, Any]:
- admins = await uow.users.get_admin_list()
+ admins = await uow.users.get_admin_list(include_superusers=False)
+
return {"admins": admins}
diff --git a/idb/bot/dialogs/admins/router.py b/idb/bot/dialogs/admins/router.py
new file mode 100644
index 0000000..b23924e
--- /dev/null
+++ b/idb/bot/dialogs/admins/router.py
@@ -0,0 +1,24 @@
+from aiogram import Router
+
+from idb.bot.dialogs.admins import (
+ mailings,
+ manage_admins,
+ submenu,
+ url,
+)
+from idb.bot.dialogs.admins.feedbacks.router import (
+ router as feedbacks_router,
+)
+from idb.bot.dialogs.admins.main_menu.dialog import (
+ dialog as main_menu_dialog,
+)
+
+dialog_router = Router(name="admin_router")
+dialog_router.include_routers(
+ feedbacks_router,
+ main_menu_dialog,
+ submenu.router,
+ url.router,
+ manage_admins.router,
+ mailings.router,
+)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/states.py b/idb/bot/dialogs/admins/states.py
similarity index 93%
rename from inclusive_dance_bot/bot/dialogs/admins/states.py
rename to idb/bot/dialogs/admins/states.py
index 12aa3c2..bb30311 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/states.py
+++ b/idb/bot/dialogs/admins/states.py
@@ -75,14 +75,16 @@ class CancelMailingSG(StatesGroup):
confirm = State()
-class FeedbackAsnwerSG(StatesGroup):
+class FeedbackAnswerSG(StatesGroup):
input_message = State()
confirm = State()
class FeedbackItemsSG(StatesGroup):
+ menu = State()
new = State()
- archive = State()
+ archived = State()
+ item = State()
class CreateMailingSG(StatesGroup):
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/__init__.py b/idb/bot/dialogs/admins/submenu/__init__.py
similarity index 63%
rename from inclusive_dance_bot/bot/dialogs/admins/url/__init__.py
rename to idb/bot/dialogs/admins/submenu/__init__.py
index c375d5a..4491a79 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/__init__.py
+++ b/idb/bot/dialogs/admins/submenu/__init__.py
@@ -1,6 +1,6 @@
from aiogram import Router
-from inclusive_dance_bot.bot.dialogs.admins.url import create, delete, read, update
+from idb.bot.dialogs.admins.submenu import create, delete, read, update
router = Router()
router.include_routers(
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/__init__.py b/idb/bot/dialogs/admins/submenu/create/__init__.py
similarity index 79%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/create/__init__.py
rename to idb/bot/dialogs/admins/submenu/create/__init__.py
index 7c2a0ab..e1a725a 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/__init__.py
+++ b/idb/bot/dialogs/admins/submenu/create/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.admins.submenu.create import (
+from idb.bot.dialogs.admins.submenu.create import (
confirm,
input_button_text,
input_message,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/confirm.py b/idb/bot/dialogs/admins/submenu/create/confirm.py
similarity index 78%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/create/confirm.py
rename to idb/bot/dialogs/admins/submenu/create/confirm.py
index 68294fa..1264b96 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/confirm.py
+++ b/idb/bot/dialogs/admins/submenu/create/confirm.py
@@ -3,24 +3,24 @@
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import (
+from idb.bot.dialogs.admins.states import (
AdminMainMenuSG,
CreateSubmenuSG,
)
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.submenu import create_submenu
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.db.uow import UnitOfWork
+from idb.logic.submenu import create_submenu
+from idb.utils.cache import AbstractBotCache
async def on_click(
c: CallbackQuery, button: Button, dialog_manager: DialogManager
) -> None:
- storage: Storage = dialog_manager.middleware_data["storage"]
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
await create_submenu(
uow=uow,
- storage=storage,
+ cache=cache,
type=dialog_manager.dialog_data["type"],
weight=dialog_manager.dialog_data["weight"],
message=dialog_manager.dialog_data["message"],
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_button_text.py b/idb/bot/dialogs/admins/submenu/create/input_button_text.py
similarity index 77%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_button_text.py
rename to idb/bot/dialogs/admins/submenu/create/input_button_text.py
index 703b3cf..7be0e23 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_button_text.py
+++ b/idb/bot/dialogs/admins/submenu/create/input_button_text.py
@@ -3,9 +3,9 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateSubmenuSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.bot.dialogs.utils.validators import validate_length
+from idb.bot.dialogs.admins.states import CreateSubmenuSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.bot.dialogs.utils.validators import validate_length
MESSAGE = "Введите текст кнопки.\n\nОграничение - 64 символа"
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_message.py b/idb/bot/dialogs/admins/submenu/create/input_message.py
similarity index 72%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_message.py
rename to idb/bot/dialogs/admins/submenu/create/input_message.py
index 4383e21..3375483 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_message.py
+++ b/idb/bot/dialogs/admins/submenu/create/input_message.py
@@ -1,10 +1,10 @@
from aiogram.types import Message
from aiogram_dialog import DialogManager, Window
from aiogram_dialog.widgets.input import TextInput
-from aiogram_dialog.widgets.text import Format
+from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateSubmenuSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
+from idb.bot.dialogs.admins.states import CreateSubmenuSG
+from idb.bot.dialogs.utils.buttons import BACK
TEMPLATE_MESSAGE = "Введите шаблон сообщения"
@@ -17,7 +17,7 @@ async def on_success(
window = Window(
- Format(TEMPLATE_MESSAGE),
+ Const(TEMPLATE_MESSAGE),
TextInput(id="input_message", on_success=on_success), # type: ignore[arg-type]
BACK,
state=CreateSubmenuSG.message,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_type.py b/idb/bot/dialogs/admins/submenu/create/input_type.py
similarity index 77%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_type.py
rename to idb/bot/dialogs/admins/submenu/create/input_type.py
index 95ba905..b2cbdab 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_type.py
+++ b/idb/bot/dialogs/admins/submenu/create/input_type.py
@@ -3,11 +3,11 @@
from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Column, Select
-from aiogram_dialog.widgets.text import Format
+from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateSubmenuSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.enums import SubmenuType
+from idb.bot.dialogs.admins.states import CreateSubmenuSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.generals.enums import SubmenuType
async def get_submenu_data(**kwargs: Any) -> dict[str, Any]:
@@ -29,7 +29,7 @@ async def on_click(
TEMPLATE_MESSAGE = "Выберите тип подменю"
window = Window(
- Format(TEMPLATE_MESSAGE),
+ Const(TEMPLATE_MESSAGE),
Column(
Select(
id="submenu_types",
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_weight.py b/idb/bot/dialogs/admins/submenu/create/input_weight.py
similarity index 76%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_weight.py
rename to idb/bot/dialogs/admins/submenu/create/input_weight.py
index ccb3070..8e73de9 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/create/input_weight.py
+++ b/idb/bot/dialogs/admins/submenu/create/input_weight.py
@@ -1,10 +1,10 @@
from aiogram.types import Message
from aiogram_dialog import DialogManager, Window
from aiogram_dialog.widgets.input import TextInput
-from aiogram_dialog.widgets.text import Format
+from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateSubmenuSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
+from idb.bot.dialogs.admins.states import CreateSubmenuSG
+from idb.bot.dialogs.utils.buttons import BACK
TEMPLATE_MESSAGE = (
"Введите вес подменю.\n\n"
@@ -20,7 +20,7 @@ async def on_success(
window = Window(
- Format(TEMPLATE_MESSAGE),
+ Const(TEMPLATE_MESSAGE),
TextInput(id="input_weight", on_success=on_success, type_factory=int), # type: ignore[arg-type]
BACK,
state=CreateSubmenuSG.weight,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/delete/__init__.py b/idb/bot/dialogs/admins/submenu/delete/__init__.py
similarity index 50%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/delete/__init__.py
rename to idb/bot/dialogs/admins/submenu/delete/__init__.py
index 0201ded..70f5c65 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/delete/__init__.py
+++ b/idb/bot/dialogs/admins/submenu/delete/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.admins.submenu.delete import confirm
+from idb.bot.dialogs.admins.submenu.delete import confirm
dialog = Dialog(
confirm.window,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/delete/confirm.py b/idb/bot/dialogs/admins/submenu/delete/confirm.py
similarity index 66%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/delete/confirm.py
rename to idb/bot/dialogs/admins/submenu/delete/confirm.py
index def7fea..709e869 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/delete/confirm.py
+++ b/idb/bot/dialogs/admins/submenu/delete/confirm.py
@@ -3,24 +3,24 @@
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import (
+from idb.bot.dialogs.admins.states import (
AdminMainMenuSG,
DeleteSubmenuSG,
)
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.bot.dialogs.utils.getters import get_submenu_data
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.submenu import delete_submenu_by_id
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.bot.dialogs.utils.getters import get_submenu_data
+from idb.db.uow import UnitOfWork
+from idb.logic.submenu import delete_submenu_by_id
+from idb.utils.cache import AbstractBotCache
async def on_click(
c: CallbackQuery, button: Button, dialog_manager: DialogManager
) -> None:
- storage: Storage = dialog_manager.middleware_data["storage"]
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
submenu_id = dialog_manager.start_data["submenu_id"]
- await delete_submenu_by_id(uow=uow, storage=storage, submenu_id=submenu_id)
+ await delete_submenu_by_id(uow=uow, cache=cache, submenu_id=submenu_id)
dialog_manager.show_mode = ShowMode.SEND
await c.bot.send_message(c.from_user.id, text="Подменю было удалено") # type: ignore[union-attr]
await dialog_manager.start(state=AdminMainMenuSG.menu, mode=StartMode.RESET_STACK)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/read/__init__.py b/idb/bot/dialogs/admins/submenu/read/__init__.py
similarity index 54%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/read/__init__.py
rename to idb/bot/dialogs/admins/submenu/read/__init__.py
index ee4827b..5a79e6d 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/read/__init__.py
+++ b/idb/bot/dialogs/admins/submenu/read/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.admins.submenu.read import item, items
+from idb.bot.dialogs.admins.submenu.read import item, items
dialog = Dialog(
items.window,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/read/item.py b/idb/bot/dialogs/admins/submenu/read/item.py
similarity index 63%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/read/item.py
rename to idb/bot/dialogs/admins/submenu/read/item.py
index 53f9e78..c3bd8b6 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/read/item.py
+++ b/idb/bot/dialogs/admins/submenu/read/item.py
@@ -1,30 +1,44 @@
from typing import Any
from aiogram_dialog import DialogManager, Window
-from aiogram_dialog.widgets.kbd import Back, Button
-from aiogram_dialog.widgets.text import Const, Format
+from aiogram_dialog.widgets.kbd import Button
+from aiogram_dialog.widgets.text import Const, Format, Jinja
-from inclusive_dance_bot.bot.dialogs.admins.states import (
+from idb.bot.dialogs.admins.states import (
AdminSubmenuSG,
ChangeSubmenuSG,
DeleteSubmenuSG,
)
-from inclusive_dance_bot.bot.dialogs.messages import SUBMENU_TEMPLATE
-from inclusive_dance_bot.bot.dialogs.utils import start_with_data
-from inclusive_dance_bot.logic.storage import Storage
+from idb.bot.dialogs.utils import start_with_data
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.utils.cache import AbstractBotCache
SUBMENU_ID = "submenu_id"
+SUBMENU_TEMPLATE = """\
+Подменю {submenu.id}
+
+Тип: {submenu.type}
+Текст кнопки: {submenu.button_text}
+Вес: {submenu.weight}
+"""
+SUBMENU_JINJA = """\
+Значение:
+
+{{ submenu.message }}
+"""
+
async def get_data(
- storage: Storage, dialog_manager: DialogManager, **kwargs: Any
+ cache: AbstractBotCache, dialog_manager: DialogManager, **kwargs: Any
) -> dict[str, Any]:
- s = await storage.get_submenu_by_id(dialog_manager.dialog_data[SUBMENU_ID])
+ s = await cache.get_submenu_by_id(dialog_manager.dialog_data[SUBMENU_ID])
return {"submenu": s}
window = Window(
Format(SUBMENU_TEMPLATE),
+ Jinja(SUBMENU_JINJA),
Button(
Const("Изменить тип"),
id="change_submenu_type",
@@ -50,7 +64,7 @@ async def get_data(
id="delete_submenu",
on_click=start_with_data(state=DeleteSubmenuSG.confirm, field=SUBMENU_ID),
),
- Back(Const("Назад")),
+ BACK,
state=AdminSubmenuSG.info,
getter=get_data,
)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/read/items.py b/idb/bot/dialogs/admins/submenu/read/items.py
similarity index 78%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/read/items.py
rename to idb/bot/dialogs/admins/submenu/read/items.py
index 102aa2d..1b2a75a 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/read/items.py
+++ b/idb/bot/dialogs/admins/submenu/read/items.py
@@ -5,20 +5,22 @@
from aiogram_dialog.widgets.kbd import Button, ScrollingGroup, Select, Start
from aiogram_dialog.widgets.text import Const, Format, List
-from inclusive_dance_bot.bot.dialogs.admins.states import (
+from idb.bot.dialogs.admins.states import (
AdminSubmenuSG,
CreateSubmenuSG,
)
-from inclusive_dance_bot.bot.dialogs.utils import sync_scroll
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.logic.storage import Storage
+from idb.bot.dialogs.utils import sync_scroll
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.utils.cache import AbstractBotCache
SCROLL_KBD_ID = "submenu_scroll_id"
SCROLL_MESSAGE_ID = "submenu_message_scroll_id"
-async def get_submenu_list_data(storage: Storage, **kwargs: Any) -> dict[str, Any]:
- return {"submenus": list((await storage.get_submenus()).values())}
+async def get_submenu_list_data(
+ cache: AbstractBotCache, **kwargs: Any
+) -> dict[str, Any]:
+ return {"submenus": list((await cache.get_submenus()).values())}
async def open_submenu(
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/update/__init__.py b/idb/bot/dialogs/admins/submenu/update/__init__.py
similarity index 78%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/update/__init__.py
rename to idb/bot/dialogs/admins/submenu/update/__init__.py
index e0009a4..9a96a24 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/update/__init__.py
+++ b/idb/bot/dialogs/admins/submenu/update/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.admins.submenu.update import (
+from idb.bot.dialogs.admins.submenu.update import (
change_button_text,
change_message,
change_type,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_button_text.py b/idb/bot/dialogs/admins/submenu/update/change_button_text.py
similarity index 63%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_button_text.py
rename to idb/bot/dialogs/admins/submenu/update/change_button_text.py
index e3c3f37..c2aa140 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_button_text.py
+++ b/idb/bot/dialogs/admins/submenu/update/change_button_text.py
@@ -3,13 +3,13 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Format
-from inclusive_dance_bot.bot.dialogs.admins.states import ChangeSubmenuSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.bot.dialogs.utils.getters import get_submenu_data
-from inclusive_dance_bot.bot.dialogs.utils.validators import validate_length
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.submenu import update_submenu_by_id
+from idb.bot.dialogs.admins.states import ChangeSubmenuSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.bot.dialogs.utils.getters import get_submenu_data
+from idb.bot.dialogs.utils.validators import validate_length
+from idb.db.uow import UnitOfWork
+from idb.logic.submenu import update_submenu_by_id
+from idb.utils.cache import AbstractBotCache
TEMPLATE_MESSAGE = "Введите новый текст кнопки\n\nТекущее: {submenu.button_text}"
@@ -19,10 +19,10 @@ async def on_success(
) -> None:
submenu_id = dialog_manager.start_data["submenu_id"]
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
- storage: Storage = dialog_manager.middleware_data["storage"]
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
await update_submenu_by_id(
uow=uow,
- storage=storage,
+ cache=cache,
submenu_id=submenu_id,
button_text=value,
)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_message.py b/idb/bot/dialogs/admins/submenu/update/change_message.py
similarity index 50%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_message.py
rename to idb/bot/dialogs/admins/submenu/update/change_message.py
index 46843c1..985fc0c 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_message.py
+++ b/idb/bot/dialogs/admins/submenu/update/change_message.py
@@ -1,16 +1,18 @@
from aiogram.types import Message
from aiogram_dialog import DialogManager, Window
from aiogram_dialog.widgets.input import TextInput
-from aiogram_dialog.widgets.text import Format
+from aiogram_dialog.widgets.text import Jinja
-from inclusive_dance_bot.bot.dialogs.admins.states import ChangeSubmenuSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.bot.dialogs.utils.getters import get_submenu_data
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.submenu import update_submenu_by_id
+from idb.bot.dialogs.admins.states import ChangeSubmenuSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.bot.dialogs.utils.getters import get_submenu_data
+from idb.db.uow import UnitOfWork
+from idb.logic.submenu import update_submenu_by_id
+from idb.utils.cache import AbstractBotCache
-TEMPLATE_MESSAGE = "Введите новый шаблон сообщения\n\nТекущее: {submenu.message}"
+TEMPLATE_MESSAGE = (
+ "Введите новый шаблон сообщения\nТекущее:\n\n{{submenu.message}}
"
+)
async def on_success(
@@ -18,10 +20,10 @@ async def on_success(
) -> None:
submenu_id = dialog_manager.start_data["submenu_id"]
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
- storage: Storage = dialog_manager.middleware_data["storage"]
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
await update_submenu_by_id(
uow=uow,
- storage=storage,
+ cache=cache,
submenu_id=submenu_id,
message=value,
)
@@ -29,7 +31,7 @@ async def on_success(
window = Window(
- Format(TEMPLATE_MESSAGE),
+ Jinja(TEMPLATE_MESSAGE),
TextInput(id="input_message", on_success=on_success), # type: ignore[arg-type]
CANCEL,
state=ChangeSubmenuSG.message,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_type.py b/idb/bot/dialogs/admins/submenu/update/change_type.py
similarity index 69%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_type.py
rename to idb/bot/dialogs/admins/submenu/update/change_type.py
index a44dde8..4e3cd0e 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_type.py
+++ b/idb/bot/dialogs/admins/submenu/update/change_type.py
@@ -5,19 +5,19 @@
from aiogram_dialog.widgets.kbd import Button, Column, Select
from aiogram_dialog.widgets.text import Format
-from inclusive_dance_bot.bot.dialogs.admins.states import ChangeSubmenuSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.enums import SubmenuType
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.submenu import update_submenu_by_id
+from idb.bot.dialogs.admins.states import ChangeSubmenuSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.db.uow import UnitOfWork
+from idb.generals.enums import SubmenuType
+from idb.logic.submenu import update_submenu_by_id
+from idb.utils.cache import AbstractBotCache
async def get_submenu_data(
- storage: Storage, dialog_manager: DialogManager, **kwargs: Any
+ cache: AbstractBotCache, dialog_manager: DialogManager, **kwargs: Any
) -> dict[str, Any]:
return {
- "submenu": await storage.get_submenu_by_id(
+ "submenu": await cache.get_submenu_by_id(
dialog_manager.start_data["submenu_id"]
),
"submenu_types": list(SubmenuType),
@@ -32,10 +32,10 @@ async def on_click(
) -> None:
submenu_id = dialog_manager.start_data["submenu_id"]
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
- storage: Storage = dialog_manager.middleware_data["storage"]
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
await update_submenu_by_id(
uow=uow,
- storage=storage,
+ cache=cache,
submenu_id=submenu_id,
type_=SubmenuType(type_),
)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_weight.py b/idb/bot/dialogs/admins/submenu/update/change_weight.py
similarity index 64%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_weight.py
rename to idb/bot/dialogs/admins/submenu/update/change_weight.py
index 45fc423..c60c2e9 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/update/change_weight.py
+++ b/idb/bot/dialogs/admins/submenu/update/change_weight.py
@@ -3,12 +3,12 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Format
-from inclusive_dance_bot.bot.dialogs.admins.states import ChangeSubmenuSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.bot.dialogs.utils.getters import get_submenu_data
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.submenu import update_submenu_by_id
+from idb.bot.dialogs.admins.states import ChangeSubmenuSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.bot.dialogs.utils.getters import get_submenu_data
+from idb.db.uow import UnitOfWork
+from idb.logic.submenu import update_submenu_by_id
+from idb.utils.cache import AbstractBotCache
TEMPLATE_MESSAGE = (
"Введите новое значение веса. "
@@ -22,9 +22,9 @@ async def on_success(
) -> None:
submenu_id = dialog_manager.start_data["submenu_id"]
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
- storage: Storage = dialog_manager.middleware_data["storage"]
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
await update_submenu_by_id(
- uow=uow, storage=storage, submenu_id=submenu_id, weight=weight
+ uow=uow, cache=cache, submenu_id=submenu_id, weight=weight
)
await dialog_manager.done()
diff --git a/inclusive_dance_bot/bot/dialogs/admins/submenu/__init__.py b/idb/bot/dialogs/admins/url/__init__.py
similarity index 62%
rename from inclusive_dance_bot/bot/dialogs/admins/submenu/__init__.py
rename to idb/bot/dialogs/admins/url/__init__.py
index f876ed3..e6ac2e3 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/submenu/__init__.py
+++ b/idb/bot/dialogs/admins/url/__init__.py
@@ -1,6 +1,6 @@
from aiogram import Router
-from inclusive_dance_bot.bot.dialogs.admins.submenu import create, delete, read, update
+from idb.bot.dialogs.admins.url import create, delete, read, update
router = Router()
router.include_routers(
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/create/__init__.py b/idb/bot/dialogs/admins/url/create/__init__.py
similarity index 72%
rename from inclusive_dance_bot/bot/dialogs/admins/url/create/__init__.py
rename to idb/bot/dialogs/admins/url/create/__init__.py
index 09ea351..dbae942 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/create/__init__.py
+++ b/idb/bot/dialogs/admins/url/create/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.admins.url.create import (
+from idb.bot.dialogs.admins.url.create import (
confirm,
input_slug,
input_value,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/create/confirm.py b/idb/bot/dialogs/admins/url/create/confirm.py
similarity index 71%
rename from inclusive_dance_bot/bot/dialogs/admins/url/create/confirm.py
rename to idb/bot/dialogs/admins/url/create/confirm.py
index 10e73b2..1087b5c 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/create/confirm.py
+++ b/idb/bot/dialogs/admins/url/create/confirm.py
@@ -3,21 +3,21 @@
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import AdminMainMenuSG, CreateUrlSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.url import create_url
+from idb.bot.dialogs.admins.states import AdminMainMenuSG, CreateUrlSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.db.uow import UnitOfWork
+from idb.logic.url import create_url
+from idb.utils.cache import AbstractBotCache
async def on_click(
c: CallbackQuery, button: Button, dialog_manager: DialogManager
) -> None:
- storage: Storage = dialog_manager.middleware_data["storage"]
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
await create_url(
uow=uow,
- storage=storage,
+ cache=cache,
slug=dialog_manager.dialog_data["slug"],
value=dialog_manager.dialog_data["value"],
)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/create/input_slug.py b/idb/bot/dialogs/admins/url/create/input_slug.py
similarity index 73%
rename from inclusive_dance_bot/bot/dialogs/admins/url/create/input_slug.py
rename to idb/bot/dialogs/admins/url/create/input_slug.py
index 0f11e41..8f63a8c 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/create/input_slug.py
+++ b/idb/bot/dialogs/admins/url/create/input_slug.py
@@ -3,10 +3,10 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateUrlSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.utils import check_slug
+from idb.bot.dialogs.admins.states import CreateUrlSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.utils.cache import AbstractBotCache
+from idb.utils.urls import check_slug
async def on_success(
@@ -17,10 +17,10 @@ async def on_success(
await message.answer(text="Некорректный слаг")
return
- storage: Storage = dialog_manager.middleware_data["storage"]
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
try:
- await storage.get_url_by_slug(slug)
+ await cache.get_url_by_slug(slug)
except KeyError:
dialog_manager.dialog_data["slug"] = slug
await dialog_manager.next()
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/create/input_value.py b/idb/bot/dialogs/admins/url/create/input_value.py
similarity index 82%
rename from inclusive_dance_bot/bot/dialogs/admins/url/create/input_value.py
rename to idb/bot/dialogs/admins/url/create/input_value.py
index dd0812e..fba09ed 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/create/input_value.py
+++ b/idb/bot/dialogs/admins/url/create/input_value.py
@@ -3,8 +3,8 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateUrlSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
+from idb.bot.dialogs.admins.states import CreateUrlSG
+from idb.bot.dialogs.utils.buttons import BACK
async def on_success(
diff --git a/inclusive_dance_bot/bot/dialogs/admins/mailings/cancel/__init__.py b/idb/bot/dialogs/admins/url/delete/__init__.py
similarity index 50%
rename from inclusive_dance_bot/bot/dialogs/admins/mailings/cancel/__init__.py
rename to idb/bot/dialogs/admins/url/delete/__init__.py
index c157dbb..5108585 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/mailings/cancel/__init__.py
+++ b/idb/bot/dialogs/admins/url/delete/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.admins.mailings.cancel import confirm
+from idb.bot.dialogs.admins.url.delete import confirm
dialog = Dialog(
confirm.window,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/delete/confirm.py b/idb/bot/dialogs/admins/url/delete/confirm.py
similarity index 64%
rename from inclusive_dance_bot/bot/dialogs/admins/url/delete/confirm.py
rename to idb/bot/dialogs/admins/url/delete/confirm.py
index 33ba0a4..6820f4b 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/delete/confirm.py
+++ b/idb/bot/dialogs/admins/url/delete/confirm.py
@@ -3,21 +3,21 @@
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import AdminMainMenuSG, DeleteUrlSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.bot.dialogs.utils.getters import get_url_data
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.url import delete_url_by_slug
+from idb.bot.dialogs.admins.states import AdminMainMenuSG, DeleteUrlSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.bot.dialogs.utils.getters import get_url_data
+from idb.db.uow import UnitOfWork
+from idb.logic.url import delete_url_by_slug
+from idb.utils.cache import AbstractBotCache
async def on_click(
c: CallbackQuery, button: Button, dialog_manager: DialogManager
) -> None:
- storage: Storage = dialog_manager.middleware_data["storage"]
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
url_slug = dialog_manager.start_data["url_slug"]
- await delete_url_by_slug(uow=uow, storage=storage, url_slug=url_slug)
+ await delete_url_by_slug(uow=uow, cache=cache, url_slug=url_slug)
dialog_manager.show_mode = ShowMode.SEND
await c.bot.send_message(c.from_user.id, text="Ссылка была удалена") # type: ignore[union-attr]
await dialog_manager.start(state=AdminMainMenuSG.menu, mode=StartMode.RESET_STACK)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/read/__init__.py b/idb/bot/dialogs/admins/url/read/__init__.py
similarity index 83%
rename from inclusive_dance_bot/bot/dialogs/admins/url/read/__init__.py
rename to idb/bot/dialogs/admins/url/read/__init__.py
index 19f95ac..d58985a 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/read/__init__.py
+++ b/idb/bot/dialogs/admins/url/read/__init__.py
@@ -2,7 +2,7 @@
from aiogram_dialog import Data, Dialog, DialogManager
-from inclusive_dance_bot.bot.dialogs.admins.url.read import item, items
+from idb.bot.dialogs.admins.url.read import item, items
async def on_process_result(
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/read/item.py b/idb/bot/dialogs/admins/url/read/item.py
similarity index 63%
rename from inclusive_dance_bot/bot/dialogs/admins/url/read/item.py
rename to idb/bot/dialogs/admins/url/read/item.py
index e978705..7d6f5db 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/read/item.py
+++ b/idb/bot/dialogs/admins/url/read/item.py
@@ -1,25 +1,26 @@
from typing import Any
from aiogram_dialog import DialogManager, Window
-from aiogram_dialog.widgets.kbd import Back, Button
+from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.admins.states import (
+from idb.bot.dialogs.admins.states import (
ChangeUrlSG,
DeleteUrlSG,
ReadUrlSG,
)
-from inclusive_dance_bot.bot.dialogs.messages import URL_TEMPLATE
-from inclusive_dance_bot.bot.dialogs.utils import start_with_data
-from inclusive_dance_bot.logic.storage import Storage
+from idb.bot.dialogs.messages import URL_TEMPLATE
+from idb.bot.dialogs.utils import start_with_data
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.utils.cache import AbstractBotCache
URL_ID = "url_slug"
async def get_data(
- storage: Storage, dialog_manager: DialogManager, **kwargs: Any
+ cache: AbstractBotCache, dialog_manager: DialogManager, **kwargs: Any
) -> dict[str, Any]:
- return {"url": await storage.get_url_by_slug(dialog_manager.dialog_data[URL_ID])}
+ return {"url": await cache.get_url_by_slug(dialog_manager.dialog_data[URL_ID])}
window = Window(
@@ -39,7 +40,7 @@ async def get_data(
id="delete_url",
on_click=start_with_data(state=DeleteUrlSG.confirm, field=URL_ID),
),
- Back(Const("Назад")),
+ BACK,
state=ReadUrlSG.item,
getter=get_data,
)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/read/items.py b/idb/bot/dialogs/admins/url/read/items.py
similarity index 72%
rename from inclusive_dance_bot/bot/dialogs/admins/url/read/items.py
rename to idb/bot/dialogs/admins/url/read/items.py
index 3a15c2b..6f562fd 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/read/items.py
+++ b/idb/bot/dialogs/admins/url/read/items.py
@@ -5,21 +5,21 @@
from aiogram_dialog.widgets.kbd import Button, ScrollingGroup, Select, Start
from aiogram_dialog.widgets.text import Const, Format, List
-from inclusive_dance_bot.bot.dialogs.admins.states import CreateUrlSG, ReadUrlSG
-from inclusive_dance_bot.bot.dialogs.utils import sync_scroll
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.logic.storage import Storage
+from idb.bot.dialogs.admins.states import CreateUrlSG, ReadUrlSG
+from idb.bot.dialogs.utils import sync_scroll
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.utils.cache import AbstractBotCache
SCROLL_KBD_ID = "url_scroll_id"
SCROLL_MESSAGE_ID = "url_message_scroll_id"
-async def get_urls_list_data(storage: Storage, **kwargs: Any) -> dict[str, Any]:
- return {"urls": list((await storage.get_urls()).values())}
+async def get_urls_list_data(cache: AbstractBotCache, **kwargs: Any) -> dict[str, Any]:
+ return {"urls": list((await cache.get_urls()).values())}
async def open_url(
- c: CallbackQuery, widget: Button, dialog_manager: DialogManager, url_slug: int
+ c: CallbackQuery, widget: Button, dialog_manager: DialogManager, url_slug: str
) -> None:
dialog_manager.dialog_data["url_slug"] = url_slug
await dialog_manager.next()
@@ -28,7 +28,7 @@ async def open_url(
window = Window(
Const("Ссылки\n"),
List(
- Format("[{pos}] {item.slug}"),
+ Format("[{pos}] {item.slug}"),
items="urls",
id=SCROLL_MESSAGE_ID,
page_size=10,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/update/__init__.py b/idb/bot/dialogs/admins/url/update/__init__.py
similarity index 54%
rename from inclusive_dance_bot/bot/dialogs/admins/url/update/__init__.py
rename to idb/bot/dialogs/admins/url/update/__init__.py
index 964a164..50ae1ce 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/update/__init__.py
+++ b/idb/bot/dialogs/admins/url/update/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.admins.url.update import change_slug, change_value
+from idb.bot.dialogs.admins.url.update import change_slug, change_value
dialog = Dialog(
change_slug.window,
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/update/change_slug.py b/idb/bot/dialogs/admins/url/update/change_slug.py
similarity index 65%
rename from inclusive_dance_bot/bot/dialogs/admins/url/update/change_slug.py
rename to idb/bot/dialogs/admins/url/update/change_slug.py
index 280fb9e..381ccf0 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/update/change_slug.py
+++ b/idb/bot/dialogs/admins/url/update/change_slug.py
@@ -3,14 +3,14 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Format
-from inclusive_dance_bot.bot.dialogs.admins.states import ChangeUrlSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.bot.dialogs.utils.getters import get_url_data
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.exceptions.url import UrlSlugAlreadyExistsError
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.url import update_url_by_slug
-from inclusive_dance_bot.utils import check_slug
+from idb.bot.dialogs.admins.states import ChangeUrlSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.bot.dialogs.utils.getters import get_url_data
+from idb.db.uow import UnitOfWork
+from idb.exceptions.url import UrlSlugAlreadyExistsError
+from idb.logic.url import update_url_by_slug
+from idb.utils.cache import AbstractBotCache
+from idb.utils.urls import check_slug
TEMPLATE_MESSAGE = (
"Введите новый слаг\n(слаг может состоять только из латинских букв"
@@ -27,11 +27,9 @@ async def on_success(
return
url_slug = dialog_manager.start_data["url_slug"]
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
- storage: Storage = dialog_manager.middleware_data["storage"]
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
try:
- await update_url_by_slug(
- uow=uow, storage=storage, url_slug=url_slug, slug=value
- )
+ await update_url_by_slug(uow=uow, cache=cache, url_slug=url_slug, slug=value)
except UrlSlugAlreadyExistsError:
dialog_manager.show_mode = ShowMode.SEND
await message.answer(text="Такой слаг уже занят. Придумайте другой")
diff --git a/inclusive_dance_bot/bot/dialogs/admins/url/update/change_value.py b/idb/bot/dialogs/admins/url/update/change_value.py
similarity index 59%
rename from inclusive_dance_bot/bot/dialogs/admins/url/update/change_value.py
rename to idb/bot/dialogs/admins/url/update/change_value.py
index b8f1b43..500b7cc 100644
--- a/inclusive_dance_bot/bot/dialogs/admins/url/update/change_value.py
+++ b/idb/bot/dialogs/admins/url/update/change_value.py
@@ -3,12 +3,12 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Format
-from inclusive_dance_bot.bot.dialogs.admins.states import ChangeUrlSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.bot.dialogs.utils.getters import get_url_data
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.url import update_url_by_slug
+from idb.bot.dialogs.admins.states import ChangeUrlSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.bot.dialogs.utils.getters import get_url_data
+from idb.db.uow import UnitOfWork
+from idb.logic.url import update_url_by_slug
+from idb.utils.cache import AbstractBotCache
TEMPLATE_MESSAGE = "Введите новое значение ссылки\n\nТекущее: {url.value}"
@@ -18,8 +18,8 @@ async def on_success(
) -> None:
url_slug = dialog_manager.start_data["url_slug"]
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
- storage: Storage = dialog_manager.middleware_data["storage"]
- await update_url_by_slug(uow=uow, storage=storage, url_slug=url_slug, value=value)
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
+ await update_url_by_slug(uow=uow, cache=cache, url_slug=url_slug, value=value)
await dialog_manager.done(result={"url_slug": url_slug})
diff --git a/idb/bot/dialogs/commands.py b/idb/bot/dialogs/commands.py
new file mode 100644
index 0000000..4285b0e
--- /dev/null
+++ b/idb/bot/dialogs/commands.py
@@ -0,0 +1,11 @@
+from aiogram.types import Message
+from aiogram_dialog import DialogManager
+
+from idb.bot.utils import start_new_dialog
+
+
+async def start_command(
+ message: Message,
+ dialog_manager: DialogManager,
+) -> None:
+ await start_new_dialog(dialog_manager=dialog_manager)
diff --git a/inclusive_dance_bot/bot/dialogs/messages.py b/idb/bot/dialogs/messages.py
similarity index 89%
rename from inclusive_dance_bot/bot/dialogs/messages.py
rename to idb/bot/dialogs/messages.py
index 4637c05..1ed7644 100644
--- a/inclusive_dance_bot/bot/dialogs/messages.py
+++ b/idb/bot/dialogs/messages.py
@@ -4,9 +4,7 @@
В этом чате вы можете получать актуальную информацию о событиях международного
движения Inclusive Dance, регистрироваться на мероприятия, задавать все
интересующие вас вопросы и многое другое.
-""".replace(
- "\n", " "
-)
+""".replace("\n", " ")
INPUT_NAME_MESSAGE = """
А теперь я хочу с вами познакомиться.
@@ -60,18 +58,13 @@
Значение: {url.value}
"""
-SUBMENU_TEMPLATE = """\
-Подменю {submenu.id}
-
-Тип: {submenu.type}
-Текст кнопки: {submenu.button_text}
-Вес: {submenu.weight}
-Значение: {submenu.message!r}
-"""
-
ADD_ADMIN_MESSAGE = """\
Введите ник пользователя в
Телеграме для добавления в группу адмиинистраторов.
Пользователь обязательно должен быть зарегистрирован в боте.
"""
+
+GOT_NEW_FEEDBACK_MESSAGE = """\
+Получена обратная связь от пользователя
+"""
diff --git a/idb/bot/dialogs/router.py b/idb/bot/dialogs/router.py
new file mode 100644
index 0000000..a6afb99
--- /dev/null
+++ b/idb/bot/dialogs/router.py
@@ -0,0 +1,18 @@
+from aiogram import Router
+from aiogram.filters import Command
+
+from idb.bot.dialogs.admins.router import (
+ dialog_router as admin_dialog_router,
+)
+from idb.bot.dialogs.commands import start_command
+from idb.bot.dialogs.users.router import (
+ dialog_router as user_dialog_router,
+)
+from idb.bot.ui_commands import Commands
+
+
+def register_dialogs(root_router: Router) -> None:
+ dialog_router = Router()
+ dialog_router.include_routers(admin_dialog_router, user_dialog_router)
+ dialog_router.message(Command(Commands.START))(start_command)
+ root_router.include_router(dialog_router)
diff --git a/inclusive_dance_bot/logic/__init__.py b/idb/bot/dialogs/users/__init__.py
similarity index 100%
rename from inclusive_dance_bot/logic/__init__.py
rename to idb/bot/dialogs/users/__init__.py
diff --git a/inclusive_dance_bot/bot/dialogs/users/feedback/__init__.py b/idb/bot/dialogs/users/feedback/__init__.py
similarity index 77%
rename from inclusive_dance_bot/bot/dialogs/users/feedback/__init__.py
rename to idb/bot/dialogs/users/feedback/__init__.py
index b8b91be..013fdcf 100644
--- a/inclusive_dance_bot/bot/dialogs/users/feedback/__init__.py
+++ b/idb/bot/dialogs/users/feedback/__init__.py
@@ -2,12 +2,12 @@
from aiogram_dialog import Dialog, DialogManager
-from inclusive_dance_bot.bot.dialogs.users.feedback import (
+from idb.bot.dialogs.users.feedback import (
confirm,
input_text,
input_title,
)
-from inclusive_dance_bot.enums import FeedbackField
+from idb.generals.enums import FeedbackField
async def on_start(data: dict[str, Any], dialog_manager: DialogManager) -> None:
diff --git a/inclusive_dance_bot/bot/dialogs/users/feedback/confirm.py b/idb/bot/dialogs/users/feedback/confirm.py
similarity index 70%
rename from inclusive_dance_bot/bot/dialogs/users/feedback/confirm.py
rename to idb/bot/dialogs/users/feedback/confirm.py
index 17bdd3d..5d135dc 100644
--- a/inclusive_dance_bot/bot/dialogs/users/feedback/confirm.py
+++ b/idb/bot/dialogs/users/feedback/confirm.py
@@ -5,15 +5,16 @@
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.messages import (
+from idb.bot.dialogs.messages import (
ANSWER_ON_FEEDBACK_MESSAGE,
FEEDBACK_CONFIRM_TEMPLATE,
+ GOT_NEW_FEEDBACK_MESSAGE,
)
-from inclusive_dance_bot.bot.dialogs.users.states import FeedbackSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.enums import FeedbackField
-from inclusive_dance_bot.logic.feedback import create_feedback
+from idb.bot.dialogs.users.states import FeedbackSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.db.uow import UnitOfWork
+from idb.generals.enums import FeedbackField
+from idb.logic.feedback import create_feedback
async def on_click(
@@ -28,10 +29,16 @@ async def on_click(
text=dialog_manager.dialog_data[FeedbackField.TEXT],
)
dialog_manager.show_mode = ShowMode.SEND
- await c.bot.send_message( # type:ignore[union-attr]
+ await c.bot.send_message( # type: ignore[union-attr]
chat_id=c.from_user.id,
text=ANSWER_ON_FEEDBACK_MESSAGE,
)
+ admins = await uow.users.get_admin_list(include_superusers=True)
+ for admin in admins:
+ await c.bot.send_message( # type: ignore[union-attr]
+ chat_id=admin.id,
+ text=GOT_NEW_FEEDBACK_MESSAGE,
+ )
await dialog_manager.done()
diff --git a/inclusive_dance_bot/bot/dialogs/users/feedback/input_text.py b/idb/bot/dialogs/users/feedback/input_text.py
similarity index 68%
rename from inclusive_dance_bot/bot/dialogs/users/feedback/input_text.py
rename to idb/bot/dialogs/users/feedback/input_text.py
index ed167db..3590ea4 100644
--- a/inclusive_dance_bot/bot/dialogs/users/feedback/input_text.py
+++ b/idb/bot/dialogs/users/feedback/input_text.py
@@ -3,9 +3,9 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.users.states import FeedbackSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.enums import FeedbackField
+from idb.bot.dialogs.users.states import FeedbackSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.generals.enums import FeedbackField
async def on_success(
@@ -17,7 +17,8 @@ async def on_success(
window = Window(
Const(
- "Опишите Вашу проблему или предложение. Администраторы обязательно его рассмотрят"
+ "Опишите Вашу проблему или предложение. "
+ "Администраторы обязательно его рассмотрят"
),
TextInput("input_message_id", on_success=on_success), # type: ignore[arg-type]
BACK,
diff --git a/inclusive_dance_bot/bot/dialogs/users/feedback/input_title.py b/idb/bot/dialogs/users/feedback/input_title.py
similarity index 76%
rename from inclusive_dance_bot/bot/dialogs/users/feedback/input_title.py
rename to idb/bot/dialogs/users/feedback/input_title.py
index 5ee7d02..52f876d 100644
--- a/inclusive_dance_bot/bot/dialogs/users/feedback/input_title.py
+++ b/idb/bot/dialogs/users/feedback/input_title.py
@@ -3,9 +3,9 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.users.states import FeedbackSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.enums import FeedbackField
+from idb.bot.dialogs.users.states import FeedbackSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.generals.enums import FeedbackField
async def on_success(
diff --git a/idb/bot/dialogs/users/main_menu/__init__.py b/idb/bot/dialogs/users/main_menu/__init__.py
new file mode 100644
index 0000000..de7dfab
--- /dev/null
+++ b/idb/bot/dialogs/users/main_menu/__init__.py
@@ -0,0 +1,10 @@
+from aiogram_dialog import Dialog
+
+from idb.bot.dialogs.users.main_menu import menu
+from idb.bot.dialogs.users.states import MainMenuSG
+from idb.bot.dialogs.utils.submenu_window import SubmenuWindow
+
+dialog = Dialog(
+ menu.window,
+ SubmenuWindow(state=MainMenuSG.message),
+)
diff --git a/inclusive_dance_bot/bot/dialogs/users/main_menu/menu.py b/idb/bot/dialogs/users/main_menu/menu.py
similarity index 87%
rename from inclusive_dance_bot/bot/dialogs/users/main_menu/menu.py
rename to idb/bot/dialogs/users/main_menu/menu.py
index 54bcaf5..921fdd4 100644
--- a/inclusive_dance_bot/bot/dialogs/users/main_menu/menu.py
+++ b/idb/bot/dialogs/users/main_menu/menu.py
@@ -5,17 +5,17 @@
from aiogram_dialog.widgets.kbd import Button, Column, Select, Start
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.users.states import (
+from idb.bot.dialogs.users.states import (
FeedbackSG,
MainMenuSG,
SubmenuSG,
)
-from inclusive_dance_bot.enums import FeedbackType, SubmenuType
-from inclusive_dance_bot.logic.storage import Storage
+from idb.generals.enums import FeedbackType, SubmenuType
+from idb.utils.cache import AbstractBotCache
-async def get_submenus_data(storage: Storage, **kwargs: Any) -> dict[str, Any]:
- submenus = await storage.get_submenus()
+async def get_submenus_data(cache: AbstractBotCache, **kwargs: Any) -> dict[str, Any]:
+ submenus = await cache.get_submenus()
return {
"submenus": list(
filter(lambda x: x.type == SubmenuType.OTHER, submenus.values())
@@ -29,8 +29,8 @@ async def open_message(
dialog_manager: DialogManager,
submenu_id: int,
) -> None:
- storage: Storage = dialog_manager.middleware_data["storage"]
- submenus = await storage.get_submenus()
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
+ submenus = await cache.get_submenus()
submenu = submenus[submenu_id]
scrolling_text = dialog_manager.find("scroll_text")
scrolling_text.widget.text = Format(submenu.message) # type: ignore[union-attr]
diff --git a/inclusive_dance_bot/bot/dialogs/users/registration/__init__.py b/idb/bot/dialogs/users/registration/__init__.py
similarity index 80%
rename from inclusive_dance_bot/bot/dialogs/users/registration/__init__.py
rename to idb/bot/dialogs/users/registration/__init__.py
index 19908ce..9baba73 100644
--- a/inclusive_dance_bot/bot/dialogs/users/registration/__init__.py
+++ b/idb/bot/dialogs/users/registration/__init__.py
@@ -1,6 +1,6 @@
from aiogram_dialog import Dialog
-from inclusive_dance_bot.bot.dialogs.users.registration import (
+from idb.bot.dialogs.users.registration import (
choose_user_types,
confirm,
input_name,
diff --git a/inclusive_dance_bot/bot/dialogs/users/registration/choose_user_types.py b/idb/bot/dialogs/users/registration/choose_user_types.py
similarity index 74%
rename from inclusive_dance_bot/bot/dialogs/users/registration/choose_user_types.py
rename to idb/bot/dialogs/users/registration/choose_user_types.py
index 6ee52a5..ec92ea0 100644
--- a/inclusive_dance_bot/bot/dialogs/users/registration/choose_user_types.py
+++ b/idb/bot/dialogs/users/registration/choose_user_types.py
@@ -5,17 +5,17 @@
from aiogram_dialog.widgets.kbd import Button, Multiselect, Next, ScrollingGroup
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.messages import CHOOSE_USER_TYPE_MESSAGE
-from inclusive_dance_bot.bot.dialogs.users.states import RegistrationSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.enums import RegistrationField
-from inclusive_dance_bot.logic.storage import Storage
+from idb.bot.dialogs.messages import CHOOSE_USER_TYPE_MESSAGE
+from idb.bot.dialogs.users.states import RegistrationSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.generals.enums import RegistrationField
+from idb.utils.cache import AbstractBotCache
async def get_user_types_data(
- dialog_manager: DialogManager, storage: Storage, **kwargs: Any
+ dialog_manager: DialogManager, cache: AbstractBotCache, **kwargs: Any
) -> dict[str, Any]:
- user_types = await storage.get_user_types()
+ user_types = await cache.get_user_types()
return {
"user_types": user_types.values(),
RegistrationField.NAME: dialog_manager.dialog_data[RegistrationField.NAME],
diff --git a/inclusive_dance_bot/bot/dialogs/users/registration/confirm.py b/idb/bot/dialogs/users/registration/confirm.py
similarity index 78%
rename from inclusive_dance_bot/bot/dialogs/users/registration/confirm.py
rename to idb/bot/dialogs/users/registration/confirm.py
index 492e842..b254f3a 100644
--- a/inclusive_dance_bot/bot/dialogs/users/registration/confirm.py
+++ b/idb/bot/dialogs/users/registration/confirm.py
@@ -5,16 +5,16 @@
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const, Format
-from inclusive_dance_bot.bot.dialogs.messages import (
+from idb.bot.dialogs.messages import (
CONFIRM_REGISTRATION_MESSAGE_TEMPLATE,
THANK_FOR_REGISTRATION_MESSAGE,
)
-from inclusive_dance_bot.bot.dialogs.users.states import MainMenuSG, RegistrationSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.enums import RegistrationField
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.user import create_user
+from idb.bot.dialogs.users.states import MainMenuSG, RegistrationSG
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.db.uow import UnitOfWork
+from idb.generals.enums import RegistrationField
+from idb.logic.users import save_profile_user
+from idb.utils.cache import AbstractBotCache
async def on_click(
@@ -30,10 +30,9 @@ async def on_click(
)
return
uow: UnitOfWork = dialog_manager.middleware_data["uow"]
- await create_user(
+ await save_profile_user(
uow=uow,
user_id=c.from_user.id,
- username=username,
name=dialog_manager.dialog_data[RegistrationField.NAME],
region=dialog_manager.dialog_data[RegistrationField.REGION],
phone_number=dialog_manager.dialog_data[RegistrationField.PHONE],
@@ -48,11 +47,11 @@ async def on_click(
async def get_user_data(
- dialog_manager: DialogManager, storage: Storage, **kwargs: Any
+ dialog_manager: DialogManager, cache: AbstractBotCache, **kwargs: Any
) -> dict[str, Any]:
user_types = filter(
lambda ut: ut.id in dialog_manager.dialog_data[RegistrationField.USER_TYPE_IDS],
- (await storage.get_user_types()).values(),
+ (await cache.get_user_types()).values(),
)
return {
"name": dialog_manager.dialog_data[RegistrationField.NAME],
diff --git a/inclusive_dance_bot/bot/dialogs/users/registration/input_name.py b/idb/bot/dialogs/users/registration/input_name.py
similarity index 66%
rename from inclusive_dance_bot/bot/dialogs/users/registration/input_name.py
rename to idb/bot/dialogs/users/registration/input_name.py
index d3120dd..bfc6de6 100644
--- a/inclusive_dance_bot/bot/dialogs/users/registration/input_name.py
+++ b/idb/bot/dialogs/users/registration/input_name.py
@@ -1,12 +1,12 @@
from aiogram.types import Message
from aiogram_dialog import DialogManager, Window
from aiogram_dialog.widgets.input import TextInput
-from aiogram_dialog.widgets.kbd import Cancel
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.messages import INPUT_NAME_MESSAGE
-from inclusive_dance_bot.bot.dialogs.users.states import RegistrationSG
-from inclusive_dance_bot.enums import RegistrationField
+from idb.bot.dialogs.messages import INPUT_NAME_MESSAGE
+from idb.bot.dialogs.users.states import RegistrationSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.generals.enums import RegistrationField
async def on_success(
@@ -19,6 +19,6 @@ async def on_success(
window = Window(
Const(INPUT_NAME_MESSAGE),
TextInput("input_name_id", on_success=on_success), # type: ignore[arg-type]
- Cancel(text=Const("Отмена")),
+ CANCEL,
state=RegistrationSG.input_name,
)
diff --git a/inclusive_dance_bot/bot/dialogs/users/registration/input_phone.py b/idb/bot/dialogs/users/registration/input_phone.py
similarity index 72%
rename from inclusive_dance_bot/bot/dialogs/users/registration/input_phone.py
rename to idb/bot/dialogs/users/registration/input_phone.py
index b60e77e..e9c9e60 100644
--- a/inclusive_dance_bot/bot/dialogs/users/registration/input_phone.py
+++ b/idb/bot/dialogs/users/registration/input_phone.py
@@ -3,9 +3,9 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.messages import INPUT_PHONE_MESSAGE
-from inclusive_dance_bot.bot.dialogs.users.states import RegistrationSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
+from idb.bot.dialogs.messages import INPUT_PHONE_MESSAGE
+from idb.bot.dialogs.users.states import RegistrationSG
+from idb.bot.dialogs.utils.buttons import BACK
async def on_success(
diff --git a/inclusive_dance_bot/bot/dialogs/users/registration/input_region.py b/idb/bot/dialogs/users/registration/input_region.py
similarity index 73%
rename from inclusive_dance_bot/bot/dialogs/users/registration/input_region.py
rename to idb/bot/dialogs/users/registration/input_region.py
index 3a0228e..798aea1 100644
--- a/inclusive_dance_bot/bot/dialogs/users/registration/input_region.py
+++ b/idb/bot/dialogs/users/registration/input_region.py
@@ -3,9 +3,9 @@
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.text import Const
-from inclusive_dance_bot.bot.dialogs.messages import INPUT_REGION_MESSAGE
-from inclusive_dance_bot.bot.dialogs.users.states import RegistrationSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
+from idb.bot.dialogs.messages import INPUT_REGION_MESSAGE
+from idb.bot.dialogs.users.states import RegistrationSG
+from idb.bot.dialogs.utils.buttons import BACK
async def on_success(
diff --git a/inclusive_dance_bot/bot/dialogs/users/__init__.py b/idb/bot/dialogs/users/router.py
similarity index 83%
rename from inclusive_dance_bot/bot/dialogs/users/__init__.py
rename to idb/bot/dialogs/users/router.py
index 5438687..f90a22e 100644
--- a/inclusive_dance_bot/bot/dialogs/users/__init__.py
+++ b/idb/bot/dialogs/users/router.py
@@ -1,6 +1,6 @@
from aiogram import Router
-from inclusive_dance_bot.bot.dialogs.users import (
+from idb.bot.dialogs.users import (
feedback,
main_menu,
registration,
diff --git a/inclusive_dance_bot/bot/dialogs/users/states.py b/idb/bot/dialogs/users/states.py
similarity index 100%
rename from inclusive_dance_bot/bot/dialogs/users/states.py
rename to idb/bot/dialogs/users/states.py
diff --git a/inclusive_dance_bot/bot/dialogs/users/submenu/__init__.py b/idb/bot/dialogs/users/submenu/__init__.py
similarity index 63%
rename from inclusive_dance_bot/bot/dialogs/users/submenu/__init__.py
rename to idb/bot/dialogs/users/submenu/__init__.py
index 022d811..0a975d2 100644
--- a/inclusive_dance_bot/bot/dialogs/users/submenu/__init__.py
+++ b/idb/bot/dialogs/users/submenu/__init__.py
@@ -2,9 +2,9 @@
from aiogram_dialog import Dialog, DialogManager
-from inclusive_dance_bot.bot.dialogs.users.states import SubmenuSG
-from inclusive_dance_bot.bot.dialogs.users.submenu import submenu_list
-from inclusive_dance_bot.bot.dialogs.utils.submenu_window import SubmenuWindow
+from idb.bot.dialogs.users.states import SubmenuSG
+from idb.bot.dialogs.users.submenu import submenu_list
+from idb.bot.dialogs.utils.submenu_window import SubmenuWindow
async def on_start(data: dict[str, Any], dialog_manager: DialogManager) -> None:
@@ -17,3 +17,4 @@ async def on_start(data: dict[str, Any], dialog_manager: DialogManager) -> None:
SubmenuWindow(SubmenuSG.submenu),
on_start=on_start,
)
+# 292990139,
diff --git a/inclusive_dance_bot/bot/dialogs/users/submenu/submenu_list.py b/idb/bot/dialogs/users/submenu/submenu_list.py
similarity index 67%
rename from inclusive_dance_bot/bot/dialogs/users/submenu/submenu_list.py
rename to idb/bot/dialogs/users/submenu/submenu_list.py
index 26684d6..ac4c846 100644
--- a/inclusive_dance_bot/bot/dialogs/users/submenu/submenu_list.py
+++ b/idb/bot/dialogs/users/submenu/submenu_list.py
@@ -3,18 +3,18 @@
from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Column, Select
-from aiogram_dialog.widgets.text import Format
+from aiogram_dialog.widgets.text import Format, Jinja
-from inclusive_dance_bot.bot.dialogs.users.states import SubmenuSG
-from inclusive_dance_bot.bot.dialogs.utils.buttons import CANCEL
-from inclusive_dance_bot.logic.storage import Storage
+from idb.bot.dialogs.users.states import SubmenuSG
+from idb.bot.dialogs.utils.buttons import CANCEL
+from idb.utils.cache import AbstractBotCache
async def get_submenu_data(
- dialog_manager: DialogManager, storage: Storage, **kwargs: Any
+ dialog_manager: DialogManager, cache: AbstractBotCache, **kwargs: Any
) -> dict[str, Any]:
submenu_type = dialog_manager.dialog_data["type"]
- submenus = await storage.get_submenus()
+ submenus = await cache.get_submenus()
return {
"submenus": list(filter(lambda x: x.type == submenu_type, submenus.values())),
"message": dialog_manager.dialog_data["message"],
@@ -27,11 +27,11 @@ async def open_message(
dialog_manager: DialogManager,
submenu_id: int,
) -> None:
- storage: Storage = dialog_manager.middleware_data["storage"]
- submenus = await storage.get_submenus()
+ cache: AbstractBotCache = dialog_manager.middleware_data["cache"]
+ submenus = await cache.get_submenus()
submenu = submenus[submenu_id]
scrolling_text = dialog_manager.find("scroll_text")
- scrolling_text.widget.text = Format(submenu.message) # type: ignore[union-attr]
+ scrolling_text.widget.text = Jinja(submenu.message) # type: ignore[union-attr]
await dialog_manager.next()
diff --git a/inclusive_dance_bot/services/__init__.py b/idb/bot/dialogs/users/windows/__init__.py
similarity index 100%
rename from inclusive_dance_bot/services/__init__.py
rename to idb/bot/dialogs/users/windows/__init__.py
diff --git a/idb/bot/dialogs/utils/__init__.py b/idb/bot/dialogs/utils/__init__.py
new file mode 100644
index 0000000..0b112d8
--- /dev/null
+++ b/idb/bot/dialogs/utils/__init__.py
@@ -0,0 +1,4 @@
+__all__ = ["sync_scroll", "start_with_data"]
+
+from idb.bot.dialogs.utils.start_with_data import start_with_data
+from idb.bot.dialogs.utils.sync_scroll import sync_scroll
diff --git a/inclusive_dance_bot/bot/dialogs/utils/buttons.py b/idb/bot/dialogs/utils/buttons.py
similarity index 51%
rename from inclusive_dance_bot/bot/dialogs/utils/buttons.py
rename to idb/bot/dialogs/utils/buttons.py
index 575a322..1c33697 100644
--- a/inclusive_dance_bot/bot/dialogs/utils/buttons.py
+++ b/idb/bot/dialogs/utils/buttons.py
@@ -1,5 +1,5 @@
from aiogram_dialog.widgets.kbd import Back, Cancel
from aiogram_dialog.widgets.text import Const
-BACK = Back(text=Const("Назад"))
-CANCEL = Cancel(text=Const("Назад"))
+BACK = Back(text=Const("⬅️ Назад"))
+CANCEL = Cancel(text=Const("⬅️ Назад"))
diff --git a/idb/bot/dialogs/utils/getters.py b/idb/bot/dialogs/utils/getters.py
new file mode 100644
index 0000000..a404c43
--- /dev/null
+++ b/idb/bot/dialogs/utils/getters.py
@@ -0,0 +1,21 @@
+from typing import Any
+
+from aiogram_dialog import DialogManager
+
+from idb.utils.cache import AbstractBotCache
+
+
+async def get_url_data(
+ cache: AbstractBotCache, dialog_manager: DialogManager, **kwargs: Any
+) -> dict[str, Any]:
+ return {"url": await cache.get_url_by_slug(dialog_manager.start_data["url_slug"])}
+
+
+async def get_submenu_data(
+ cache: AbstractBotCache, dialog_manager: DialogManager, **kwargs: Any
+) -> dict[str, Any]:
+ return {
+ "submenu": await cache.get_submenu_by_id(
+ dialog_manager.start_data["submenu_id"]
+ )
+ }
diff --git a/inclusive_dance_bot/bot/dialogs/utils/input_form_field.py b/idb/bot/dialogs/utils/input_form_field.py
similarity index 90%
rename from inclusive_dance_bot/bot/dialogs/utils/input_form_field.py
rename to idb/bot/dialogs/utils/input_form_field.py
index 0b6ea90..492d9e8 100644
--- a/inclusive_dance_bot/bot/dialogs/utils/input_form_field.py
+++ b/idb/bot/dialogs/utils/input_form_field.py
@@ -30,8 +30,8 @@ def __init__(
on_success=self.on_success, # type: ignore[arg-type]
type_factory=type_factory,
),
- Cancel(text=Const("Отмена"), when=F["is_first"]),
- Back(text=Const("Назад"), when=~F["is_first"]),
+ Cancel(text=Const("⬅️ Отмена"), when=F["is_first"]),
+ Back(text=Const("⬅️ Назад"), when=~F["is_first"]),
state=state,
getter=self.get_data,
)
diff --git a/inclusive_dance_bot/bot/dialogs/utils/start_with_data.py b/idb/bot/dialogs/utils/start_with_data.py
similarity index 90%
rename from inclusive_dance_bot/bot/dialogs/utils/start_with_data.py
rename to idb/bot/dialogs/utils/start_with_data.py
index eb272aa..2393d93 100644
--- a/inclusive_dance_bot/bot/dialogs/utils/start_with_data.py
+++ b/idb/bot/dialogs/utils/start_with_data.py
@@ -14,6 +14,6 @@ async def on_click(
callback: CallbackQuery, button: Button, manager: DialogManager
) -> None:
data = {field: manager.dialog_data.get(field)}
- await manager.start(state, data, mode=StartMode.NORMAL)
+ await manager.start(state, data, mode=mode)
return on_click
diff --git a/inclusive_dance_bot/bot/dialogs/utils/submenu_window.py b/idb/bot/dialogs/utils/submenu_window.py
similarity index 64%
rename from inclusive_dance_bot/bot/dialogs/utils/submenu_window.py
rename to idb/bot/dialogs/utils/submenu_window.py
index 498c919..7d6e7af 100644
--- a/inclusive_dance_bot/bot/dialogs/utils/submenu_window.py
+++ b/idb/bot/dialogs/utils/submenu_window.py
@@ -1,3 +1,4 @@
+from collections.abc import Mapping
from typing import Any
from aiogram.fsm.state import State
@@ -5,8 +6,9 @@
from aiogram_dialog.widgets.kbd import NumberedPager
from aiogram_dialog.widgets.text import Format, ScrollingText
-from inclusive_dance_bot.bot.dialogs.utils.buttons import BACK
-from inclusive_dance_bot.logic.storage import Storage
+from idb.bot.dialogs.utils.buttons import BACK
+from idb.generals.models.url import Url
+from idb.utils.cache import AbstractBotCache
class SubmenuWindow(Window):
@@ -16,11 +18,13 @@ def __init__(self, state: State) -> None:
NumberedPager(scroll="scroll_text", when=when_), # type: ignore[arg-type]
BACK,
state=state,
- getter=self.get_urls,
+ getter=self.get_urls, # type: ignore[arg-type]
)
- async def get_urls(self, storage: Storage, **kwargs: Any) -> dict[str, Any]:
- return await storage.get_urls()
+ async def get_urls(
+ self, cache: AbstractBotCache, **kwargs: Any
+ ) -> Mapping[str, Url]:
+ return await cache.get_urls()
def when_(data: dict, widget: NumberedPager, dialog_manager: DialogManager) -> bool:
diff --git a/inclusive_dance_bot/bot/dialogs/utils/sync_scroll.py b/idb/bot/dialogs/utils/sync_scroll.py
similarity index 70%
rename from inclusive_dance_bot/bot/dialogs/utils/sync_scroll.py
rename to idb/bot/dialogs/utils/sync_scroll.py
index fdd1c50..068c84e 100644
--- a/inclusive_dance_bot/bot/dialogs/utils/sync_scroll.py
+++ b/idb/bot/dialogs/utils/sync_scroll.py
@@ -6,16 +6,17 @@
def sync_scroll(
scroll_id: str,
-) -> Callable[[ChatEvent, ManagedScroll, DialogManager], Awaitable[None],]:
+) -> Callable[
+ [ChatEvent, ManagedScroll, DialogManager],
+ Awaitable[None],
+]:
async def on_page_changed(
event: ChatEvent,
widget: ManagedScroll,
dialog_manager: DialogManager,
) -> None:
page = await widget.get_page()
- other_scroll: ManagedScroll = dialog_manager.find(
- scroll_id
- ) # type: ignore[assignment]
+ other_scroll: ManagedScroll = dialog_manager.find(scroll_id) # type: ignore[assignment]
await other_scroll.set_page(page=page)
return on_page_changed
diff --git a/inclusive_dance_bot/bot/dialogs/utils/validators.py b/idb/bot/dialogs/utils/validators.py
similarity index 100%
rename from inclusive_dance_bot/bot/dialogs/utils/validators.py
rename to idb/bot/dialogs/utils/validators.py
diff --git a/inclusive_dance_bot/bot/factory.py b/idb/bot/factory.py
similarity index 74%
rename from inclusive_dance_bot/bot/factory.py
rename to idb/bot/factory.py
index c3bd55b..fbbd9aa 100644
--- a/inclusive_dance_bot/bot/factory.py
+++ b/idb/bot/factory.py
@@ -1,3 +1,4 @@
+import ujson
from aiogram import Bot
from aiogram.enums import ParseMode
from aiogram.fsm.storage.base import BaseStorage
@@ -5,10 +6,10 @@
from aiogram.fsm.storage.redis import DefaultKeyBuilder, RedisStorage
-def get_bot(telegram_bot_token: str) -> Bot:
+def get_bot(telegram_bot_token: str, parse_mode: ParseMode) -> Bot:
return Bot(
token=telegram_bot_token,
- parse_mode=ParseMode.HTML,
+ parse_mode=parse_mode,
)
@@ -18,4 +19,6 @@ def get_storage(debug: bool, redis_dsn: str) -> BaseStorage:
return RedisStorage.from_url(
url=redis_dsn,
key_builder=DefaultKeyBuilder(with_destiny=True),
+ json_loads=ujson.loads,
+ json_dumps=ujson.dumps,
)
diff --git a/idb/bot/handlers.py b/idb/bot/handlers.py
new file mode 100644
index 0000000..a795352
--- /dev/null
+++ b/idb/bot/handlers.py
@@ -0,0 +1,31 @@
+import logging
+
+from aiogram.exceptions import TelegramBadRequest
+from aiogram.types import ErrorEvent
+from aiogram_dialog import DialogManager
+
+from idb.bot.utils import start_new_dialog
+
+log = logging.getLogger(__name__)
+
+
+async def on_unknown_intent(event: ErrorEvent, dialog_manager: DialogManager) -> None:
+ """Example of handling UnknownIntent Error and starting new dialog."""
+ log.error("Restarting dialog: %s", event.exception)
+ if event.update.callback_query:
+ await event.update.callback_query.answer(
+ "Бот был перезапущен для тех. обслуживания.\n"
+ "Вы будете перенаправлены в главное меню.",
+ )
+ if event.update.callback_query.message:
+ try:
+ await event.update.callback_query.message.delete()
+ except TelegramBadRequest:
+ pass # whatever
+ await start_new_dialog(dialog_manager=dialog_manager)
+
+
+async def on_unknown_state(event: ErrorEvent, dialog_manager: DialogManager) -> None:
+ """Example of handling UnknownState Error and starting new dialog."""
+ log.error("Restarting dialog: %s", event.exception)
+ await start_new_dialog(dialog_manager=dialog_manager)
diff --git a/tests/test_unit/test_services/__init__.py b/idb/bot/middlewares/__init__.py
similarity index 100%
rename from tests/test_unit/test_services/__init__.py
rename to idb/bot/middlewares/__init__.py
diff --git a/inclusive_dance_bot/bot/middlewares/storage.py b/idb/bot/middlewares/cache.py
similarity index 67%
rename from inclusive_dance_bot/bot/middlewares/storage.py
rename to idb/bot/middlewares/cache.py
index b802215..d382c40 100644
--- a/inclusive_dance_bot/bot/middlewares/storage.py
+++ b/idb/bot/middlewares/cache.py
@@ -4,13 +4,13 @@
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Update
-from inclusive_dance_bot.logic.storage import Storage
+from idb.utils.cache import AbstractBotCache
-class StorageMiddleware(BaseMiddleware):
- def __init__(self, storage: Storage) -> None:
+class CacheMiddleware(BaseMiddleware):
+ def __init__(self, cache: AbstractBotCache) -> None:
super().__init__()
- self.storage = storage
+ self._cache = cache
async def __call__(
self,
@@ -18,5 +18,5 @@ async def __call__(
event: Update, # type: ignore[override]
data: dict[str, Any],
) -> Any:
- data["storage"] = self.storage
+ data["cache"] = self._cache
return await handler(event, data)
diff --git a/inclusive_dance_bot/bot/middlewares/uow.py b/idb/bot/middlewares/uow.py
similarity index 63%
rename from inclusive_dance_bot/bot/middlewares/uow.py
rename to idb/bot/middlewares/uow.py
index 54bbdf2..78daeac 100644
--- a/inclusive_dance_bot/bot/middlewares/uow.py
+++ b/idb/bot/middlewares/uow.py
@@ -3,14 +3,14 @@
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Update
+from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
-from inclusive_dance_bot.db.uow.main import UnitOfWork
+from idb.db.uow import uow_context
class UowMiddleware(BaseMiddleware):
- def __init__(self, uow: UnitOfWork) -> None:
- super().__init__()
- self.uow = uow
+ def __init__(self, sessionmaker: async_sessionmaker[AsyncSession]) -> None:
+ self.sessionmaker = sessionmaker
async def __call__(
self,
@@ -18,6 +18,6 @@ async def __call__(
event: Update, # type: ignore[override]
data: dict[str, Any],
) -> Any:
- async with self.uow as uow:
+ async with uow_context(self.sessionmaker) as uow:
data["uow"] = uow
return await handler(event, data)
diff --git a/idb/bot/middlewares/user.py b/idb/bot/middlewares/user.py
new file mode 100644
index 0000000..7c10351
--- /dev/null
+++ b/idb/bot/middlewares/user.py
@@ -0,0 +1,49 @@
+from collections.abc import Awaitable, Callable
+from typing import Any
+
+from aiogram import BaseMiddleware
+from aiogram.types import TelegramObject, Update
+from aiogram.types import User as TelegramUser
+
+from idb.db.uow import UnitOfWork
+from idb.generals.models.user import BotUser, User
+
+
+class UserMiddleware(BaseMiddleware):
+ _telegram_bot_admin_ids: list[int]
+
+ def __init__(self, telegram_bot_admin_ids: list[int]) -> None:
+ self._telegram_bot_admin_ids = telegram_bot_admin_ids
+
+ async def __call__(
+ self,
+ handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
+ event: Update, # type: ignore[override]
+ data: dict[str, Any],
+ ) -> Any:
+ telegram_user: TelegramUser = data["event_from_user"]
+ uow: UnitOfWork = data["uow"]
+ user = await self._get_or_create_user(uow=uow, telegram_user=telegram_user)
+ data["user"] = BotUser(
+ telegram_user=telegram_user,
+ user=user,
+ )
+ return await handler(event, data)
+
+ async def _get_or_create_user(
+ self,
+ uow: UnitOfWork,
+ telegram_user: TelegramUser,
+ ) -> User:
+ user = await uow.users.get_by_id_or_none(telegram_user.id)
+ if user is None:
+ is_superuser = telegram_user.id in self._telegram_bot_admin_ids
+ user = await uow.users.create(
+ id=telegram_user.id,
+ username=telegram_user.username,
+ is_admin=is_superuser,
+ is_superuser=is_superuser,
+ profile=dict(),
+ )
+ await uow.commit()
+ return user
diff --git a/tests/test_unit/test_services/test_feedback/__init__.py b/idb/bot/services/__init__.py
similarity index 100%
rename from tests/test_unit/test_services/test_feedback/__init__.py
rename to idb/bot/services/__init__.py
diff --git a/idb/bot/services/bot.py b/idb/bot/services/bot.py
new file mode 100644
index 0000000..2fb7d3c
--- /dev/null
+++ b/idb/bot/services/bot.py
@@ -0,0 +1,73 @@
+import logging
+
+from aiogram import Bot, Dispatcher
+from aiogram.filters import ExceptionTypeFilter
+from aiogram.fsm.storage.memory import SimpleEventIsolation
+from aiogram_dialog import setup_dialogs
+from aiogram_dialog.api.exceptions import UnknownIntent, UnknownState
+from aiogram_dialog.widgets.text.jinja import setup_jinja
+from aiomisc import Service
+from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
+
+from idb.bot.dialogs.router import register_dialogs
+from idb.bot.factory import get_storage
+from idb.bot.handlers import on_unknown_intent, on_unknown_state
+from idb.bot.middlewares.cache import CacheMiddleware
+from idb.bot.middlewares.uow import UowMiddleware
+from idb.bot.middlewares.user import UserMiddleware
+from idb.bot.ui_commands import set_ui_commands
+from idb.bot.utils import as_local_fmt
+from idb.utils.cache import AbstractBotCache
+
+log = logging.getLogger(__name__)
+
+
+class AiogramBotService(Service):
+ __required__ = ("debug", "redis_dsn", "telegram_bot_admin_ids")
+
+ __dependencies__ = ("sessionmaker", "bot", "cache")
+
+ debug: bool
+ redis_dsn: str
+ telegram_bot_admin_ids: list[int]
+
+ sessionmaker: async_sessionmaker[AsyncSession]
+ bot: Bot
+ cache: AbstractBotCache
+
+ async def start(self) -> None:
+ log.info("Initialize bot")
+ await set_ui_commands(self.bot)
+ await self.bot.delete_webhook(drop_pending_updates=True)
+
+ dp = Dispatcher(
+ storage=get_storage(
+ debug=self.debug,
+ redis_dsn=self.redis_dsn,
+ ),
+ events_isolation=SimpleEventIsolation(),
+ )
+ dp.update.outer_middleware(UowMiddleware(sessionmaker=self.sessionmaker))
+ dp.update.outer_middleware(CacheMiddleware(cache=self.cache))
+ dp.update.outer_middleware(
+ UserMiddleware(telegram_bot_admin_ids=self.telegram_bot_admin_ids)
+ )
+ register_dialogs(dp)
+ setup_dialogs(dp)
+ dp.errors.register(
+ on_unknown_intent,
+ ExceptionTypeFilter(UnknownIntent),
+ )
+ dp.errors.register(
+ on_unknown_state,
+ ExceptionTypeFilter(UnknownState),
+ )
+ setup_jinja(
+ dp=self.bot,
+ filters={
+ "as_local_fmt": as_local_fmt,
+ },
+ )
+ self.start_event.set()
+ log.info("Start polling")
+ await dp.start_polling(self.bot)
diff --git a/idb/bot/services/periodic.py b/idb/bot/services/periodic.py
new file mode 100644
index 0000000..59460a4
--- /dev/null
+++ b/idb/bot/services/periodic.py
@@ -0,0 +1,21 @@
+from typing import Any
+
+from aiogram import Bot
+from aiomisc.service.periodic import PeriodicService
+from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
+
+from idb.db.uow import uow_context
+from idb.logic.mailing import send_mailings
+
+
+class PeriodicMailingService(PeriodicService):
+ __required__ = ("gap",)
+ __dependencies__ = ("bot", "sessionmaker")
+
+ bot: Bot
+ sessionmaker: async_sessionmaker[AsyncSession]
+ gap: int
+
+ async def callback(self) -> Any:
+ async with uow_context(self.sessionmaker) as uow:
+ await send_mailings(uow=uow, bot=self.bot, gap=self.gap)
diff --git a/inclusive_dance_bot/bot/ui_commands.py b/idb/bot/ui_commands.py
similarity index 100%
rename from inclusive_dance_bot/bot/ui_commands.py
rename to idb/bot/ui_commands.py
diff --git a/idb/bot/utils.py b/idb/bot/utils.py
new file mode 100644
index 0000000..9e4e9ce
--- /dev/null
+++ b/idb/bot/utils.py
@@ -0,0 +1,41 @@
+from datetime import datetime
+
+import pytz
+from aiogram_dialog import DialogManager, ShowMode, StartMode
+
+from idb.bot.dialogs.admins.states import AdminMainMenuSG
+from idb.bot.dialogs.messages import START_MESSAGE
+from idb.bot.dialogs.users.states import MainMenuSG, RegistrationSG
+from idb.generals.models.user import BotUser
+
+LOCAL_TZ = pytz.timezone("Europe/Moscow")
+
+
+async def start_new_dialog(dialog_manager: DialogManager) -> None:
+ user: BotUser = dialog_manager.middleware_data["user"]
+ if user.is_admin:
+ await dialog_manager.start(
+ AdminMainMenuSG.menu, mode=StartMode.RESET_STACK, show_mode=ShowMode.SEND
+ )
+ elif user.is_anonymous:
+ chat_id = dialog_manager.event.from_user.id # type: ignore[union-attr]
+ await dialog_manager.event.bot.send_message( # type: ignore[union-attr]
+ chat_id=chat_id,
+ text=START_MESSAGE,
+ )
+ await dialog_manager.start(
+ RegistrationSG.input_name,
+ mode=StartMode.RESET_STACK,
+ )
+ else:
+ await dialog_manager.start(
+ MainMenuSG.menu, mode=StartMode.RESET_STACK, show_mode=ShowMode.SEND
+ )
+
+
+def as_local_dt(dt: datetime) -> datetime:
+ return dt.astimezone(LOCAL_TZ)
+
+
+def as_local_fmt(dt: datetime) -> str:
+ return as_local_dt(dt).strftime("%H:%M:%S %d.%m.%Y")
diff --git a/tests/test_unit/test_services/test_mailing/__init__.py b/idb/db/__init__.py
similarity index 100%
rename from tests/test_unit/test_services/test_mailing/__init__.py
rename to idb/db/__init__.py
diff --git a/inclusive_dance_bot/db/__main__.py b/idb/db/__main__.py
similarity index 72%
rename from inclusive_dance_bot/db/__main__.py
rename to idb/db/__main__.py
index 8a80e8a..ffff942 100644
--- a/inclusive_dance_bot/db/__main__.py
+++ b/idb/db/__main__.py
@@ -4,9 +4,9 @@
from alembic.config import CommandLine
-from inclusive_dance_bot.db.utils import make_alembic_config
+from idb.db.utils import make_alembic_config
-DEFAULT_PG_URL = "postgresql://user:secret@localhost/inclusive_dance_bot"
+DEFAULT_PG_DSN = "postgresql+asyncpg://pguser:pguser@localhost/pgdb"
def main() -> None:
@@ -16,8 +16,8 @@ def main() -> None:
alembic.parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter
alembic.parser.add_argument(
"--pg-url",
- default=os.getenv("PG_URL", DEFAULT_PG_URL),
- help="Database URL [env var: PG_URL]",
+ default=os.getenv("APP_PG_DSN", DEFAULT_PG_DSN),
+ help="Database URL [env var: APP_PG_DSN]",
)
options = alembic.parser.parse_args()
diff --git a/inclusive_dance_bot/db/alembic.ini b/idb/db/alembic.ini
similarity index 100%
rename from inclusive_dance_bot/db/alembic.ini
rename to idb/db/alembic.ini
diff --git a/inclusive_dance_bot/db/base.py b/idb/db/base.py
similarity index 100%
rename from inclusive_dance_bot/db/base.py
rename to idb/db/base.py
diff --git a/inclusive_dance_bot/db/migrations/env.py b/idb/db/migrations/env.py
similarity index 97%
rename from inclusive_dance_bot/db/migrations/env.py
rename to idb/db/migrations/env.py
index c1ef47f..7cfd564 100644
--- a/inclusive_dance_bot/db/migrations/env.py
+++ b/idb/db/migrations/env.py
@@ -6,7 +6,7 @@
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
-from inclusive_dance_bot.db.models import Base
+from idb.db.models import Base
config = context.config
diff --git a/inclusive_dance_bot/db/migrations/script.py.mako b/idb/db/migrations/script.py.mako
similarity index 100%
rename from inclusive_dance_bot/db/migrations/script.py.mako
rename to idb/db/migrations/script.py.mako
diff --git a/inclusive_dance_bot/db/migrations/versions/2023_11_14_0228a02c3bb6_initial_commit.py b/idb/db/migrations/versions/2024_01_03_41c796b69ba4_.py
similarity index 76%
rename from inclusive_dance_bot/db/migrations/versions/2023_11_14_0228a02c3bb6_initial_commit.py
rename to idb/db/migrations/versions/2024_01_03_41c796b69ba4_.py
index 4348467..d7c0e64 100644
--- a/inclusive_dance_bot/db/migrations/versions/2023_11_14_0228a02c3bb6_initial_commit.py
+++ b/idb/db/migrations/versions/2024_01_03_41c796b69ba4_.py
@@ -1,15 +1,16 @@
-"""Initial commit
+"""empty message
-Revision ID: 0228a02c3bb6
+Revision ID: 41c796b69ba4
Revises:
-Create Date: 2023-11-14 18:36:06.158969
+Create Date: 2024-01-03 22:29:54.480730
"""
import sqlalchemy as sa
from alembic import op
+from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
-revision = "0228a02c3bb6"
+revision = "41c796b69ba4"
down_revision = None
branch_labels = None
depends_on = None
@@ -94,11 +95,10 @@ def upgrade() -> None:
op.create_table(
"users",
sa.Column("id", sa.BigInteger(), nullable=False),
- sa.Column("username", sa.String(length=256), nullable=False),
- sa.Column("name", sa.String(length=256), nullable=False),
- sa.Column("region", sa.String(length=256), nullable=False),
- sa.Column("phone_number", sa.String(length=16), nullable=False),
+ sa.Column("username", sa.String(length=256), nullable=True),
sa.Column("is_admin", sa.Boolean(), nullable=False),
+ sa.Column("is_superuser", sa.Boolean(), nullable=False),
+ sa.Column("profile", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
@@ -112,7 +112,6 @@ def upgrade() -> None:
nullable=False,
),
sa.PrimaryKeyConstraint("id", name=op.f("pk__users")),
- sa.UniqueConstraint("username", name=op.f("uq__users__username")),
)
op.create_table(
"feedback",
@@ -179,11 +178,56 @@ def upgrade() -> None:
"user_id", "user_type_id", name=op.f("pk__user_type_user")
),
)
+ op.create_table(
+ "answer",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("feedback_id", sa.Integer(), nullable=False),
+ sa.Column("from_user_id", sa.BigInteger(), nullable=False),
+ sa.Column("to_user_id", sa.BigInteger(), nullable=False),
+ sa.Column("text", sa.String(length=4096), nullable=False),
+ sa.Column(
+ "created_at",
+ sa.DateTime(timezone=True),
+ server_default=sa.text("TIMEZONE('utc', now())"),
+ nullable=False,
+ ),
+ sa.Column(
+ "updated_at",
+ sa.DateTime(timezone=True),
+ server_default=sa.text("TIMEZONE('utc', now())"),
+ nullable=False,
+ ),
+ sa.ForeignKeyConstraint(
+ ["feedback_id"],
+ ["feedback.id"],
+ name=op.f("fk__answer__feedback_id__feedback"),
+ ),
+ sa.ForeignKeyConstraint(
+ ["from_user_id"], ["users.id"], name=op.f("fk__answer__from_user_id__users")
+ ),
+ sa.ForeignKeyConstraint(
+ ["to_user_id"], ["users.id"], name=op.f("fk__answer__to_user_id__users")
+ ),
+ sa.PrimaryKeyConstraint("id", name=op.f("pk__answer")),
+ )
+ op.create_index(
+ op.f("ix__answer__feedback_id"), "answer", ["feedback_id"], unique=False
+ )
+ op.create_index(
+ op.f("ix__answer__from_user_id"), "answer", ["from_user_id"], unique=False
+ )
+ op.create_index(
+ op.f("ix__answer__to_user_id"), "answer", ["to_user_id"], unique=False
+ )
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f("ix__answer__to_user_id"), table_name="answer")
+ op.drop_index(op.f("ix__answer__from_user_id"), table_name="answer")
+ op.drop_index(op.f("ix__answer__feedback_id"), table_name="answer")
+ op.drop_table("answer")
op.drop_table("user_type_user")
op.drop_table("mailing_user_type")
op.drop_index(op.f("ix__feedback__user_id"), table_name="feedback")
diff --git a/tests/test_unit/test_services/test_url/__init__.py b/idb/db/migrations/versions/__init__.py
similarity index 100%
rename from tests/test_unit/test_services/test_url/__init__.py
rename to idb/db/migrations/versions/__init__.py
diff --git a/inclusive_dance_bot/db/models.py b/idb/db/models.py
similarity index 78%
rename from inclusive_dance_bot/db/models.py
rename to idb/db/models.py
index 09d05e2..44f9de3 100644
--- a/inclusive_dance_bot/db/models.py
+++ b/idb/db/models.py
@@ -1,24 +1,25 @@
from datetime import datetime
+from typing import Any
from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, Integer, String
+from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy_utils import ChoiceType
-from inclusive_dance_bot.db.base import Base, TimestampMixin
-from inclusive_dance_bot.enums import FeedbackType, MailingStatus, SubmenuType
+from idb.db.base import Base, TimestampMixin
+from idb.generals.enums import FeedbackType, MailingStatus, SubmenuType
class User(TimestampMixin, Base):
__tablename__ = "users" # type: ignore[assignment]
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
- username: Mapped[str] = mapped_column(String(256), nullable=False, unique=True)
- name: Mapped[str] = mapped_column(String(256), nullable=False, default="")
- region: Mapped[str] = mapped_column(String(256), nullable=False, default="")
- phone_number: Mapped[str] = mapped_column(String(16), nullable=False, default="")
+ username: Mapped[str] = mapped_column(String(256), nullable=True)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
-
- user_types: Mapped[list["UserType"]] = relationship(
- "UserType", secondary="user_type_user"
+ is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
+ profile: Mapped[dict[str, Any]] = mapped_column(
+ JSONB,
+ nullable=False,
+ default={},
)
@@ -76,6 +77,29 @@ class Feedback(TimestampMixin, Base):
)
+class Answer(TimestampMixin, Base):
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ feedback_id: Mapped[int] = mapped_column(
+ Integer,
+ ForeignKey("feedback.id"),
+ nullable=False,
+ index=True,
+ )
+ from_user_id: Mapped[int] = mapped_column(
+ BigInteger,
+ ForeignKey("users.id"),
+ nullable=False,
+ index=True,
+ )
+ to_user_id: Mapped[int] = mapped_column(
+ BigInteger,
+ ForeignKey("users.id"),
+ nullable=False,
+ index=True,
+ )
+ text: Mapped[str] = mapped_column(String(4096), nullable=False)
+
+
class Mailing(TimestampMixin, Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
scheduled_at: Mapped[datetime | None] = mapped_column(
diff --git a/tests/test_unit/test_services/test_user/__init__.py b/idb/db/repositories/__init__.py
similarity index 100%
rename from tests/test_unit/test_services/test_user/__init__.py
rename to idb/db/repositories/__init__.py
diff --git a/idb/db/repositories/answer.py b/idb/db/repositories/answer.py
new file mode 100644
index 0000000..be50c71
--- /dev/null
+++ b/idb/db/repositories/answer.py
@@ -0,0 +1,59 @@
+from collections.abc import Sequence
+from typing import NoReturn
+
+from sqlalchemy import insert, select
+from sqlalchemy.exc import DBAPIError, IntegrityError
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from idb.db.models import Answer as AnswerDb
+from idb.db.repositories.base import Repository
+from idb.exceptions.base import InclusiveDanceError
+from idb.exceptions.user import InvalidUserIDError
+from idb.generals.models.answer import Answer
+
+
+class AnswerRepository(Repository[AnswerDb]):
+ def __init__(self, session: AsyncSession) -> None:
+ super().__init__(model=AnswerDb, session=session)
+
+ async def create(
+ self,
+ *,
+ feedback_id: int,
+ from_user_id: int,
+ to_user_id: int,
+ text: str,
+ ) -> Answer:
+ query = (
+ insert(AnswerDb)
+ .values(
+ feedback_id=feedback_id,
+ from_user_id=from_user_id,
+ to_user_id=to_user_id,
+ text=text,
+ )
+ .returning(AnswerDb)
+ )
+ try:
+ obj = (await self._session.scalars(query)).one()
+ except IntegrityError as e:
+ self._raise_error(e)
+ await self._session.flush()
+ return Answer.model_validate(obj)
+
+ async def history(self, feedback_id: int) -> Sequence[Answer]:
+ query = (
+ select(AnswerDb)
+ .where(AnswerDb.feedback_id == feedback_id)
+ .order_by(AnswerDb.created_at)
+ )
+ objs = (await self._session.scalars(query)).all()
+ return [Answer.model_validate(obj) for obj in objs]
+
+ def _raise_error(self, e: DBAPIError) -> NoReturn:
+ constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
+ if constraint == "fk__answer__to_user_id__users":
+ raise InvalidUserIDError from e
+ if constraint == "fk__answer__from_user_id__users":
+ raise InvalidUserIDError from e
+ raise InclusiveDanceError from e
diff --git a/inclusive_dance_bot/db/repositories/base.py b/idb/db/repositories/base.py
similarity index 91%
rename from inclusive_dance_bot/db/repositories/base.py
rename to idb/db/repositories/base.py
index 23b73e7..1b77e98 100644
--- a/inclusive_dance_bot/db/repositories/base.py
+++ b/idb/db/repositories/base.py
@@ -5,8 +5,8 @@
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
-from inclusive_dance_bot.db.base import Base
-from inclusive_dance_bot.exceptions import EntityNotFoundError
+from idb.db.base import Base
+from idb.exceptions import EntityNotFoundError
Model = TypeVar("Model", bound=Base)
diff --git a/idb/db/repositories/feedback.py b/idb/db/repositories/feedback.py
new file mode 100644
index 0000000..2936e49
--- /dev/null
+++ b/idb/db/repositories/feedback.py
@@ -0,0 +1,92 @@
+from collections.abc import Sequence
+from typing import Any, NoReturn
+
+from sqlalchemy import desc, func, insert, select
+from sqlalchemy.exc import DBAPIError, IntegrityError
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from idb.db.models import Feedback as FeedbackDb
+from idb.db.repositories.base import Repository
+from idb.exceptions import InclusiveDanceError, InvalidUserIDError
+from idb.generals.enums import FeedbackType
+from idb.generals.models.feedback import Feedback
+
+
+class FeedbackRepository(Repository[FeedbackDb]):
+ def __init__(self, session: AsyncSession) -> None:
+ super().__init__(model=FeedbackDb, session=session)
+
+ async def read_by_id(self, feedback_id: int) -> Feedback:
+ obj = await self._get_by_id(feedback_id)
+ return Feedback.model_validate(obj)
+
+ async def create(
+ self, *, user_id: int, type: FeedbackType, title: str, text: str
+ ) -> Feedback:
+ query = (
+ insert(FeedbackDb)
+ .values(
+ user_id=user_id,
+ type=type,
+ title=title,
+ text=text,
+ )
+ .returning(FeedbackDb)
+ )
+ try:
+ obj = (await self._session.scalars(query)).one()
+ except IntegrityError as e:
+ self._raise_error(e)
+ await self._session.flush()
+ return Feedback.model_validate(obj)
+
+ async def total_count(self) -> int:
+ return (
+ await self._session.execute(select(func.count("*")).select_from(FeedbackDb))
+ ).scalar_one()
+
+ async def new_count(self) -> int:
+ return (
+ await self._session.execute(
+ select(func.count("*"))
+ .select_from(FeedbackDb)
+ .where(FeedbackDb.is_viewed.is_(False))
+ )
+ ).scalar_one()
+
+ async def new_items(self) -> Sequence[Feedback]:
+ query = (
+ select(FeedbackDb)
+ .where(FeedbackDb.is_viewed.is_(False))
+ .order_by(desc(FeedbackDb.created_at))
+ )
+ objs = (await self._session.scalars(query)).all()
+ return [Feedback.model_validate(obj) for obj in objs]
+
+ async def viewed_items(self) -> Sequence[Feedback]:
+ query = (
+ select(FeedbackDb)
+ .where(FeedbackDb.is_viewed.is_(True))
+ .order_by(desc(FeedbackDb.created_at))
+ )
+ objs = (await self._session.scalars(query)).all()
+ return [Feedback.model_validate(obj) for obj in objs]
+
+ async def archive_count(self) -> int:
+ return (
+ await self._session.execute(
+ select(func.count("*"))
+ .select_from(FeedbackDb)
+ .where(FeedbackDb.is_viewed.is_(True))
+ )
+ ).scalar_one()
+
+ async def update_by_id(self, feedback_id: int, **kwargs: Any) -> Feedback:
+ obj = await self._update(FeedbackDb.id == feedback_id, **kwargs)
+ return Feedback.model_validate(obj)
+
+ def _raise_error(self, e: DBAPIError) -> NoReturn:
+ constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
+ if constraint == "fk__feedback__user_id__users":
+ raise InvalidUserIDError from e
+ raise InclusiveDanceError from e
diff --git a/inclusive_dance_bot/db/repositories/mailing.py b/idb/db/repositories/mailing.py
similarity index 58%
rename from inclusive_dance_bot/db/repositories/mailing.py
rename to idb/db/repositories/mailing.py
index 8d93b9d..66ebc45 100644
--- a/inclusive_dance_bot/db/repositories/mailing.py
+++ b/idb/db/repositories/mailing.py
@@ -1,32 +1,33 @@
from datetime import datetime, timedelta
from typing import Any, NoReturn
-from sqlalchemy import insert, select, update
+from sqlalchemy import func, insert, select, update
from sqlalchemy.exc import DBAPIError, IntegrityError, NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
-from inclusive_dance_bot.db.models import Mailing, MailingUserType
-from inclusive_dance_bot.db.repositories.base import Repository
-from inclusive_dance_bot.dto import MailingDto
-from inclusive_dance_bot.enums import MailingStatus
-from inclusive_dance_bot.exceptions import (
+from idb.db.models import Mailing as MailingDb
+from idb.db.models import MailingUserType as MailingUserTypeDb
+from idb.db.repositories.base import Repository
+from idb.exceptions import (
EntityNotFoundError,
InclusiveDanceError,
MailingNotFoundError,
)
+from idb.generals.enums import MailingStatus
+from idb.generals.models.mailing import Mailing
-class MailingRepository(Repository[Mailing]):
+class MailingRepository(Repository[MailingDb]):
def __init__(self, session: AsyncSession) -> None:
- super().__init__(model=Mailing, session=session)
+ super().__init__(model=MailingDb, session=session)
- async def get_by_id(self, mailing_id: int) -> MailingDto:
+ async def get_by_id(self, mailing_id: int) -> Mailing:
try:
obj = await self._session.get_one(
- Mailing, mailing_id, options=(selectinload(Mailing.user_types),)
+ MailingDb, mailing_id, options=(selectinload(MailingDb.user_types),)
)
- return MailingDto.from_orm(obj)
+ return Mailing.model_validate(obj)
except NoResultFound as e:
raise MailingNotFoundError from e
@@ -38,9 +39,9 @@ async def create(
scheduled_at: datetime | None,
status: MailingStatus,
sent_at: datetime | None,
- ) -> MailingDto:
+ ) -> Mailing:
stmt = (
- insert(Mailing)
+ insert(MailingDb)
.values(
title=title,
content=content,
@@ -48,24 +49,24 @@ async def create(
status=status,
sent_at=sent_at,
)
- .returning(Mailing)
- .options(selectinload(Mailing.user_types))
+ .returning(MailingDb)
+ .options(selectinload(MailingDb.user_types))
)
try:
- result = await self._session.scalars(stmt)
+ obj = (await self._session.scalars(stmt)).one()
except IntegrityError as e:
self._raise_error(e)
else:
await self._session.flush()
- return MailingDto.from_orm(result.one())
+ return Mailing.model_validate(obj)
async def create_mailing_user_type(
self, *, mailing_id: int, user_type_id: int
- ) -> MailingUserType:
+ ) -> MailingUserTypeDb:
stmt = (
- insert(MailingUserType)
+ insert(MailingUserTypeDb)
.values(mailing_id=mailing_id, user_type_id=user_type_id)
- .returning(MailingUserType)
+ .returning(MailingUserTypeDb)
)
try:
result = await self._session.scalars(stmt)
@@ -77,42 +78,42 @@ async def create_mailing_user_type(
async def get_new_mailings(
self, now: datetime | None = None, gap: int | None = None
- ) -> list[MailingDto]:
+ ) -> list[Mailing]:
return await self.get_mailings(
- now, gap, Mailing.status == MailingStatus.SCHEDULED
+ now, gap, MailingDb.status == MailingStatus.SCHEDULED
)
- async def get_archive_mailings(self) -> list[MailingDto]:
+ async def get_archive_mailings(self) -> list[Mailing]:
return await self.get_mailings(
- None, None, Mailing.status != MailingStatus.SCHEDULED
+ None, None, MailingDb.status != MailingStatus.SCHEDULED
)
async def get_mailings(
self, now: datetime | None = None, gap: int | None = None, *args: Any
- ) -> list[MailingDto]:
+ ) -> list[Mailing]:
stmt = (
- select(Mailing)
- .options(selectinload(Mailing.user_types))
- .order_by(Mailing.created_at)
+ select(MailingDb)
+ .options(selectinload(MailingDb.user_types))
+ .order_by(MailingDb.created_at)
)
for arg in args:
stmt = stmt.where(arg)
if gap is not None and now is not None:
- stmt = stmt.where(Mailing.scheduled_at < now + timedelta(seconds=gap))
+ stmt = stmt.where(MailingDb.scheduled_at < now + timedelta(seconds=gap))
result = await self._session.scalars(stmt)
- return [MailingDto.from_orm(obj) for obj in result]
+ return [Mailing.model_validate(obj) for obj in result]
- async def update_by_id(self, mailing_id: int, **kwargs: Any) -> MailingDto:
- obj = await self._update(Mailing.id == mailing_id, **kwargs)
- return MailingDto.from_orm(obj)
+ async def update_by_id(self, mailing_id: int, **kwargs: Any) -> Mailing:
+ obj = await self._update(MailingDb.id == mailing_id, **kwargs)
+ return Mailing.model_validate(obj)
- async def _update(self, *args: Any, **kwargs: Any) -> Mailing:
+ async def _update(self, *args: Any, **kwargs: Any) -> MailingDb:
query = update(self._model).where(*args).values(**kwargs).returning(self._model)
result = await self._session.scalars(
select(self._model)
.from_statement(query)
- .options(selectinload(Mailing.user_types))
+ .options(selectinload(MailingDb.user_types))
)
try:
obj = result.one()
@@ -122,5 +123,10 @@ async def _update(self, *args: Any, **kwargs: Any) -> Mailing:
await self._session.refresh(obj)
return obj
+ async def total_count(self) -> int:
+ return (
+ await self._session.execute(select(func.count("*")).select_from(MailingDb))
+ ).scalar_one()
+
def _raise_error(self, e: DBAPIError) -> NoReturn:
raise InclusiveDanceError from e
diff --git a/idb/db/repositories/submenu.py b/idb/db/repositories/submenu.py
new file mode 100644
index 0000000..51207b8
--- /dev/null
+++ b/idb/db/repositories/submenu.py
@@ -0,0 +1,121 @@
+from collections.abc import Sequence
+from typing import Any, NoReturn
+
+from sqlalchemy import ScalarResult, delete, desc, select
+from sqlalchemy.dialects.postgresql import insert
+from sqlalchemy.exc import DBAPIError, IntegrityError, NoResultFound
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from idb.db.models import Submenu as SubmenuDb
+from idb.db.repositories.base import Repository
+from idb.exceptions import (
+ InclusiveDanceError,
+ SubmenuAlreadyExistsError,
+ SubmenuNotFoundError,
+)
+from idb.exceptions.base import EntityNotFoundError
+from idb.generals.enums import SubmenuType
+from idb.generals.models.submenu import Submenu
+
+
+class SubmenuRepository(Repository[SubmenuDb]):
+ def __init__(self, session: AsyncSession) -> None:
+ super().__init__(model=SubmenuDb, session=session)
+
+ async def create(
+ self,
+ type: SubmenuType,
+ button_text: str,
+ message: str,
+ weight: int = 0,
+ id: int | None = None,
+ ) -> Submenu:
+ data = dict(type=type, button_text=button_text, message=message, weight=weight)
+ if id is not None:
+ data["id"] = id
+ stmt = insert(SubmenuDb).values(**data).returning(SubmenuDb)
+ try:
+ result: ScalarResult[SubmenuDb] = await self._session.scalars(stmt)
+ except IntegrityError as e:
+ self._raise_error(e)
+
+ await self._session.flush()
+ return Submenu.model_validate(result.one())
+
+ async def upsert(
+ self,
+ *,
+ id: int,
+ type: SubmenuType,
+ weight: int,
+ button_text: str,
+ message: str,
+ ) -> Submenu:
+ stmt = (
+ insert(SubmenuDb)
+ .values(
+ id=id,
+ type=type,
+ weight=weight,
+ button_text=button_text,
+ message=message,
+ )
+ .on_conflict_do_update(
+ index_elements=[SubmenuDb.id],
+ set_={
+ "id": id,
+ "type": type,
+ "weight": weight,
+ "button_text": button_text,
+ "message": message,
+ },
+ )
+ .returning(SubmenuDb)
+ )
+ try:
+ result: ScalarResult[SubmenuDb] = await self._session.scalars(stmt)
+ except IntegrityError as e:
+ self._raise_error(e)
+ await self._session.flush()
+ return Submenu.model_validate(result.one())
+
+ async def get_by_id(self, submenu_id: int) -> Submenu:
+ try:
+ obj = await self._get_by_id(submenu_id)
+ except NoResultFound as e:
+ raise SubmenuNotFoundError from e
+ return Submenu.model_validate(obj)
+
+ async def update_by_id(self, submenu_id: int, **kwargs: Any) -> Submenu:
+ try:
+ obj = await self._update(SubmenuDb.id == submenu_id, **kwargs)
+ except EntityNotFoundError as e:
+ raise SubmenuNotFoundError from e
+ except IntegrityError as e:
+ self._raise_error(e)
+ return Submenu.model_validate(obj)
+
+ async def delete_by_id(self, submenu_id: int) -> None:
+ stmt = delete(SubmenuDb).where(SubmenuDb.id == submenu_id)
+ await self._session.execute(stmt)
+
+ async def list(self) -> tuple[Submenu, ...]:
+ query = select(SubmenuDb).order_by(desc(SubmenuDb.weight), SubmenuDb.id)
+ objs = (await self._session.scalars(query)).all()
+ return tuple(Submenu.model_validate(obj) for obj in objs)
+
+ async def get_list_by_type(self, submenu_type: SubmenuType) -> Sequence[Submenu]:
+ stmt = (
+ select(SubmenuDb)
+ .where(SubmenuDb.type == submenu_type)
+ .order_by(desc(SubmenuDb.weight), SubmenuDb.id)
+ )
+
+ objs = (await self._session.scalars(stmt)).all()
+ return [Submenu.model_validate(obj) for obj in objs]
+
+ def _raise_error(self, e: DBAPIError) -> NoReturn:
+ constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
+ if constraint == "pk__submenu":
+ raise SubmenuAlreadyExistsError from e
+ raise InclusiveDanceError from e
diff --git a/idb/db/repositories/url.py b/idb/db/repositories/url.py
new file mode 100644
index 0000000..5e1c437
--- /dev/null
+++ b/idb/db/repositories/url.py
@@ -0,0 +1,81 @@
+from typing import Any, NoReturn
+
+from sqlalchemy import ScalarResult, delete, select
+from sqlalchemy.dialects.postgresql import insert
+from sqlalchemy.exc import DBAPIError, IntegrityError
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from idb.db.models import Url as UrlDb
+from idb.db.repositories.base import Repository
+from idb.exceptions import (
+ InclusiveDanceError,
+ UrlAlreadyExistsError,
+ UrlSlugAlreadyExistsError,
+)
+from idb.exceptions.base import EntityNotFoundError
+from idb.exceptions.url import UrlNotFoundError
+from idb.generals.models.url import Url
+
+
+class UrlRepository(Repository[UrlDb]):
+ def __init__(self, session: AsyncSession) -> None:
+ super().__init__(model=UrlDb, session=session)
+
+ async def create(self, slug: str, value: str, id: int | None = None) -> Url:
+ data: dict[str, str | int] = dict(slug=slug, value=value)
+ if id is not None:
+ data["id"] = id
+ stmt = insert(UrlDb).values(**data).returning(UrlDb)
+ try:
+ result: ScalarResult[UrlDb] = await self._session.scalars(stmt)
+ except IntegrityError as e:
+ self._raise_error(e)
+ await self._session.flush()
+ return Url.model_validate(result.one())
+
+ async def update_by_slug(self, url_slug: str, **kwargs: Any) -> Url:
+ try:
+ url = await self._update(UrlDb.slug == url_slug, **kwargs)
+ except EntityNotFoundError as e:
+ raise UrlNotFoundError from e
+ except IntegrityError as e:
+ self._raise_error(e)
+ return Url.model_validate(url)
+
+ async def delete_by_slug(self, url_slug: str) -> None:
+ stmt = delete(UrlDb).where(UrlDb.slug == url_slug)
+ await self._session.execute(stmt)
+
+ async def list(self) -> tuple[Url, ...]:
+ stmt = select(UrlDb).order_by(UrlDb.id)
+ objs = (await self._session.scalars(stmt)).all()
+ return tuple(Url.model_validate(obj) for obj in objs)
+
+ async def upsert(self, *, id: int, slug: str, value: str) -> Url:
+ stmt = (
+ insert(UrlDb)
+ .values(id=id, slug=slug, value=value)
+ .on_conflict_do_update(
+ index_elements=[UrlDb.slug],
+ set_={
+ "id": id,
+ "slug": slug,
+ "value": value,
+ },
+ )
+ .returning(UrlDb)
+ )
+ try:
+ result: ScalarResult[UrlDb] = await self._session.scalars(stmt)
+ except IntegrityError as e:
+ self._raise_error(e)
+ await self._session.flush()
+ return Url.model_validate(result.one())
+
+ def _raise_error(self, e: DBAPIError) -> NoReturn:
+ constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
+ if constraint == "pk__url":
+ raise UrlAlreadyExistsError from e
+ if constraint == "uq__url__slug":
+ raise UrlSlugAlreadyExistsError from e
+ raise InclusiveDanceError from e
diff --git a/idb/db/repositories/user.py b/idb/db/repositories/user.py
new file mode 100644
index 0000000..cb9d9e3
--- /dev/null
+++ b/idb/db/repositories/user.py
@@ -0,0 +1,131 @@
+from collections.abc import Mapping, Sequence
+from typing import Any, NoReturn
+
+from sqlalchemy import ScalarResult, func, insert, select
+from sqlalchemy.exc import DBAPIError, IntegrityError, NoResultFound
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from idb.db.models import (
+ User as UserDb,
+)
+from idb.db.models import (
+ UserType as UserTypeDb,
+)
+from idb.db.models import (
+ UserTypeUser as UserTypeUserDb,
+)
+from idb.db.repositories.base import Repository
+from idb.exceptions import (
+ EntityNotFoundError,
+ InclusiveDanceError,
+ UserAlreadyExistsError,
+ UserNotFoundError,
+)
+from idb.generals.models.user import User
+from idb.generals.models.user_type import UserType
+
+
+class UserRepository(Repository[UserDb]):
+ def __init__(self, session: AsyncSession) -> None:
+ super().__init__(model=UserDb, session=session)
+
+ async def create(
+ self,
+ *,
+ id: int,
+ username: str | None,
+ is_admin: bool,
+ is_superuser: bool,
+ profile: Mapping[str, Any],
+ ) -> User:
+ stmt = (
+ insert(UserDb)
+ .values(
+ id=id,
+ username=username,
+ is_admin=is_admin,
+ is_superuser=is_superuser,
+ profile=profile,
+ )
+ .returning(UserDb)
+ )
+ try:
+ result: ScalarResult[UserDb] = await self._session.scalars(stmt)
+ except IntegrityError as e:
+ self._raise_error(e)
+ await self._session.flush()
+ return User.model_validate(result.one())
+
+ async def update_by_id(self, user_id: int, **kwargs: Any) -> User:
+ obj = await self._update(UserDb.id == user_id, **kwargs)
+ return User.model_validate(obj)
+
+ async def get_by_id(self, user_id: int) -> User:
+ try:
+ obj = await self._get_by_id(obj_id=user_id)
+ except NoResultFound as e:
+ raise UserNotFoundError from e
+ return User.model_validate(obj)
+
+ async def get_by_id_or_none(self, user_id: int) -> User | None:
+ obj = await self._get_by_id_or_none(user_id)
+ return User.model_validate(obj) if obj else None
+
+ async def get_admin_list(self, include_superusers: bool) -> list[User]:
+ stmt = select(UserDb).where(UserDb.is_admin.is_(True))
+ if not include_superusers:
+ stmt = stmt.where(UserDb.is_superuser.is_(False))
+ return [User.model_validate(obj) for obj in await self._session.scalars(stmt)]
+
+ async def add_to_admins(self, username: str) -> User:
+ try:
+ user = await self._update(
+ UserDb.username == username,
+ UserDb.is_admin.is_(False),
+ is_admin=True,
+ )
+ return User.model_validate(user)
+ except EntityNotFoundError as e:
+ raise UserNotFoundError from e
+
+ async def delete_from_admins(self, user_id: int) -> User:
+ try:
+ user = await self._update(
+ UserDb.id == user_id,
+ UserDb.is_admin.is_(True),
+ is_admin=False,
+ )
+ except EntityNotFoundError as e:
+ raise UserNotFoundError from e
+ return User.model_validate(user)
+
+ async def get_list_by_user_types(
+ self, user_types: Sequence[UserType], ignore_admins: bool = True
+ ) -> list[User]:
+ stmt = select(UserDb)
+ if len(user_types) != 0:
+ stmt = (
+ stmt.distinct(UserDb.id)
+ .join(UserTypeUserDb, UserDb.id == UserTypeUserDb.user_id)
+ .join(UserTypeDb, UserTypeUserDb.user_type_id == UserTypeDb.id)
+ .where(UserTypeDb.name.in_([ut.name for ut in user_types]))
+ )
+
+ if ignore_admins:
+ stmt = stmt.where(UserDb.is_admin.is_(False))
+ result = await self._session.scalars(stmt)
+ return [User.model_validate(obj) for obj in result]
+
+ async def total_count(self) -> int:
+ query = (
+ select(func.count("*"))
+ .select_from(UserDb)
+ .where(UserDb.is_admin.is_(False))
+ )
+ return (await self._session.execute(query)).scalar_one()
+
+ def _raise_error(self, e: DBAPIError) -> NoReturn:
+ constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
+ if constraint == "pk__users":
+ raise UserAlreadyExistsError from e
+ raise InclusiveDanceError from e
diff --git a/idb/db/repositories/user_type.py b/idb/db/repositories/user_type.py
new file mode 100644
index 0000000..8a33cec
--- /dev/null
+++ b/idb/db/repositories/user_type.py
@@ -0,0 +1,64 @@
+from typing import NoReturn
+
+from sqlalchemy import ScalarResult, select
+from sqlalchemy.dialects.postgresql import insert
+from sqlalchemy.exc import DBAPIError, IntegrityError
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from idb.db.models import UserType as UserTypeDb
+from idb.db.repositories.base import Repository
+from idb.exceptions import (
+ InclusiveDanceError,
+ UserTypeAlreadyExistsError,
+)
+from idb.generals.models.user_type import UserType
+
+
+class UserTypeRepository(Repository[UserTypeDb]):
+ def __init__(self, session: AsyncSession) -> None:
+ super().__init__(model=UserTypeDb, session=session)
+
+ async def create(self, *, name: str, id: int | None = None) -> UserType:
+ data: dict[str, str | int] = dict(name=name)
+ if id is not None:
+ data["id"] = id
+ stmt = insert(UserTypeDb).values(**data).returning(UserTypeDb)
+ try:
+ result: ScalarResult[UserTypeDb] = await self._session.scalars(stmt)
+ except IntegrityError as e:
+ self._raise_error(e)
+ await self._session.flush()
+ return UserType.model_validate(result.one())
+
+ async def list(self) -> tuple[UserType, ...]:
+ stmt = select(UserTypeDb).order_by(UserTypeDb.id)
+ return tuple(
+ UserType.model_validate(obj)
+ for obj in (await self._session.scalars(stmt)).all()
+ )
+
+ async def upsert(self, *, id: int, name: str) -> UserType:
+ stmt = (
+ insert(UserTypeDb)
+ .values(id=id, name=name)
+ .on_conflict_do_update(
+ index_elements=[UserTypeDb.id],
+ set_={
+ "id": id,
+ "name": name,
+ },
+ )
+ .returning(UserTypeDb)
+ )
+ try:
+ result: ScalarResult[UserTypeDb] = await self._session.scalars(stmt)
+ except IntegrityError as e:
+ self._raise_error(e)
+ await self._session.flush()
+ return UserType.model_validate(result.one())
+
+ def _raise_error(self, e: DBAPIError) -> NoReturn:
+ constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
+ if constraint in ("pk__user_type", "ix__user_type__name"):
+ raise UserTypeAlreadyExistsError from e
+ raise InclusiveDanceError from e
diff --git a/inclusive_dance_bot/db/repositories/user_type_user.py b/idb/db/repositories/user_type_user.py
similarity index 90%
rename from inclusive_dance_bot/db/repositories/user_type_user.py
rename to idb/db/repositories/user_type_user.py
index dfdf446..b19b9ed 100644
--- a/inclusive_dance_bot/db/repositories/user_type_user.py
+++ b/idb/db/repositories/user_type_user.py
@@ -4,9 +4,9 @@
from sqlalchemy.exc import DBAPIError, IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
-from inclusive_dance_bot.db.models import UserTypeUser
-from inclusive_dance_bot.db.repositories.base import Repository
-from inclusive_dance_bot.exceptions import (
+from idb.db.models import UserTypeUser
+from idb.db.repositories.base import Repository
+from idb.exceptions import (
InclusiveDanceError,
InvalidUserIDError,
InvalidUserTypeIDError,
diff --git a/idb/db/uow.py b/idb/db/uow.py
new file mode 100644
index 0000000..8cc2549
--- /dev/null
+++ b/idb/db/uow.py
@@ -0,0 +1,44 @@
+import asyncio
+from collections.abc import AsyncGenerator
+from contextlib import asynccontextmanager
+
+from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
+
+from idb.db.repositories.answer import AnswerRepository
+from idb.db.repositories.feedback import FeedbackRepository
+from idb.db.repositories.mailing import MailingRepository
+from idb.db.repositories.submenu import SubmenuRepository
+from idb.db.repositories.url import UrlRepository
+from idb.db.repositories.user import UserRepository
+from idb.db.repositories.user_type import UserTypeRepository
+from idb.db.repositories.user_type_user import UserTypeUserRepository
+
+
+class UnitOfWork:
+ def __init__(self, sessionmaker: async_sessionmaker[AsyncSession]) -> None:
+ self._session = sessionmaker()
+ self.submenus = SubmenuRepository(self._session)
+ self.feedbacks = FeedbackRepository(self._session)
+ self.mailings = MailingRepository(self._session)
+ self.urls = UrlRepository(self._session)
+ self.users = UserRepository(self._session)
+ self.user_types = UserTypeRepository(self._session)
+ self.user_type_users = UserTypeUserRepository(self._session)
+ self.answer = AnswerRepository(self._session)
+
+ async def commit(self) -> None:
+ await self._session.commit()
+
+ async def rollback(self) -> None:
+ await self._session.rollback()
+ task = asyncio.create_task(self._session.close())
+ await asyncio.shield(task)
+
+
+@asynccontextmanager
+async def uow_context(
+ sessionmaker: async_sessionmaker[AsyncSession],
+) -> AsyncGenerator[UnitOfWork, None]:
+ uow = UnitOfWork(sessionmaker=sessionmaker)
+ yield uow
+ await uow.rollback()
diff --git a/inclusive_dance_bot/db/utils.py b/idb/db/utils.py
similarity index 87%
rename from inclusive_dance_bot/db/utils.py
rename to idb/db/utils.py
index b88e388..1e1792a 100644
--- a/inclusive_dance_bot/db/utils.py
+++ b/idb/db/utils.py
@@ -11,9 +11,9 @@
create_async_engine,
)
-import inclusive_dance_bot
+import idb
-PROJECT_PATH = Path(inclusive_dance_bot.__file__).parent.parent.resolve()
+PROJECT_PATH = Path(idb.__file__).parent.parent.resolve()
def create_engine(connection_uri: str, **engine_kwargs: Any) -> AsyncEngine:
@@ -30,7 +30,7 @@ def create_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSessi
def make_alembic_config(cmd_opts: Namespace, base_path: Path = PROJECT_PATH) -> Config:
if not os.path.isabs(cmd_opts.config):
- cmd_opts.config = str(base_path / "inclusive_dance_bot/db" / cmd_opts.config)
+ cmd_opts.config = str(base_path / "idb/db" / cmd_opts.config)
config = Config(
file_=cmd_opts.config,
diff --git a/idb/deps.py b/idb/deps.py
new file mode 100644
index 0000000..6fb4faf
--- /dev/null
+++ b/idb/deps.py
@@ -0,0 +1,44 @@
+import asyncio
+from argparse import Namespace
+from collections.abc import AsyncGenerator
+
+from aiogram import Bot
+from aiomisc_dependency import dependency
+from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
+
+from idb.bot.factory import get_bot
+from idb.db.uow import uow_context
+from idb.db.utils import create_engine, create_session_factory
+from idb.utils.cache import MemoryCache
+
+
+def config_deps(arguments: Namespace) -> None:
+ @dependency
+ async def bot() -> Bot:
+ return get_bot(
+ telegram_bot_token=arguments.telegram_bot_token,
+ parse_mode=arguments.telegram_parse_mode,
+ )
+
+ @dependency
+ async def engine() -> AsyncGenerator[AsyncEngine, None]:
+ engine = create_engine(
+ connection_uri=arguments.pg_dsn,
+ echo=arguments.debug,
+ )
+ yield engine
+ await asyncio.shield(engine.dispose())
+
+ @dependency
+ async def sessionmaker(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
+ return create_session_factory(engine=engine)
+
+ @dependency
+ async def cache(sessionmaker: async_sessionmaker[AsyncSession]) -> MemoryCache:
+ cache = MemoryCache()
+ async with uow_context(sessionmaker=sessionmaker) as uow:
+ await cache.load_cache(uow=uow)
+
+ return cache
+
+ return
diff --git a/tests/test_unit/test_services/test_mailing/test_process_new_mailing.py b/idb/dto.py
similarity index 100%
rename from tests/test_unit/test_services/test_mailing/test_process_new_mailing.py
rename to idb/dto.py
diff --git a/inclusive_dance_bot/exceptions/__init__.py b/idb/exceptions/__init__.py
similarity index 75%
rename from inclusive_dance_bot/exceptions/__init__.py
rename to idb/exceptions/__init__.py
index 32ac10d..0a5f704 100644
--- a/inclusive_dance_bot/exceptions/__init__.py
+++ b/idb/exceptions/__init__.py
@@ -1,19 +1,19 @@
-from inclusive_dance_bot.exceptions.base import (
+from idb.exceptions.base import (
EntityAlreadyExistsError,
EntityNotFoundError,
InclusiveDanceError,
)
-from inclusive_dance_bot.exceptions.mailing import MailingNotFoundError
-from inclusive_dance_bot.exceptions.submenu import (
+from idb.exceptions.mailing import MailingNotFoundError
+from idb.exceptions.submenu import (
SubmenuAlreadyExistsError,
SubmenuNotFoundError,
)
-from inclusive_dance_bot.exceptions.url import (
+from idb.exceptions.url import (
UrlAlreadyExistsError,
UrlNotFoundError,
UrlSlugAlreadyExistsError,
)
-from inclusive_dance_bot.exceptions.user import (
+from idb.exceptions.user import (
InvalidUserIDError,
InvalidUserTypeIDError,
UserAlreadyExistsError,
diff --git a/inclusive_dance_bot/exceptions/base.py b/idb/exceptions/base.py
similarity index 100%
rename from inclusive_dance_bot/exceptions/base.py
rename to idb/exceptions/base.py
diff --git a/idb/exceptions/mailing.py b/idb/exceptions/mailing.py
new file mode 100644
index 0000000..e89efbc
--- /dev/null
+++ b/idb/exceptions/mailing.py
@@ -0,0 +1,5 @@
+from idb.exceptions.base import EntityNotFoundError
+
+
+class MailingNotFoundError(EntityNotFoundError):
+ pass
diff --git a/inclusive_dance_bot/exceptions/submenu.py b/idb/exceptions/submenu.py
similarity index 78%
rename from inclusive_dance_bot/exceptions/submenu.py
rename to idb/exceptions/submenu.py
index 9855261..b7c4f0d 100644
--- a/inclusive_dance_bot/exceptions/submenu.py
+++ b/idb/exceptions/submenu.py
@@ -1,4 +1,4 @@
-from inclusive_dance_bot.exceptions.base import (
+from idb.exceptions.base import (
EntityAlreadyExistsError,
EntityNotFoundError,
)
diff --git a/inclusive_dance_bot/exceptions/url.py b/idb/exceptions/url.py
similarity index 83%
rename from inclusive_dance_bot/exceptions/url.py
rename to idb/exceptions/url.py
index f226023..0298824 100644
--- a/inclusive_dance_bot/exceptions/url.py
+++ b/idb/exceptions/url.py
@@ -1,4 +1,4 @@
-from inclusive_dance_bot.exceptions.base import (
+from idb.exceptions.base import (
EntityAlreadyExistsError,
EntityNotFoundError,
)
diff --git a/inclusive_dance_bot/exceptions/user.py b/idb/exceptions/user.py
similarity index 90%
rename from inclusive_dance_bot/exceptions/user.py
rename to idb/exceptions/user.py
index 4212207..87a487e 100644
--- a/inclusive_dance_bot/exceptions/user.py
+++ b/idb/exceptions/user.py
@@ -1,4 +1,4 @@
-from inclusive_dance_bot.exceptions.base import (
+from idb.exceptions.base import (
EntityAlreadyExistsError,
EntityNotFoundError,
InclusiveDanceError,
diff --git a/tests/test_unit/test_services/test_mailing/test_save_mailing.py b/idb/generals/__init__.py
similarity index 100%
rename from tests/test_unit/test_services/test_mailing/test_save_mailing.py
rename to idb/generals/__init__.py
diff --git a/inclusive_dance_bot/enums.py b/idb/generals/enums.py
similarity index 76%
rename from inclusive_dance_bot/enums.py
rename to idb/generals/enums.py
index 2cb8e72..5938f84 100644
--- a/inclusive_dance_bot/enums.py
+++ b/idb/generals/enums.py
@@ -17,11 +17,16 @@ class FeedbackType(StrEnum):
ADVERTISEMENT = "ADVERTISEMENT"
+FEEDBACK_TYPE_MAPPING = {
+ FeedbackType.QUESTION: "Вопрос",
+ FeedbackType.ADVERTISEMENT: "Предложение",
+}
+
+
@unique
-class StorageType(StrEnum):
- URL = "URL"
- USER_TYPE = "USER_TYPE"
- SUBMENU = "SUBMENU"
+class FeedbackStatus(StrEnum):
+ NEW = "NEW"
+ ARCHIVED = "ARCHIVED"
@unique
diff --git a/tests/test_unit/test_services/test_mailing/test_send_mailngs.py b/idb/generals/models/__init__.py
similarity index 100%
rename from tests/test_unit/test_services/test_mailing/test_send_mailngs.py
rename to idb/generals/models/__init__.py
diff --git a/idb/generals/models/answer.py b/idb/generals/models/answer.py
new file mode 100644
index 0000000..d90e884
--- /dev/null
+++ b/idb/generals/models/answer.py
@@ -0,0 +1,15 @@
+from datetime import datetime
+
+from pydantic import BaseModel, ConfigDict
+
+
+class Answer(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: int
+ feedback_id: int
+ from_user_id: int
+ to_user_id: int
+ text: str
+ created_at: datetime
+ updated_at: datetime
diff --git a/idb/generals/models/feedback.py b/idb/generals/models/feedback.py
new file mode 100644
index 0000000..0f2834f
--- /dev/null
+++ b/idb/generals/models/feedback.py
@@ -0,0 +1,21 @@
+from datetime import datetime
+
+from pydantic import BaseModel, ConfigDict, PositiveInt
+
+from idb.generals.enums import FeedbackType
+
+
+class Feedback(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: PositiveInt
+ user_id: int
+ type: FeedbackType
+ title: str
+ text: str
+ is_viewed: bool
+ viewed_at: datetime | None
+ is_answered: bool
+ answered_at: datetime | None
+ created_at: datetime
+ updated_at: datetime
diff --git a/idb/generals/models/mailing.py b/idb/generals/models/mailing.py
new file mode 100644
index 0000000..c052763
--- /dev/null
+++ b/idb/generals/models/mailing.py
@@ -0,0 +1,22 @@
+from collections.abc import Sequence
+from datetime import datetime
+
+from pydantic import BaseModel, ConfigDict, PositiveInt
+
+from idb.generals.enums import MailingStatus
+from idb.generals.models.user_type import UserType
+
+
+class Mailing(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ created_at: datetime
+ updated_at: datetime
+ id: PositiveInt
+ scheduled_at: datetime | None
+ sent_at: datetime | None
+ cancelled_at: datetime | None
+ status: MailingStatus
+ title: str
+ content: str
+ user_types: Sequence[UserType]
diff --git a/idb/generals/models/submenu.py b/idb/generals/models/submenu.py
new file mode 100644
index 0000000..22b8275
--- /dev/null
+++ b/idb/generals/models/submenu.py
@@ -0,0 +1,13 @@
+from pydantic import BaseModel, ConfigDict, PositiveInt
+
+from idb.generals.enums import SubmenuType
+
+
+class Submenu(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: PositiveInt
+ type: SubmenuType
+ weight: int
+ button_text: str
+ message: str
diff --git a/idb/generals/models/url.py b/idb/generals/models/url.py
new file mode 100644
index 0000000..4ee168b
--- /dev/null
+++ b/idb/generals/models/url.py
@@ -0,0 +1,12 @@
+from pydantic import BaseModel, ConfigDict, PositiveInt
+
+
+class Url(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: PositiveInt
+ slug: str
+ value: str
+
+ def __str__(self) -> str:
+ return self.value
diff --git a/idb/generals/models/user.py b/idb/generals/models/user.py
new file mode 100644
index 0000000..c6ea965
--- /dev/null
+++ b/idb/generals/models/user.py
@@ -0,0 +1,48 @@
+from collections.abc import Mapping
+from dataclasses import dataclass
+from typing import Any
+
+from aiogram.types import User as TelegramUser
+from pydantic import BaseModel, ConfigDict, NonNegativeInt
+
+ANONYMOUS_USER_ID = 0
+
+
+class User(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: NonNegativeInt
+ username: str | None
+ is_admin: bool
+ is_superuser: bool
+ profile: Mapping[str, Any]
+
+ @property
+ def name(self) -> str | None:
+ return self.profile.get("name")
+
+ @property
+ def region(self) -> str | None:
+ return self.profile.get("region")
+
+ @property
+ def phone_number(self) -> str | None:
+ return self.profile.get("phone_number")
+
+
+@dataclass
+class BotUser:
+ telegram_user: TelegramUser
+ user: User
+
+ @property
+ def is_superuser(self) -> bool:
+ return self.user.is_superuser
+
+ @property
+ def is_admin(self) -> bool:
+ return self.user.is_admin or self.is_superuser
+
+ @property
+ def is_anonymous(self) -> bool:
+ return not bool(self.user.profile)
diff --git a/idb/generals/models/user_type.py b/idb/generals/models/user_type.py
new file mode 100644
index 0000000..79dcd56
--- /dev/null
+++ b/idb/generals/models/user_type.py
@@ -0,0 +1,8 @@
+from pydantic import BaseModel, ConfigDict, PositiveInt
+
+
+class UserType(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: PositiveInt
+ name: str
diff --git a/idb/logic/__init__.py b/idb/logic/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/idb/logic/answer.py b/idb/logic/answer.py
new file mode 100644
index 0000000..866bf60
--- /dev/null
+++ b/idb/logic/answer.py
@@ -0,0 +1,24 @@
+from idb.db.uow import UnitOfWork
+from idb.exceptions.base import InclusiveDanceError
+from idb.generals.models.answer import Answer
+
+
+async def create_feedback_answer(
+ uow: UnitOfWork,
+ from_user_id: int,
+ feedback_id: int,
+ text: str,
+) -> Answer:
+ feedback = await uow.feedbacks.read_by_id(feedback_id)
+ try:
+ answer = await uow.answer.create(
+ feedback_id=feedback_id,
+ from_user_id=from_user_id,
+ to_user_id=feedback.user_id,
+ text=text,
+ )
+ await uow.commit()
+ except InclusiveDanceError as e:
+ await uow.rollback()
+ raise e
+ return answer
diff --git a/idb/logic/feedback.py b/idb/logic/feedback.py
new file mode 100644
index 0000000..a0545a7
--- /dev/null
+++ b/idb/logic/feedback.py
@@ -0,0 +1,56 @@
+from datetime import datetime
+
+from idb.db.uow import UnitOfWork
+from idb.exceptions.base import InclusiveDanceError
+from idb.generals.enums import FeedbackType
+
+
+async def create_feedback(
+ uow: UnitOfWork, user_id: int, type: FeedbackType, title: str, text: str
+) -> None:
+ """Создает новую обратную связь от пользователя"""
+ try:
+ await uow.feedbacks.create(
+ user_id=user_id,
+ type=type,
+ title=title,
+ text=text,
+ )
+ await uow.commit()
+ except InclusiveDanceError as e:
+ await uow.rollback()
+ raise e
+
+
+async def set_feedback_as_viewed(
+ uow: UnitOfWork,
+ feedback_id: int,
+ dt: datetime,
+) -> None:
+ try:
+ await uow.feedbacks.update_by_id(
+ feedback_id=feedback_id,
+ is_viewed=True,
+ viewed_at=dt,
+ )
+ await uow.commit()
+ except InclusiveDanceError as e:
+ await uow.rollback()
+ raise e
+
+
+async def update_answered_feedback(
+ uow: UnitOfWork,
+ feedback_id: int,
+ dt: datetime,
+) -> None:
+ try:
+ await uow.feedbacks.update_by_id(
+ feedback_id=feedback_id,
+ is_answered=True,
+ answered_at=dt,
+ )
+ await uow.commit()
+ except InclusiveDanceError as e:
+ await uow.rollback()
+ raise e
diff --git a/inclusive_dance_bot/logic/mailing.py b/idb/logic/mailing.py
similarity index 87%
rename from inclusive_dance_bot/logic/mailing.py
rename to idb/logic/mailing.py
index 5d00727..d4264f9 100644
--- a/inclusive_dance_bot/logic/mailing.py
+++ b/idb/logic/mailing.py
@@ -1,14 +1,14 @@
import logging
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from typing import Any
import pytz
from aiogram import Bot
from aiogram.enums import ParseMode
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.dto import MailingDto
-from inclusive_dance_bot.enums import MailingStatus
+from idb.db.uow import UnitOfWork
+from idb.generals.enums import MailingStatus
+from idb.generals.models.mailing import Mailing
log = logging.getLogger(__name__)
@@ -19,14 +19,14 @@ async def send_mailings(
gap: int,
) -> None:
new_mailings = await uow.mailings.get_new_mailings(
- gap=gap, now=datetime.now(tz=timezone.utc)
+ gap=gap, now=datetime.now(tz=UTC)
)
log.info("Found %d new mailings", len(new_mailings))
for mailing in new_mailings:
await process_new_mailing(uow=uow, bot=bot, mailing=mailing)
-async def process_new_mailing(uow: UnitOfWork, bot: Bot, mailing: MailingDto) -> None:
+async def process_new_mailing(uow: UnitOfWork, bot: Bot, mailing: Mailing) -> None:
log.info("Start process mailing_id=%s", mailing.id)
users = await uow.users.get_list_by_user_types(user_types=mailing.user_types)
for user in users:
@@ -71,7 +71,7 @@ async def save_mailing(
async def update_mailing_by_id(
uow: UnitOfWork, mailing_id: int, **kwargs: Any
-) -> MailingDto:
+) -> Mailing:
mailing = await uow.mailings.update_by_id(mailing_id=mailing_id, **kwargs)
await uow.commit()
return mailing
diff --git a/inclusive_dance_bot/logic/submenu.py b/idb/logic/submenu.py
similarity index 70%
rename from inclusive_dance_bot/logic/submenu.py
rename to idb/logic/submenu.py
index 2d40f3c..9645450 100644
--- a/inclusive_dance_bot/logic/submenu.py
+++ b/idb/logic/submenu.py
@@ -1,19 +1,19 @@
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.dto import SubmenuDto
-from inclusive_dance_bot.enums import SubmenuType
-from inclusive_dance_bot.exceptions.base import InclusiveDanceError
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.utils import NOT_SET, NotSet
+from idb.db.uow import UnitOfWork
+from idb.exceptions.base import InclusiveDanceError
+from idb.generals.enums import SubmenuType
+from idb.generals.models.submenu import Submenu
+from idb.utils.cache import AbstractBotCache
+from idb.utils.urls import NOT_SET, NotSet
async def create_submenu(
uow: UnitOfWork,
- storage: Storage,
+ cache: AbstractBotCache,
type: SubmenuType,
message: str,
button_text: str,
weight: int,
-) -> SubmenuDto:
+) -> Submenu:
try:
submenu = await uow.submenus.create(
type=type,
@@ -25,27 +25,27 @@ async def create_submenu(
await uow.rollback()
raise e
await uow.commit()
- await storage.refresh_submenus()
+ await cache.update_submenu(id_=submenu.id, submenu=submenu)
return submenu
async def delete_submenu_by_id(
- uow: UnitOfWork, storage: Storage, submenu_id: int
+ uow: UnitOfWork, cache: AbstractBotCache, submenu_id: int
) -> None:
await uow.submenus.delete_by_id(submenu_id=submenu_id)
await uow.commit()
- await storage.refresh_submenus()
+ await cache.update_submenu(id_=submenu_id, submenu=None)
async def update_submenu_by_id(
uow: UnitOfWork,
- storage: Storage,
+ cache: AbstractBotCache,
submenu_id: int,
weight: int | NotSet = NOT_SET,
type_: SubmenuType | NotSet = NOT_SET,
button_text: str | NotSet = NOT_SET,
message: str | NotSet = NOT_SET,
-) -> SubmenuDto:
+) -> Submenu:
data: dict[str, str | int | SubmenuType] = {}
if not isinstance(weight, NotSet):
data["weight"] = weight
@@ -63,5 +63,5 @@ async def update_submenu_by_id(
await uow.rollback()
raise e
await uow.commit()
- await storage.refresh_submenus()
+ await cache.update_submenu(id_=submenu.id, submenu=submenu)
return submenu
diff --git a/idb/logic/url.py b/idb/logic/url.py
new file mode 100644
index 0000000..9d7933a
--- /dev/null
+++ b/idb/logic/url.py
@@ -0,0 +1,47 @@
+from typing import Any
+
+from idb.db.uow import UnitOfWork
+from idb.exceptions.base import InclusiveDanceError
+from idb.exceptions.url import UrlSlugAlreadyExistsError
+from idb.generals.models.url import Url
+from idb.utils.cache import AbstractBotCache
+
+
+async def create_url(
+ uow: UnitOfWork, cache: AbstractBotCache, slug: str, value: str
+) -> Url:
+ """Создает новую ссылку"""
+ try:
+ url = await uow.urls.create(slug=slug, value=value)
+ except UrlSlugAlreadyExistsError as e:
+ await uow.rollback()
+ raise e
+ await uow.commit()
+ await cache.update_url(slug=url.slug, url=url)
+ return url
+
+
+async def update_url_by_slug(
+ uow: UnitOfWork,
+ cache: AbstractBotCache,
+ url_slug: str,
+ **kwargs: Any,
+) -> Url:
+ try:
+ url = await uow.urls.update_by_slug(url_slug=url_slug, **kwargs)
+ except InclusiveDanceError as e:
+ await uow.rollback()
+ raise e
+ await uow.commit()
+ if url.slug != url_slug:
+ await cache.update_url(slug=url_slug, url=None)
+ await cache.update_url(slug=url.slug, url=url)
+ return url
+
+
+async def delete_url_by_slug(
+ uow: UnitOfWork, cache: AbstractBotCache, url_slug: str
+) -> None:
+ await uow.urls.delete_by_slug(url_slug=url_slug)
+ await uow.commit()
+ await cache.update_url(slug=url_slug, url=None)
diff --git a/idb/logic/users.py b/idb/logic/users.py
new file mode 100644
index 0000000..1184905
--- /dev/null
+++ b/idb/logic/users.py
@@ -0,0 +1,51 @@
+from collections.abc import Iterable
+
+from idb.db.uow import UnitOfWork
+from idb.exceptions.base import InclusiveDanceError
+
+
+async def save_profile_user(
+ uow: UnitOfWork,
+ user_id: int,
+ name: str,
+ region: str,
+ phone_number: str,
+ user_type_ids: Iterable[int],
+) -> None:
+ """Сохраняет профиль нового пользователя"""
+ try:
+ await uow.users.update_by_id(
+ user_id=user_id,
+ profile={
+ "name": name,
+ "region": region,
+ "phone_number": phone_number,
+ },
+ )
+ for user_type_id in user_type_ids:
+ await uow.user_type_users.create(
+ user_id=user_id,
+ user_type_id=user_type_id,
+ )
+ await uow.commit()
+ except InclusiveDanceError as e:
+ await uow.rollback()
+ raise e
+
+
+async def add_user_to_admins(uow: UnitOfWork, username: str) -> None:
+ try:
+ await uow.users.add_to_admins(username=username)
+ await uow.commit()
+ except InclusiveDanceError as e:
+ await uow.rollback()
+ raise e
+
+
+async def delete_from_admins(uow: UnitOfWork, user_id: int) -> None:
+ try:
+ await uow.users.delete_from_admins(user_id=user_id)
+ await uow.commit()
+ except InclusiveDanceError as e:
+ await uow.rollback()
+ raise e
diff --git a/idb/utils/__init__.py b/idb/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/idb/utils/cache.py b/idb/utils/cache.py
new file mode 100644
index 0000000..3ed2aea
--- /dev/null
+++ b/idb/utils/cache.py
@@ -0,0 +1,220 @@
+import json
+from abc import ABC, abstractmethod
+from collections.abc import AsyncGenerator, Callable, Mapping, MutableMapping
+from dataclasses import asdict, dataclass
+from enum import StrEnum, unique
+from typing import Any
+
+from redis.asyncio.client import Redis
+from redis.asyncio.connection import ConnectionPool
+
+from idb.db.uow import UnitOfWork
+from idb.generals.models.submenu import Submenu
+from idb.generals.models.url import Url
+from idb.generals.models.user_type import UserType
+
+_JsonLoads = Callable[..., Any]
+_JsonDumps = Callable[..., str]
+
+
+@unique
+class CacheType(StrEnum):
+ URL = "URL"
+ USER_TYPE = "USER_TYPE"
+ SUBMENU = "SUBMENU"
+
+
+@dataclass(frozen=True)
+class CacheKey:
+ entity_type: CacheType
+ entity_id: str
+
+
+class KeyBuilder:
+ def __init__(self, prefix: str = "bot", sep: str = "__") -> None:
+ self._sep = sep
+ self._prefix = prefix
+
+ def build_key(self, group: CacheType, entity_id: str | int) -> str:
+ return self._sep.join([self._prefix, group, str(entity_id)])
+
+ def build_group_pattern(self, group: CacheType) -> str:
+ return self._sep.join([self._prefix, group]) + "*"
+
+ def parse_id(self, dumped_key: str) -> str:
+ return dumped_key.split(self._sep)[-1]
+
+
+class AbstractBotCache(ABC):
+ @abstractmethod
+ async def get_urls(self) -> Mapping[str, Url]:
+ pass
+
+ @abstractmethod
+ async def get_url_by_slug(self, slug: str) -> Url:
+ pass
+
+ @abstractmethod
+ async def get_submenus(self) -> Mapping[int, Submenu]:
+ pass
+
+ @abstractmethod
+ async def get_submenu_by_id(self, id_: int) -> Submenu:
+ pass
+
+ @abstractmethod
+ async def get_user_types(self) -> Mapping[int, UserType]:
+ pass
+
+ @abstractmethod
+ async def update_url(self, slug: str, url: Url | None) -> None:
+ pass
+
+ @abstractmethod
+ async def update_submenu(self, id_: int, submenu: Submenu | None) -> None:
+ pass
+
+ @abstractmethod
+ async def load_cache(self, uow: UnitOfWork) -> None:
+ pass
+
+
+class RedisCache(AbstractBotCache):
+ _redis: Redis
+ _key_builder: KeyBuilder
+ _json_dumps: _JsonDumps
+ _json_loads: _JsonLoads
+
+ def __init__(
+ self,
+ redis: Redis,
+ key_builder: KeyBuilder,
+ json_dumps: _JsonDumps = json.dumps,
+ json_loads: _JsonLoads = json.loads,
+ ) -> None:
+ self._redis = redis
+ self._key_builder = key_builder
+ self._json_dumps = json_dumps
+ self._json_loads = json_loads
+
+ @classmethod
+ def from_url(
+ cls, url: str, connection_kwargs: dict[str, Any] | None = None, **kwargs: Any
+ ) -> "RedisCache":
+ if connection_kwargs is None:
+ connection_kwargs = {}
+ pool = ConnectionPool.from_url(url, **connection_kwargs)
+ redis = Redis(connection_pool=pool)
+ return cls(redis=redis, **kwargs)
+
+ async def close(self) -> None:
+ await self._redis.close()
+
+ async def flushall(self) -> None:
+ await self._redis.flushall()
+
+ async def _set_data(self, group: CacheType, entity_id: int | str, obj: Any) -> None:
+ key = self._key_builder.build_key(group, entity_id)
+ if obj is None:
+ await self._redis.delete(key)
+ else:
+ await self._redis.set(obj, self._json_dumps(asdict(obj)))
+
+ async def _get_data(
+ self, group: CacheType, entity_id: int | str
+ ) -> Mapping[str, Any]:
+ key = self._key_builder.build_key(group, entity_id)
+ return self._json_loads(self._redis.get(key))
+
+ async def load_cache(self, uow: UnitOfWork) -> None:
+ for submenu in await uow.submenus.list():
+ await self._set_data(CacheType.SUBMENU, submenu.id, submenu)
+
+ for url in await uow.urls.list():
+ await self._set_data(CacheType.URL, url.slug, url)
+
+ for ut in await uow.user_types.list():
+ await self._set_data(CacheType.USER_TYPE, ut.id, ut)
+
+ async def _get_group_iter(
+ self, group: CacheType
+ ) -> AsyncGenerator[tuple[str, Any], None]:
+ pattern = self._key_builder.build_group_pattern(group)
+ async for key in self._redis.scan_iter(pattern):
+ yield key, await self._redis.get(key)
+
+ async def get_submenus(self) -> Mapping[int, Submenu]:
+ submenus: dict[int, Submenu] = {}
+ async for key, value in self._get_group_iter(CacheType.SUBMENU):
+ submenus[int(key)] = Submenu(**self._json_loads(value))
+ return submenus
+
+ async def get_urls(self) -> Mapping[str, Url]:
+ urls: dict[str, Url] = {}
+ async for key, value in self._get_group_iter(CacheType.URL):
+ urls[key] = Url(**self._json_loads(value))
+ return urls
+
+ async def get_user_types(self) -> Mapping[int, UserType]:
+ user_types: dict[int, UserType] = {}
+ async for key, value in self._get_group_iter(CacheType.USER_TYPE):
+ user_types[int(key)] = UserType(**self._json_loads(value))
+ return user_types
+
+ async def update_submenu(self, id_: int, submenu: Submenu | None) -> None:
+ await self._set_data(group=CacheType.SUBMENU, entity_id=id_, obj=submenu)
+
+ async def update_url(self, slug: str, url: Url | None) -> None:
+ await self._set_data(group=CacheType.URL, entity_id=slug, obj=url)
+
+ async def get_submenu_by_id(self, id_: int) -> Submenu:
+ data = await self._get_data(group=CacheType.SUBMENU, entity_id=id_)
+ return Submenu(**data)
+
+ async def get_url_by_slug(self, slug: str) -> Url:
+ data = await self._get_data(group=CacheType.URL, entity_id=slug)
+ return Url(**data)
+
+
+class MemoryCache(AbstractBotCache):
+ def __init__(self) -> None:
+ self._urls: MutableMapping[str, Url] = {}
+ self._submenus: MutableMapping[int, Submenu] = {}
+ self._user_types: MutableMapping[int, UserType] = {}
+
+ async def load_cache(self, uow: UnitOfWork) -> None:
+ urls = await uow.urls.list()
+ self._urls = {url.slug: url for url in urls}
+
+ submenus = await uow.submenus.list()
+ self._submenus = {submenu.id: submenu for submenu in submenus}
+
+ user_types = await uow.user_types.list()
+ self._user_types = {ut.id: ut for ut in user_types}
+
+ async def get_urls(self) -> Mapping[str, Url]:
+ return self._urls
+
+ async def get_url_by_slug(self, slug: str) -> Url:
+ return self._urls[slug]
+
+ async def get_submenus(self) -> Mapping[int, Submenu]:
+ return self._submenus
+
+ async def get_submenu_by_id(self, id_: int) -> Submenu:
+ return self._submenus[id_]
+
+ async def get_user_types(self) -> Mapping[int, UserType]:
+ return self._user_types
+
+ async def update_url(self, slug: str, url: Url | None) -> None:
+ if url is None and slug in self._urls:
+ del self._urls[slug]
+ elif url is not None:
+ self._urls[slug] = url
+
+ async def update_submenu(self, id_: int, submenu: Submenu | None) -> None:
+ if submenu is None and id_ in self._submenus:
+ del self._submenus[id_]
+ elif submenu is not None:
+ self._submenus[id_] = submenu
diff --git a/inclusive_dance_bot/utils.py b/idb/utils/urls.py
similarity index 57%
rename from inclusive_dance_bot/utils.py
rename to idb/utils/urls.py
index 2ace252..81e69d9 100644
--- a/inclusive_dance_bot/utils.py
+++ b/idb/utils/urls.py
@@ -1,9 +1,9 @@
import string
-def check_slug(s: str) -> bool:
+def check_slug(slug: str) -> bool:
alphabet = string.ascii_lowercase + string.digits + "_"
- return all(l in alphabet for l in s)
+ return all(symbol in alphabet for symbol in slug)
class NotSet:
diff --git a/inclusive_dance_bot/bot/dialogs/__init__.py b/inclusive_dance_bot/bot/dialogs/__init__.py
deleted file mode 100644
index 9b7dcd3..0000000
--- a/inclusive_dance_bot/bot/dialogs/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from aiogram import Router
-from aiogram.filters import Command
-
-from inclusive_dance_bot.bot.dialogs import admins, users
-from inclusive_dance_bot.bot.dialogs.commands import start_command
-from inclusive_dance_bot.bot.ui_commands import Commands
-
-
-def register_dialogs(router: Router) -> None:
- dialog_router = Router()
- dialog_router.include_routers(admins.dialog_router, users.dialog_router)
- dialog_router.message(Command(Commands.START))(start_command)
- router.include_router(dialog_router)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/__init__.py b/inclusive_dance_bot/bot/dialogs/admins/__init__.py
deleted file mode 100644
index e218f48..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/__init__.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from aiogram import Router
-
-from inclusive_dance_bot.bot.dialogs.admins import (
- feedbacks,
- mailings,
- main_menu,
- manage_admins,
- submenu,
- url,
-)
-
-dialog_router = Router(name="admin_router")
-dialog_router.include_routers(
- feedbacks.router,
- main_menu.dialog,
- submenu.router,
- url.router,
- manage_admins.router,
- mailings.router,
-)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/__init__.py b/inclusive_dance_bot/bot/dialogs/admins/feedbacks/__init__.py
deleted file mode 100644
index 01cbd31..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from aiogram import Router
-
-from inclusive_dance_bot.bot.dialogs.admins.feedbacks import answer, items
-
-router = Router()
-router.include_routers(
- items.dialog,
- answer.dialog,
-)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/answer/__init__.py b/inclusive_dance_bot/bot/dialogs/admins/feedbacks/answer/__init__.py
deleted file mode 100644
index ac97b58..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/answer/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from aiogram_dialog import Dialog
-
-from inclusive_dance_bot.bot.dialogs.admins.feedbacks.answer import input_message
-
-dialog = Dialog(
- input_message.window,
-)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/answer/input_message.py b/inclusive_dance_bot/bot/dialogs/admins/feedbacks/answer/input_message.py
deleted file mode 100644
index 062e7f2..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/answer/input_message.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from aiogram_dialog import Window
-
-from inclusive_dance_bot.bot.dialogs.admins.states import FeedbackAsnwerSG
-
-window = Window(
- state=FeedbackAsnwerSG.input_message,
-)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/items/__init__.py b/inclusive_dance_bot/bot/dialogs/admins/feedbacks/items/__init__.py
deleted file mode 100644
index 0614ca0..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/items/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from aiogram_dialog import Dialog
-
-from inclusive_dance_bot.bot.dialogs.admins.feedbacks.items import archive, new
-
-dialog = Dialog(
- new.window,
- archive.window,
-)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/items/archive.py b/inclusive_dance_bot/bot/dialogs/admins/feedbacks/items/archive.py
deleted file mode 100644
index 86057df..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/items/archive.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from aiogram_dialog import Window
-
-from inclusive_dance_bot.bot.dialogs.admins.states import FeedbackItemsSG
-
-window = Window(
- state=FeedbackItemsSG.archive,
-)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/items/new.py b/inclusive_dance_bot/bot/dialogs/admins/feedbacks/items/new.py
deleted file mode 100644
index 928dc2a..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/feedbacks/items/new.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from aiogram_dialog import Window
-
-from inclusive_dance_bot.bot.dialogs.admins.states import FeedbackItemsSG
-
-window = Window(
- state=FeedbackItemsSG.new,
-)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/main_menu/__init__.py b/inclusive_dance_bot/bot/dialogs/admins/main_menu/__init__.py
deleted file mode 100644
index ea42e07..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/main_menu/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from aiogram_dialog import Dialog
-
-from inclusive_dance_bot.bot.dialogs.admins.main_menu.read import menu
-
-dialog = Dialog(menu.window)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/main_menu/read/menu.py b/inclusive_dance_bot/bot/dialogs/admins/main_menu/read/menu.py
deleted file mode 100644
index 16efc1d..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/main_menu/read/menu.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from typing import Any
-
-from aiogram_dialog import DialogManager, Window
-from aiogram_dialog.widgets.kbd import Start
-from aiogram_dialog.widgets.text import Const, Format
-
-from inclusive_dance_bot.bot.dialogs.admins.states import (
- AdminFeedbackSG,
- AdminMainMenuSG,
- AdminSubmenuSG,
- MailingsSG,
- ManageAdminSG,
- ReadUrlSG,
-)
-from inclusive_dance_bot.logic.user import MegaUser
-
-
-def when_(data: dict, widget: Any, dialog_manager: DialogManager) -> bool:
- user: MegaUser = data["middleware_data"]["user"]
- return user.is_superuser
-
-
-window = Window(
- Const("Меню администратора"),
- Format("*Здесь должна быть статистика по пользователям*"),
- Start(
- id="feedbacks",
- text=Const("Обратная связь от пользователей"),
- state=AdminFeedbackSG.items,
- ),
- Start(
- id="mailings",
- text=Const("Рассылки"),
- state=MailingsSG.menu,
- ),
- Start(
- id="manage_submenu_id",
- text=Const("Управление подменю"),
- state=AdminSubmenuSG.items,
- ),
- Start(
- id="manage_url_id",
- text=Const("Управление ссылками"),
- state=ReadUrlSG.items,
- ),
- Start(
- id="manage_admin_id",
- text=Const("Управление администраторами"),
- state=ManageAdminSG.items,
- when=when_,
- ),
- state=AdminMainMenuSG.menu,
-)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/add/__init__.py b/inclusive_dance_bot/bot/dialogs/admins/manage_admins/add/__init__.py
deleted file mode 100644
index 8109571..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/add/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from aiogram_dialog import Dialog
-
-from inclusive_dance_bot.bot.dialogs.admins.manage_admins.add import input_username
-
-dialog = Dialog(
- input_username.window,
-)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/delete/__init__.py b/inclusive_dance_bot/bot/dialogs/admins/manage_admins/delete/__init__.py
deleted file mode 100644
index 01689c2..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/delete/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from aiogram_dialog import Dialog
-
-from inclusive_dance_bot.bot.dialogs.admins.manage_admins.delete import confirm
-
-dialog = Dialog(confirm.window)
diff --git a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/read/__init__.py b/inclusive_dance_bot/bot/dialogs/admins/manage_admins/read/__init__.py
deleted file mode 100644
index b06f298..0000000
--- a/inclusive_dance_bot/bot/dialogs/admins/manage_admins/read/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from aiogram_dialog import Dialog
-
-from inclusive_dance_bot.bot.dialogs.admins.manage_admins.read import items
-
-dialog = Dialog(
- items.window,
-)
diff --git a/inclusive_dance_bot/bot/dialogs/commands.py b/inclusive_dance_bot/bot/dialogs/commands.py
deleted file mode 100644
index f478f46..0000000
--- a/inclusive_dance_bot/bot/dialogs/commands.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from aiogram.types import Message
-from aiogram_dialog import DialogManager, StartMode
-
-from inclusive_dance_bot.bot.dialogs.admins.states import AdminMainMenuSG
-from inclusive_dance_bot.bot.dialogs.messages import START_MESSAGE
-from inclusive_dance_bot.bot.dialogs.users.states import MainMenuSG as UserMainMenuSG
-from inclusive_dance_bot.bot.dialogs.users.states import RegistrationSG
-from inclusive_dance_bot.logic.user import MegaUser
-
-
-async def start_command(
- message: Message,
- dialog_manager: DialogManager,
- user: MegaUser,
-) -> None:
- if user.is_admin:
- await dialog_manager.start(AdminMainMenuSG.menu, mode=StartMode.RESET_STACK)
- elif user.is_anonymous:
- await message.answer(START_MESSAGE)
- await dialog_manager.start(
- RegistrationSG.input_name,
- mode=StartMode.RESET_STACK,
- )
- else:
- await dialog_manager.start(UserMainMenuSG.menu, mode=StartMode.RESET_STACK)
diff --git a/inclusive_dance_bot/bot/dialogs/users/main_menu/__init__.py b/inclusive_dance_bot/bot/dialogs/users/main_menu/__init__.py
deleted file mode 100644
index 0d00d77..0000000
--- a/inclusive_dance_bot/bot/dialogs/users/main_menu/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from aiogram_dialog import Dialog
-
-from inclusive_dance_bot.bot.dialogs.users.main_menu import menu
-from inclusive_dance_bot.bot.dialogs.users.states import MainMenuSG
-from inclusive_dance_bot.bot.dialogs.utils.submenu_window import SubmenuWindow
-
-dialog = Dialog(
- menu.window,
- SubmenuWindow(state=MainMenuSG.message),
-)
diff --git a/inclusive_dance_bot/bot/dialogs/utils/__init__.py b/inclusive_dance_bot/bot/dialogs/utils/__init__.py
deleted file mode 100644
index b0d7c68..0000000
--- a/inclusive_dance_bot/bot/dialogs/utils/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-__all__ = ["sync_scroll", "start_with_data"]
-
-from inclusive_dance_bot.bot.dialogs.utils.start_with_data import start_with_data
-from inclusive_dance_bot.bot.dialogs.utils.sync_scroll import sync_scroll
diff --git a/inclusive_dance_bot/bot/dialogs/utils/getters.py b/inclusive_dance_bot/bot/dialogs/utils/getters.py
deleted file mode 100644
index f380914..0000000
--- a/inclusive_dance_bot/bot/dialogs/utils/getters.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from typing import Any
-
-from aiogram_dialog import DialogManager
-
-from inclusive_dance_bot.logic.storage import Storage
-
-
-async def get_url_data(
- storage: Storage, dialog_manager: DialogManager, **kwargs: Any
-) -> dict[str, Any]:
- return {"url": await storage.get_url_by_slug(dialog_manager.start_data["url_slug"])}
-
-
-async def get_submenu_data(
- storage: Storage, dialog_manager: DialogManager, **kwargs: Any
-) -> dict[str, Any]:
- return {
- "submenu": await storage.get_submenu_by_id(
- dialog_manager.start_data["submenu_id"]
- )
- }
diff --git a/inclusive_dance_bot/bot/middlewares/user.py b/inclusive_dance_bot/bot/middlewares/user.py
deleted file mode 100644
index 50fb876..0000000
--- a/inclusive_dance_bot/bot/middlewares/user.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from collections.abc import Awaitable, Callable
-from typing import Any
-
-from aiogram import BaseMiddleware
-from aiogram.types import TelegramObject, Update
-from aiogram.types import User as AiogramUser
-
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.user import MegaUser
-
-
-class UserMiddleware(BaseMiddleware):
- _telegram_bot_admin_ids: list[int]
-
- def __init__(self, telegram_bot_admin_ids: list[int]) -> None:
- self._telegram_bot_admin_ids = telegram_bot_admin_ids
-
- async def __call__(
- self,
- handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
- event: Update, # type: ignore[override]
- data: dict[str, Any],
- ) -> Any:
- aiogram_user: AiogramUser = data["event_from_user"]
- uow: UnitOfWork = data["uow"]
- data["user"] = MegaUser(
- aiogram_user=aiogram_user,
- user=await uow.users.get_by_id(aiogram_user.id),
- superuser_ids=self._telegram_bot_admin_ids,
- )
- return await handler(event, data)
diff --git a/inclusive_dance_bot/db/repositories/feedback.py b/inclusive_dance_bot/db/repositories/feedback.py
deleted file mode 100644
index fe265aa..0000000
--- a/inclusive_dance_bot/db/repositories/feedback.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from typing import NoReturn
-
-from sqlalchemy import ScalarResult, insert
-from sqlalchemy.exc import DBAPIError, IntegrityError
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from inclusive_dance_bot.db.models import Feedback
-from inclusive_dance_bot.db.repositories.base import Repository
-from inclusive_dance_bot.dto import FeedbackDto
-from inclusive_dance_bot.enums import FeedbackType
-from inclusive_dance_bot.exceptions import InclusiveDanceError, InvalidUserIDError
-
-
-class FeedbackRepository(Repository[Feedback]):
- def __init__(self, session: AsyncSession) -> None:
- super().__init__(model=Feedback, session=session)
-
- async def create(
- self, *, user_id: int, type: FeedbackType, title: str, text: str
- ) -> FeedbackDto:
- query = (
- insert(Feedback)
- .values(
- user_id=user_id,
- type=type,
- title=title,
- text=text,
- )
- .returning(Feedback)
- )
- try:
- result: ScalarResult[Feedback] = await self._session.scalars(query)
- except IntegrityError as e:
- self._raise_error(e)
- else:
- await self._session.flush()
- return FeedbackDto.from_orm(result.one())
-
- def _raise_error(self, e: DBAPIError) -> NoReturn:
- constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
- if constraint == "fk__feedback__user_id__users":
- raise InvalidUserIDError from e
- raise InclusiveDanceError from e
diff --git a/inclusive_dance_bot/db/repositories/submenu.py b/inclusive_dance_bot/db/repositories/submenu.py
deleted file mode 100644
index 03ce2f2..0000000
--- a/inclusive_dance_bot/db/repositories/submenu.py
+++ /dev/null
@@ -1,82 +0,0 @@
-from typing import Any, NoReturn
-
-from sqlalchemy import ScalarResult, delete, desc, insert, select
-from sqlalchemy.exc import DBAPIError, IntegrityError, NoResultFound
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from inclusive_dance_bot.db.models import Submenu
-from inclusive_dance_bot.db.repositories.base import Repository
-from inclusive_dance_bot.dto import SubmenuDto
-from inclusive_dance_bot.enums import SubmenuType
-from inclusive_dance_bot.exceptions import (
- InclusiveDanceError,
- SubmenuAlreadyExistsError,
- SubmenuNotFoundError,
-)
-from inclusive_dance_bot.exceptions.base import EntityNotFoundError
-
-
-class SubmenuRepository(Repository[Submenu]):
- def __init__(self, session: AsyncSession) -> None:
- super().__init__(model=Submenu, session=session)
-
- async def create(
- self,
- type: SubmenuType,
- button_text: str,
- message: str,
- weight: int = 0,
- id: int | None = None,
- ) -> SubmenuDto:
- data = dict(type=type, button_text=button_text, message=message, weight=weight)
- if id is not None:
- data["id"] = id
- stmt = insert(Submenu).values(**data).returning(Submenu)
- try:
- result: ScalarResult[Submenu] = await self._session.scalars(stmt)
- except IntegrityError as e:
- self._raise_error(e)
- else:
- await self._session.flush()
- return SubmenuDto.from_orm(result.one())
-
- async def get_by_id(self, submenu_id: int) -> SubmenuDto:
- try:
- obj = await self._get_by_id(submenu_id)
- return SubmenuDto.from_orm(obj)
- except NoResultFound as e:
- raise SubmenuNotFoundError from e
-
- async def update_by_id(self, submenu_id: int, **kwargs: Any) -> SubmenuDto:
- try:
- submenu = await self._update(Submenu.id == submenu_id, **kwargs)
- except EntityNotFoundError as e:
- raise SubmenuNotFoundError from e
- except IntegrityError as e:
- self._raise_error(e)
- return SubmenuDto.from_orm(submenu)
-
- async def delete_by_id(self, submenu_id: int) -> None:
- stmt = delete(Submenu).where(Submenu.id == submenu_id)
- await self._session.execute(stmt)
-
- async def get_list(self) -> tuple[SubmenuDto, ...]:
- query = select(Submenu).order_by(desc(Submenu.weight), Submenu.id)
- objs = (await self._session.scalars(query)).all()
- return tuple(SubmenuDto.from_orm(obj) for obj in objs)
-
- async def get_list_by_type(self, submenu_type: SubmenuType) -> list[SubmenuDto]:
- stmt = (
- select(Submenu)
- .where(Submenu.type == submenu_type)
- .order_by(desc(Submenu.weight), Submenu.id)
- )
-
- objs = (await self._session.scalars(stmt)).all()
- return [SubmenuDto.from_orm(obj) for obj in objs]
-
- def _raise_error(self, e: DBAPIError) -> NoReturn:
- constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
- if constraint == "pk__submenu":
- raise SubmenuAlreadyExistsError from e
- raise InclusiveDanceError from e
diff --git a/inclusive_dance_bot/db/repositories/url.py b/inclusive_dance_bot/db/repositories/url.py
deleted file mode 100644
index 823b6c4..0000000
--- a/inclusive_dance_bot/db/repositories/url.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from typing import Any, NoReturn
-
-from sqlalchemy import ScalarResult, delete, insert, select
-from sqlalchemy.exc import DBAPIError, IntegrityError
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from inclusive_dance_bot.db.models import Url
-from inclusive_dance_bot.db.repositories.base import Repository
-from inclusive_dance_bot.dto import UrlDto
-from inclusive_dance_bot.exceptions import (
- InclusiveDanceError,
- UrlAlreadyExistsError,
- UrlSlugAlreadyExistsError,
-)
-from inclusive_dance_bot.exceptions.base import EntityNotFoundError
-from inclusive_dance_bot.exceptions.url import UrlNotFoundError
-
-
-class UrlRepository(Repository[Url]):
- def __init__(self, session: AsyncSession) -> None:
- super().__init__(model=Url, session=session)
-
- async def create(self, slug: str, value: str, id: int | None = None) -> UrlDto:
- data: dict[str, str | int] = dict(slug=slug, value=value)
- if id is not None:
- data["id"] = id
- stmt = insert(Url).values(**data).returning(Url)
- try:
- result: ScalarResult[Url] = await self._session.scalars(stmt)
- except IntegrityError as e:
- self._raise_error(e)
- else:
- await self._session.flush()
- return UrlDto.from_orm(result.one())
-
- async def update_by_slug(self, url_slug: str, **kwargs: Any) -> UrlDto:
- try:
- url = await self._update(Url.slug == url_slug, **kwargs)
- except EntityNotFoundError as e:
- raise UrlNotFoundError from e
- except IntegrityError as e:
- self._raise_error(e)
- return UrlDto.from_orm(url)
-
- async def delete_by_slug(self, url_slug: str) -> None:
- stmt = delete(Url).where(Url.slug == url_slug)
- await self._session.execute(stmt)
-
- async def get_list(self) -> tuple[UrlDto, ...]:
- stmt = select(Url).order_by(Url.slug)
- objs = (await self._session.scalars(stmt)).all()
- return tuple(UrlDto.from_orm(obj) for obj in objs)
-
- def _raise_error(self, e: DBAPIError) -> NoReturn:
- constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
- if constraint == "pk__url":
- raise UrlAlreadyExistsError from e
- if constraint == "uq__url__slug":
- raise UrlSlugAlreadyExistsError from e
- raise InclusiveDanceError from e
diff --git a/inclusive_dance_bot/db/repositories/user.py b/inclusive_dance_bot/db/repositories/user.py
deleted file mode 100644
index 3a7eb27..0000000
--- a/inclusive_dance_bot/db/repositories/user.py
+++ /dev/null
@@ -1,92 +0,0 @@
-from collections.abc import Sequence
-from typing import NoReturn
-
-from sqlalchemy import ScalarResult, insert, select
-from sqlalchemy.exc import DBAPIError, IntegrityError
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from inclusive_dance_bot.db.models import User, UserType, UserTypeUser
-from inclusive_dance_bot.db.repositories.base import Repository
-from inclusive_dance_bot.dto import ANONYMOUS_USER, UserDto, UserTypeDto
-from inclusive_dance_bot.exceptions import (
- EntityNotFoundError,
- InclusiveDanceError,
- UserAlreadyExistsError,
- UserNotFoundError,
-)
-
-
-class UserRepository(Repository[User]):
- def __init__(self, session: AsyncSession) -> None:
- super().__init__(model=User, session=session)
-
- async def create(
- self, *, user_id: int, username: str, name: str, region: str, phone_number: str
- ) -> UserDto:
- stmt = (
- insert(User)
- .values(
- id=user_id,
- username=username,
- name=name,
- region=region,
- phone_number=phone_number,
- )
- .returning(User)
- )
- try:
- result: ScalarResult[User] = await self._session.scalars(stmt)
- except IntegrityError as e:
- self._raise_error(e)
- else:
- await self._session.flush()
- return UserDto.from_orm(result.one())
-
- async def get_by_id(self, user_id: int) -> UserDto:
- obj = await self._get_by_id_or_none(obj_id=user_id)
- return UserDto.from_orm(obj) if obj else ANONYMOUS_USER
-
- async def get_admin_list(self) -> list[UserDto]:
- stmt = select(User).where(User.is_admin.is_(True))
- return [UserDto.from_orm(obj) for obj in await self._session.scalars(stmt)]
-
- async def add_to_admins(self, username: str) -> UserDto:
- try:
- user = await self._update(
- User.username == username, User.is_admin.is_(False), is_admin=True
- )
- return UserDto.from_orm(user)
- except EntityNotFoundError as e:
- raise UserNotFoundError from e
-
- async def delete_from_admins(self, user_id: int) -> UserDto:
- try:
- user = await self._update(
- User.id == user_id, User.is_admin.is_(True), is_admin=False
- )
- return UserDto.from_orm(user)
- except EntityNotFoundError as e:
- raise UserNotFoundError from e
-
- async def get_list_by_user_types(
- self, user_types: Sequence[UserTypeDto], ignore_admins: bool = True
- ) -> list[UserDto]:
- stmt = select(User)
- if len(user_types) != 0:
- stmt = (
- stmt.distinct(User.id)
- .join(UserTypeUser, User.id == UserTypeUser.user_id)
- .join(UserType, UserTypeUser.user_type_id == UserType.id)
- .where(UserType.name.in_([ut.name for ut in user_types]))
- )
-
- if ignore_admins:
- stmt = stmt.where(User.is_admin.is_(False))
- result = await self._session.scalars(stmt)
- return [UserDto.from_orm(obj) for obj in result]
-
- def _raise_error(self, e: DBAPIError) -> NoReturn:
- constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
- if constraint == "pk__users":
- raise UserAlreadyExistsError from e
- raise InclusiveDanceError from e
diff --git a/inclusive_dance_bot/db/repositories/user_type.py b/inclusive_dance_bot/db/repositories/user_type.py
deleted file mode 100644
index 3007363..0000000
--- a/inclusive_dance_bot/db/repositories/user_type.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from typing import NoReturn
-
-from sqlalchemy import ScalarResult, insert, select
-from sqlalchemy.exc import DBAPIError, IntegrityError
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from inclusive_dance_bot.db.models import UserType
-from inclusive_dance_bot.db.repositories.base import Repository
-from inclusive_dance_bot.dto import UserTypeDto
-from inclusive_dance_bot.exceptions import (
- InclusiveDanceError,
- UserTypeAlreadyExistsError,
-)
-
-
-class UserTypeRepository(Repository[UserType]):
- def __init__(self, session: AsyncSession) -> None:
- super().__init__(model=UserType, session=session)
-
- async def create(self, *, name: str, id: int | None = None) -> UserTypeDto:
- data: dict[str, str | int] = dict(name=name)
- if id is not None:
- data["id"] = id
- stmt = insert(UserType).values(**data).returning(UserType)
- try:
- result: ScalarResult[UserType] = await self._session.scalars(stmt)
- except IntegrityError as e:
- self._raise_error(e)
- else:
- await self._session.flush()
- return UserTypeDto.from_orm(result.one())
-
- async def get_list(self) -> tuple[UserTypeDto, ...]:
- stmt = select(UserType).order_by(UserType.id)
- return tuple(
- UserTypeDto.from_orm(obj)
- for obj in (await self._session.scalars(stmt)).all()
- )
-
- def _raise_error(self, e: DBAPIError) -> NoReturn:
- constraint = e.__cause__.__cause__.constraint_name # type: ignore[union-attr]
- if constraint in ("pk__user_type", "ix__user_type__name"):
- raise UserTypeAlreadyExistsError from e
- raise InclusiveDanceError from e
diff --git a/inclusive_dance_bot/db/uow/base.py b/inclusive_dance_bot/db/uow/base.py
deleted file mode 100644
index aa91713..0000000
--- a/inclusive_dance_bot/db/uow/base.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from abc import ABC, abstractmethod
-from types import TracebackType
-from typing import Self
-
-
-class UnitOfWorkBase(ABC):
- async def __aenter__(self) -> Self:
- return self
-
- async def __aexit__(
- self,
- exc_type: type[BaseException],
- exc_value: BaseException,
- traceback: TracebackType,
- ) -> None:
- await self.rollback()
-
- @abstractmethod
- async def commit(self) -> None:
- raise NotImplementedError()
-
- @abstractmethod
- async def rollback(self) -> None:
- raise NotImplementedError()
diff --git a/inclusive_dance_bot/db/uow/main.py b/inclusive_dance_bot/db/uow/main.py
deleted file mode 100644
index 102ed95..0000000
--- a/inclusive_dance_bot/db/uow/main.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import asyncio
-from types import TracebackType
-from typing import Self
-
-from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
-
-from inclusive_dance_bot.db.repositories.feedback import FeedbackRepository
-from inclusive_dance_bot.db.repositories.mailing import MailingRepository
-from inclusive_dance_bot.db.repositories.submenu import SubmenuRepository
-from inclusive_dance_bot.db.repositories.url import UrlRepository
-from inclusive_dance_bot.db.repositories.user import UserRepository
-from inclusive_dance_bot.db.repositories.user_type import UserTypeRepository
-from inclusive_dance_bot.db.repositories.user_type_user import UserTypeUserRepository
-from inclusive_dance_bot.db.uow.base import UnitOfWorkBase
-
-
-class UnitOfWork(UnitOfWorkBase):
- def __init__(self, sessionmaker: async_sessionmaker[AsyncSession]) -> None:
- self._sessionmaker = sessionmaker
-
- async def __aenter__(self) -> Self:
- self._session = self._sessionmaker()
- self.submenus = SubmenuRepository(self._session)
- self.feedbacks = FeedbackRepository(self._session)
- self.mailings = MailingRepository(self._session)
- self.urls = UrlRepository(self._session)
- self.users = UserRepository(self._session)
- self.user_types = UserTypeRepository(self._session)
- self.user_type_users = UserTypeUserRepository(self._session)
- return await super().__aenter__()
-
- async def __aexit__(
- self,
- exc_type: type[BaseException],
- exc_value: BaseException,
- traceback: TracebackType,
- ) -> None:
- await self._session.rollback()
- task = asyncio.create_task(self._session.close())
- await asyncio.shield(task)
-
- async def commit(self) -> None:
- await self._session.commit()
-
- async def rollback(self) -> None:
- await self._session.rollback()
diff --git a/inclusive_dance_bot/deps.py b/inclusive_dance_bot/deps.py
deleted file mode 100644
index 706d70b..0000000
--- a/inclusive_dance_bot/deps.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from argparse import Namespace
-from collections.abc import AsyncGenerator
-
-from aiogram import Bot
-from aiomisc_dependency import dependency
-from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
-
-from inclusive_dance_bot.bot.factory import get_bot
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.db.utils import create_engine, create_session_factory
-
-
-def config_deps(arguments: Namespace) -> None:
- @dependency
- async def bot() -> Bot:
- return get_bot(telegram_bot_token=arguments.telegram_bot_token)
-
- @dependency
- async def engine() -> AsyncGenerator[AsyncEngine, None]:
- engine = create_engine(
- connection_uri=arguments.pg_dsn,
- echo=arguments.debug,
- )
- yield engine
- await engine.dispose()
-
- @dependency
- async def session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
- return create_session_factory(engine=engine)
-
- @dependency
- async def uow(session_factory: async_sessionmaker[AsyncSession]) -> UnitOfWork:
- return UnitOfWork(sessionmaker=session_factory)
-
- return
diff --git a/inclusive_dance_bot/dto.py b/inclusive_dance_bot/dto.py
deleted file mode 100644
index 3a7db6c..0000000
--- a/inclusive_dance_bot/dto.py
+++ /dev/null
@@ -1,148 +0,0 @@
-from __future__ import annotations
-
-from collections.abc import Sequence
-from dataclasses import dataclass
-from datetime import datetime
-from typing import TYPE_CHECKING
-
-from inclusive_dance_bot.enums import FeedbackType, MailingStatus, SubmenuType
-
-if TYPE_CHECKING:
- from inclusive_dance_bot.db.models import (
- Feedback,
- Mailing,
- Submenu,
- Url,
- User,
- UserType,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UserDto:
- id: int
- name: str
- username: str
- region: str
- phone_number: str
- is_admin: bool
-
- @classmethod
- def from_orm(cls, obj: User) -> UserDto:
- return cls(
- id=obj.id,
- name=obj.name,
- username=obj.username,
- region=obj.name,
- phone_number=obj.phone_number,
- is_admin=obj.is_admin,
- )
-
-
-ANONYMOUS_USER = UserDto(
- id=0,
- name="Anonymous",
- region="Earth",
- phone_number="",
- is_admin=False,
- username="anonymous",
-)
-
-
-@dataclass(frozen=True, slots=True)
-class UserTypeDto:
- id: int
- name: str
-
- @classmethod
- def from_orm(cls, obj: UserType) -> UserTypeDto:
- return cls(id=obj.id, name=obj.name)
-
-
-@dataclass(frozen=True, slots=True)
-class SubmenuDto:
- id: int
- type: SubmenuType
- weight: int
- button_text: str
- message: str
-
- @classmethod
- def from_orm(cls, obj: Submenu) -> SubmenuDto:
- return cls(
- id=obj.id,
- type=obj.type,
- weight=obj.weight,
- button_text=obj.button_text,
- message=obj.message,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class UrlDto:
- id: int
- slug: str
- value: str
-
- @classmethod
- def from_orm(cls, obj: Url) -> UrlDto:
- return cls(id=obj.id, slug=obj.slug, value=obj.value)
-
- def __str__(self) -> str:
- return self.value
-
-
-@dataclass(frozen=True, slots=True)
-class FeedbackDto:
- id: int
- user_id: int
- type: FeedbackType
- title: str
- text: str
- is_viewed: bool
- viewed_at: datetime | None
- is_answered: bool
- answered_at: datetime | None
-
- @classmethod
- def from_orm(cls, obj: Feedback) -> FeedbackDto:
- return cls(
- id=obj.id,
- user_id=obj.user_id,
- type=obj.type,
- title=obj.title,
- text=obj.text,
- is_viewed=obj.is_viewed,
- viewed_at=obj.viewed_at,
- is_answered=obj.is_answered,
- answered_at=obj.answered_at,
- )
-
-
-@dataclass(frozen=True, slots=True)
-class MailingDto:
- created_at: datetime
- updated_at: datetime
- id: int
- scheduled_at: datetime | None
- sent_at: datetime | None
- cancelled_at: datetime | None
- status: MailingStatus
- title: str
- content: str
- user_types: Sequence[UserTypeDto]
-
- @classmethod
- def from_orm(cls, obj: Mailing) -> MailingDto:
- return cls(
- created_at=obj.created_at,
- updated_at=obj.updated_at,
- id=obj.id,
- scheduled_at=obj.scheduled_at,
- cancelled_at=obj.cancelled_at,
- status=obj.status,
- title=obj.title,
- content=obj.content,
- user_types=tuple(UserTypeDto.from_orm(ut) for ut in obj.user_types),
- sent_at=obj.sent_at,
- )
diff --git a/inclusive_dance_bot/exceptions/mailing.py b/inclusive_dance_bot/exceptions/mailing.py
deleted file mode 100644
index 1539855..0000000
--- a/inclusive_dance_bot/exceptions/mailing.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from inclusive_dance_bot.exceptions.base import EntityNotFoundError
-
-
-class MailingNotFoundError(EntityNotFoundError):
- pass
diff --git a/inclusive_dance_bot/init_data.py b/inclusive_dance_bot/init_data.py
deleted file mode 100644
index 83abda8..0000000
--- a/inclusive_dance_bot/init_data.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import asyncio
-import logging
-
-from inclusive_dance_bot.arguments import get_parser
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.db.utils import create_engine, create_session_factory
-from inclusive_dance_bot.enums import SubmenuType
-from inclusive_dance_bot.exceptions import (
- SubmenuAlreadyExistsError,
- UrlAlreadyExistsError,
- UserTypeAlreadyExistsError,
-)
-
-logging.basicConfig(
- level=logging.INFO,
- format="%(asctime)s | %(name)-12s | %(levelname)-8s | %(message)s",
- datefmt="%H:%M:%S %d.%m.%Y",
-)
-log = logging.getLogger(__name__)
-
-USER_TYPES = (
- (1, "Руководитель коллектива"),
- (2, "Хореограф / педагог"),
- (3, "Специалист социокультурной сферы"),
- (4, "Танцор с ОВЗ"),
- (5, "Родитель танцора"),
- (6, "Танцующий волонтер"),
- (7, "Волонтер-организатор"),
- (8, "Зритель"),
- (9, "Партнер / благотворитель"),
- (10, "Представитель СМИ"),
- (11, "Просто интересуюсь"),
-)
-URLS = (
- (1, "buy_form", "https://example.com"),
- (2, "google_doc", "https://example.com"),
- (3, "ticket_timepad", "https://example.com"),
- (4, "google_form_seminar", "https://example.com"),
- (5, "buy_form_url_1", "https://example.com"),
- (6, "buy_form_url_2", "https://example.com"),
- (7, "buy_form_url_3", "https://example.com"),
- (8, "buy_form_url_4", "https://example.com"),
- (9, "buy_form_url_5", "https://example.com"),
- (10, "buy_form_url_6", "https://example.com"),
- (11, "buy_form_url_7", "https://example.com"),
- (12, "buy_form_url_8", "https://example.com"),
-)
-SUBMENUS = (
- (1, SubmenuType.EVENT, "Клуб професионалов Inclusive Dance", "message"),
- (
- 2,
- SubmenuType.EVENT,
- "Фестиваль Inclusive Dance в Москве - октябрь 2023",
- "message",
- ),
- (3, SubmenuType.EVENT, "Социальное исследование", "message"),
- (4, SubmenuType.EDUCATION, "Клуб профессионалов Inclusive Dance", "message"),
- (5, SubmenuType.EDUCATION, "Семинары по инклюзивному танцу", "message"),
- (6, SubmenuType.EDUCATION, "Онлайн-курсы по инклюзивному танцу", "message"),
- (7, SubmenuType.ENROLL, 'Студия м. "Авимоторная" (Москва)', "message"),
- (8, SubmenuType.ENROLL, 'Студия м. "Войковская" (Москва)', "message"),
- (9, SubmenuType.CHARITY, "Сделать пожертвование", "message"),
- (10, SubmenuType.CHARITY, "Стать волонтером проекта", "message"),
- (11, SubmenuType.CHARITY, "Стать партнером проекта", "message"),
- (12, SubmenuType.CHARITY, "Рассказать о проекте", "message"),
- (13, SubmenuType.CHARITY, "Организовать показ фильма", "message"),
- (14, SubmenuType.INFORMATION, "Что такое инклюзивный танец?", "message"),
- (15, SubmenuType.INFORMATION, "О проекте Inclusive Dance?", "message"),
- (16, SubmenuType.INFORMATION, "Новости проекта", "message"),
- (
- 17,
- SubmenuType.INFORMATION,
- 'Документальный фильм "Танцевать под дождем"',
- "Здесь должна быть очень важная информация о фильме"
- ' и ссылка',
- ),
- (18, SubmenuType.INFORMATION, "Ссылки на наши ресурсы", "message"),
- (19, SubmenuType.INFORMATION, "Задать вопрос команде", "message"),
- (20, SubmenuType.OTHER, "Стать волонтером", "message"),
- (21, SubmenuType.OTHER, "Купить билет", "message"),
-)
-
-
-async def init_data(uow: UnitOfWork) -> None:
- log.info("Run init data")
- try:
- async with uow:
- for url in URLS:
- await uow.urls.create(slug=url[1], value=url[2], id=url[0])
- await uow.commit()
- log.info("Urls successfully initialized")
- except UrlAlreadyExistsError:
- log.warning("Urls already in database")
-
- try:
- async with uow:
- for user_type in USER_TYPES:
- await uow.user_types.create(id=user_type[0], name=user_type[1])
- await uow.commit()
- log.info("UserTypes successfully initialized")
- except UserTypeAlreadyExistsError:
- log.warning("UserTypes already in database")
-
- try:
- async with uow:
- for submenu in SUBMENUS:
- await uow.submenus.create(
- id=submenu[0],
- type=submenu[1],
- button_text=submenu[2],
- message=submenu[3],
- )
- await uow.commit()
- log.info("Submenu successfully initialized")
- except SubmenuAlreadyExistsError:
- log.warning("Submenu already in database")
- log.info("Finish init data")
-
-
-def main() -> None:
- parser = get_parser()
- arguments = parser.parse_args()
- engine = create_engine(connection_uri=arguments.pg_dsn)
- session_factory = create_session_factory(engine=engine)
- uow = UnitOfWork(sessionmaker=session_factory)
- asyncio.run(init_data(uow=uow))
-
-
-if __name__ == "__main__":
- main()
diff --git a/inclusive_dance_bot/logic/feedback.py b/inclusive_dance_bot/logic/feedback.py
deleted file mode 100644
index 6ca7db3..0000000
--- a/inclusive_dance_bot/logic/feedback.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.enums import FeedbackType
-from inclusive_dance_bot.exceptions.base import InclusiveDanceError
-
-
-async def create_feedback(
- uow: UnitOfWork, user_id: int, type: FeedbackType, title: str, text: str
-) -> None:
- """Создает новую обратную связь от пользователя"""
- try:
- await uow.feedbacks.create(
- user_id=user_id,
- type=type,
- title=title,
- text=text,
- )
- await uow.commit()
- except InclusiveDanceError as e:
- await uow.rollback()
- raise e
diff --git a/inclusive_dance_bot/logic/storage.py b/inclusive_dance_bot/logic/storage.py
deleted file mode 100644
index b293068..0000000
--- a/inclusive_dance_bot/logic/storage.py
+++ /dev/null
@@ -1,86 +0,0 @@
-from collections.abc import Iterator, MutableMapping
-from typing import Any
-
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.dto import SubmenuDto, UrlDto, UserTypeDto
-from inclusive_dance_bot.enums import StorageType
-
-
-class CacheStorage(MutableMapping):
- def __init__(self) -> None:
- self.__storage: dict[str, Any] = {}
-
- def __getitem__(self, key: str) -> Any:
- return self.__storage[key]
-
- def __setitem__(self, key: str, value: Any) -> None:
- self.__storage[key] = value
-
- def __delitem__(self, key: str) -> None:
- del self.__storage[key]
-
- def __len__(self) -> int:
- return len(self.__storage)
-
- def __iter__(self) -> Iterator:
- return iter(self.__storage)
-
- def __repr__(self) -> str:
- return repr(self.__storage)
-
- def clear(self) -> None:
- self.__storage.clear()
-
-
-class Storage:
- _uow: UnitOfWork
- _cache: CacheStorage
-
- def __init__(self, uow: UnitOfWork) -> None:
- self._uow = uow
- self._cache = CacheStorage()
-
- async def get_urls(self) -> dict[str, UrlDto]:
- if StorageType.URL not in self._cache:
- self._cache[StorageType.URL] = {
- url.slug: url for url in await self._uow.urls.get_list()
- }
- return self._cache[StorageType.URL]
-
- async def get_url_by_slug(self, slug: str) -> UrlDto:
- urls = await self.get_urls()
- return urls[slug]
-
- async def get_user_types(self) -> dict[int, UserTypeDto]:
- if StorageType.USER_TYPE not in self._cache:
- self._cache[StorageType.USER_TYPE] = {
- ut.id: ut for ut in await self._uow.user_types.get_list()
- }
- return self._cache[StorageType.USER_TYPE]
-
- async def get_submenus(self) -> dict[int, SubmenuDto]:
- if StorageType.SUBMENU not in self._cache:
- self._cache[StorageType.SUBMENU] = {
- e.id: e for e in await self._uow.submenus.get_list()
- }
- return self._cache[StorageType.SUBMENU]
-
- async def get_submenu_by_id(self, submenu_id: int) -> SubmenuDto:
- submenus = await self.get_submenus()
- return submenus[submenu_id]
-
- async def refresh_all(self) -> None:
- self._cache.clear()
- await self.get_urls()
- await self.get_submenus()
- await self.get_user_types()
-
- async def refresh_urls(self) -> None:
- if StorageType.URL in self._cache:
- del self._cache[StorageType.URL]
- await self.get_urls()
-
- async def refresh_submenus(self) -> None:
- if StorageType.SUBMENU in self._cache:
- del self._cache[StorageType.SUBMENU]
- await self.get_submenus()
diff --git a/inclusive_dance_bot/logic/url.py b/inclusive_dance_bot/logic/url.py
deleted file mode 100644
index 54b483e..0000000
--- a/inclusive_dance_bot/logic/url.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from typing import Any
-
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.dto import UrlDto
-from inclusive_dance_bot.exceptions.base import InclusiveDanceError
-from inclusive_dance_bot.exceptions.url import UrlSlugAlreadyExistsError
-from inclusive_dance_bot.logic.storage import Storage
-
-
-async def create_url(
- uow: UnitOfWork, storage: Storage, slug: str, value: str
-) -> UrlDto:
- """Создает новую ссылку"""
- try:
- url = await uow.urls.create(slug=slug, value=value)
- except UrlSlugAlreadyExistsError as e:
- await uow.rollback()
- raise e
- await uow.commit()
- await storage.refresh_urls()
- return url
-
-
-async def update_url_by_slug(
- uow: UnitOfWork,
- storage: Storage,
- url_slug: str,
- **kwargs: Any,
-) -> UrlDto:
- try:
- url = await uow.urls.update_by_slug(url_slug=url_slug, **kwargs)
- except InclusiveDanceError as e:
- await uow.rollback()
- raise e
- await uow.commit()
- await storage.refresh_urls()
- return url
-
-
-async def delete_url_by_slug(uow: UnitOfWork, storage: Storage, url_slug: str) -> None:
- await uow.urls.delete_by_slug(url_slug=url_slug)
- await uow.commit()
- await storage.refresh_urls()
diff --git a/inclusive_dance_bot/logic/user.py b/inclusive_dance_bot/logic/user.py
deleted file mode 100644
index da92f98..0000000
--- a/inclusive_dance_bot/logic/user.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from collections.abc import Iterable
-
-from aiogram.types import User as AiogramUser
-
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.dto import UserDto
-from inclusive_dance_bot.exceptions.base import InclusiveDanceError
-
-
-class MegaUser:
- def __init__(
- self, aiogram_user: AiogramUser, user: UserDto, superuser_ids: list[int]
- ) -> None:
- self._aiogram_user = aiogram_user
- self._user = user
- self._superuser_ids = superuser_ids
-
- def __repr__(self) -> str:
- return f"MegaUser(user={self._user},auser={self._aiogram_user})"
-
- @property
- def is_superuser(self) -> bool:
- return self._aiogram_user.id in self._superuser_ids
-
- @property
- def is_admin(self) -> bool:
- return self._user.is_admin or self.is_superuser
-
- @property
- def is_anonymous(self) -> bool:
- return self._user.id == 0
-
-
-async def create_user(
- uow: UnitOfWork,
- user_id: int,
- username: str,
- name: str,
- region: str,
- phone_number: str,
- user_type_ids: Iterable[int],
-) -> None:
- """Создает нового пользователя"""
- try:
- await uow.users.create(
- user_id=user_id,
- username=username,
- name=name,
- region=region,
- phone_number=phone_number,
- )
- for user_type_id in user_type_ids:
- await uow.user_type_users.create(user_id=user_id, user_type_id=user_type_id)
- await uow.commit()
- except InclusiveDanceError as e:
- await uow.rollback()
- raise e
-
-
-async def add_user_to_admins(uow: UnitOfWork, username: str) -> None:
- try:
- await uow.users.add_to_admins(username=username)
- await uow.commit()
- except InclusiveDanceError as e:
- await uow.rollback()
- raise e
-
-
-async def delete_from_admins(uow: UnitOfWork, user_id: int) -> None:
- try:
- await uow.users.delete_from_admins(user_id=user_id)
- await uow.commit()
- except InclusiveDanceError as e:
- await uow.rollback()
- raise e
diff --git a/inclusive_dance_bot/services/bot.py b/inclusive_dance_bot/services/bot.py
deleted file mode 100644
index 3ae470a..0000000
--- a/inclusive_dance_bot/services/bot.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import logging
-
-from aiogram import Bot, Dispatcher
-from aiogram.fsm.storage.memory import SimpleEventIsolation
-from aiogram_dialog import setup_dialogs
-from aiomisc import Service
-
-from inclusive_dance_bot.bot.dialogs import register_dialogs
-from inclusive_dance_bot.bot.factory import get_storage
-from inclusive_dance_bot.bot.middlewares.storage import StorageMiddleware
-from inclusive_dance_bot.bot.middlewares.uow import UowMiddleware
-from inclusive_dance_bot.bot.middlewares.user import UserMiddleware
-from inclusive_dance_bot.bot.ui_commands import set_ui_commands
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-
-log = logging.getLogger(__name__)
-
-
-class AiogramBotService(Service):
- __required__ = ("debug", "redis_dsn", "telegram_bot_admin_ids")
-
- __dependencies__ = ("uow", "bot")
-
- debug: bool
- redis_dsn: str
- telegram_bot_admin_ids: list[int]
-
- uow: UnitOfWork
- bot: Bot
-
- async def start(self) -> None:
- log.info("Initialize bot")
- await set_ui_commands(self.bot)
- await self.bot.delete_webhook(drop_pending_updates=True)
-
- storage = Storage(uow=self.uow)
-
- async with self.uow:
- await storage.refresh_all()
-
- dp = Dispatcher(
- storage=get_storage(
- debug=self.debug,
- redis_dsn=self.redis_dsn,
- ),
- events_isolation=SimpleEventIsolation(),
- )
- dp.update.outer_middleware(UowMiddleware(uow=self.uow))
- dp.update.outer_middleware(StorageMiddleware(storage=storage))
- dp.update.outer_middleware(
- UserMiddleware(telegram_bot_admin_ids=self.telegram_bot_admin_ids)
- )
- register_dialogs(dp)
- setup_dialogs(dp)
-
- self.start_event.set()
- log.info("Start polling")
- await dp.start_polling(self.bot)
diff --git a/inclusive_dance_bot/services/periodic.py b/inclusive_dance_bot/services/periodic.py
deleted file mode 100644
index 4b18f5c..0000000
--- a/inclusive_dance_bot/services/periodic.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from typing import Any
-
-from aiogram import Bot
-from aiomisc.service.periodic import PeriodicService
-
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.mailing import send_mailings
-
-
-class PeriodicMailingService(PeriodicService):
- __required__ = ("gap",)
- __dependencies__ = ("bot", "uow")
-
- bot: Bot
- uow: UnitOfWork
- gap: int
-
- async def callback(self) -> Any:
- async with self.uow:
- await send_mailings(uow=self.uow, bot=self.bot, gap=self.gap)
diff --git a/inutils/init_data.py b/inutils/init_data.py
new file mode 100644
index 0000000..8488662
--- /dev/null
+++ b/inutils/init_data.py
@@ -0,0 +1,139 @@
+import argparse
+import asyncio
+import logging
+from pathlib import Path
+from typing import Any
+
+import yaml
+from aiomisc_log import LogFormat, LogLevel, basic_config
+from configargparse import ArgumentParser
+from pydantic import BaseModel, HttpUrl, ValidationError, field_validator
+
+from idb.db.uow import UnitOfWork
+from idb.db.utils import create_engine, create_session_factory
+from idb.generals.enums import SubmenuType
+from idb.utils.urls import check_slug
+
+log = logging.getLogger(__name__)
+
+
+class UserTypeSchema(BaseModel):
+ id: int
+ name: str
+
+
+class UrlSchema(BaseModel):
+ id: int
+ slug: str
+ value: HttpUrl
+
+ @field_validator("slug")
+ @classmethod
+ def check_slug(cls, v: str) -> str:
+ if not check_slug(v):
+ raise ValueError
+
+ return v
+
+
+class SubmenuSchema(BaseModel):
+ id: int
+ type: SubmenuType
+ weight: int
+ button_text: str
+ message: str
+
+
+async def write_urls(uow: UnitOfWork, urls: list[dict[str, Any]]) -> None:
+ for u in urls:
+ try:
+ url = UrlSchema.model_validate(u)
+ except ValidationError:
+ log.warning("Incorrect url data: %s", u)
+ else:
+ await uow.urls.upsert(**url.model_dump(mode="json"))
+
+
+async def write_user_types(uow: UnitOfWork, user_types: list[dict[str, Any]]) -> None:
+ for ut in user_types:
+ try:
+ user_type = UserTypeSchema.model_validate(ut)
+ except ValidationError:
+ log.warning("Incorrect user type data: %s", ut)
+ else:
+ await uow.user_types.upsert(**user_type.model_dump(mode="json"))
+
+
+async def write_submenus(uow: UnitOfWork, submenus: list[dict[str, Any]]) -> None:
+ for s in submenus:
+ try:
+ submenu = SubmenuSchema.model_validate(s)
+ except ValidationError:
+ log.warning("Incorrect submenu data: %s", s)
+ else:
+ await uow.submenus.upsert(**submenu.model_dump(mode="json"))
+
+
+async def write_data(uow: UnitOfWork, filename: Path) -> None:
+ log.info("Run init data")
+ data = read_data(filename=filename)
+ user_types = data.get("user_types")
+ urls = data.get("urls")
+ submenus = data.get("submenus")
+ if urls:
+ await write_urls(uow=uow, urls=urls)
+
+ if user_types:
+ await write_user_types(uow=uow, user_types=user_types)
+
+ if submenus:
+ await write_submenus(uow=uow, submenus=submenus)
+
+ await uow.commit()
+
+
+def get_parser() -> ArgumentParser:
+ parser = ArgumentParser(
+ allow_abbrev=False,
+ auto_env_var_prefix="APP_",
+ description="Inclusive Dance Bot",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+
+ group = parser.add_argument_group("Logging options")
+ group.add_argument("--log-level", choices=LogLevel.choices(), default=LogLevel.info)
+ group.add_argument(
+ "--log-format", choices=LogFormat.choices(), default=LogFormat.color
+ )
+
+ group = parser.add_argument_group("PostgreSQL options")
+ group.add_argument("--pg-dsn", type=str, required=True)
+
+ group = parser.add_argument_group("Load data params")
+ group.add_argument("--init-data-path", type=Path, required=True)
+
+ return parser
+
+
+def read_data(filename: Path) -> dict[str, Any]:
+ with open(filename) as f:
+ return yaml.safe_load(f)
+
+
+def main() -> None:
+ parser = get_parser()
+ arguments = parser.parse_args()
+ if not arguments.init_data_path.exists():
+ raise FileNotFoundError
+ basic_config(
+ level=arguments.log_level,
+ log_format=arguments.log_format,
+ )
+ engine = create_engine(connection_uri=arguments.pg_dsn)
+ session_factory = create_session_factory(engine=engine)
+ uow = UnitOfWork(sessionmaker=session_factory)
+ asyncio.run(write_data(uow=uow, filename=arguments.init_data_path))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/inutils/init_data.yaml b/inutils/init_data.yaml
new file mode 100644
index 0000000..2017b1d
--- /dev/null
+++ b/inutils/init_data.yaml
@@ -0,0 +1,180 @@
+user_types:
+ - id: 1
+ name: Руководитель коллектива
+
+ - id: 2
+ name: Хореограф / педагог
+
+ - id: 3
+ name: Специалист социокультурной сферы
+
+ - id: 4
+ name: Танцор с ОВЗ
+
+ - id: 5
+ name: Родитель танцора
+
+ - id: 6
+ name: Танцующий волонтер
+
+ - id: 7
+ name: Волонтер-организатор
+
+ - id: 8
+ name: Зритель
+
+ - id: 9
+ name: Партнер / благотворитель
+
+ - id: 10
+ name: Представитель СМИ
+
+ - id: 11
+ name: Просто интересуюсь
+
+urls:
+ - id: 1
+ slug: club_payment
+ description: Оплата за участие в Клубе
+ value: https://c.cloudpayments.ru/payments/b37773a15dc544f996ce2de65265d75d
+
+ - id: 2
+ slug: festival_press_release
+ description: Пресс-релиз фестиваля (ссылка на гугл-док)
+ value:
+
+ - id: 3
+ slug: buy_ticket
+ description: Купить билет (ссылка на таймпед)
+ value:
+
+ - id: 4
+ slug: seminar_registration
+ description: Зарегистрироваться на семинар (ссылка на гугл форму)
+ value: https://docs.google.com/forms/d/e/1FAIpQLSfqtSmNF2pR0h1gj2_8NO9tAqTBLTAPvR6HgXFiYdgu-wBdSw/viewform
+
+ - id: 5
+ slug: seminar_payment
+ description: Оплата за участие в семинаре
+ value: https://c.cloudpayments.ru/payments/75b956c67f444832b6c1d48c815874fd
+
+ - id: 6
+ slug: studio_registration
+ description: Запись в студию
+ value: https://docs.google.com/forms/d/e/1FAIpQLSf9gn-X7ERtEcChdmqNFqbIPxD7dWwh_RR_iL97ySZe6bBJDQ/viewform
+
+ - id: 7
+ slug: volunteer_form
+ description: Анкета волонтера
+ value: https://docs.google.com/forms/d/e/1FAIpQLScFw1yGSI2ZF4jiUiO6xeTYY2gn8gwlnDXrI9M1DHctZ81-UQ/viewform
+
+ - id: 8
+ slug: charity_payment
+ description: Форма пожертвования
+ value: https://inclusive-dance.ru/pomoch-nam/?to=one
+
+ - id: 9
+ slug: about_us
+ description: О нас
+ value: https://inclusive-dance.ru/o-fonde/
+
+ - id: 10
+ slug: blog
+ description: Блог
+ value: https://inclusive-dance.ru/news/
+
+ - id: 11
+ slug: dancing_film_site
+ description: Сайт фильма "Танцевать под дождем"
+ value: https://www.id-film.ru/
+
+ - id: 12
+ slug: dancing_film_watch
+ description: Посмотреть фильм "Танцевать под дождем"
+ value: https://wink.ru/movies/tantsevat-pod-dozhdem-year-2023
+
+submenus:
+ - id: 1
+ type: CHARITY
+ button_text: Сделать пожертвование
+ weight: 100
+ message: |
+ Сделать пожертвование
+
+ - id: 2
+ type: CHARITY
+ button_text: Стать волонтером проекта
+ weight: 90
+ message: |
+ Вы увлекаетесь танцами и хотите заниматься в инклюзивном танцевальном коллективе? А может вы желаете помогать на социальных мероприятиях?
+
+ Тогда заполните анкету, чтобы мы могли связаться с вами и обсудить возможные варианты вашей помощи.
+
+ Заполнить анкету волонтера
+
+ - id: 3
+ type: CHARITY
+ button_text: Стать партнером проекта
+ weight: 80
+ message: |
+ Ссылка на сайт для партнеров
+
+ - id: 4
+ type: CHARITY
+ button_text: Рассказать о проекте
+ weight: 70
+ message: |
+ Ссылка на форму
+
+ - id: 5
+ type: CHARITY
+ button_text: Организовать показ фильма
+ weight: 60
+ message: |
+ Ссылка на форму
+
+ - id: 6
+ type: EVENT
+ button_text: Клуб профессионалов Inclusive dancing_film_site
+ weight: 100
+ message: |
+ Оплата за участие в Клубе
+
+ - id: 7
+ type: EVENT
+ button_text: Фестиваль Inclusive Dance в Москве - октябрь 2023
+ weight: 90
+ message: |
+ Пресс-релиз фестиваля
+
+ Купить билет
+
+ - id: 8
+ type: EVENT
+ button_text: Социальное исследование
+ weight: 80
+ message: |
+ Запись на исследование
+
+ - id: 9
+ type: INFORMATION
+ button_text: Что такое инклюзивный танец?
+ weight: 100
+ message: |
+ Короткое сообщение от бота об инклюзивном танце
+
+ - id: 10
+ type: INFORMATION
+ button_text: О проекте Inclusive Dance
+ weight: 90
+ message: |
+ Подробнее о проекте Inclusive Dance можно узнать на официальном сайте
+
+ - id: 11
+ type: INFORMATION
+ button_text: Новости проекта
+ weight: 80
+ message: |-
+ Все новости на нашем официальном сайте
+
+ Хотите оперативно получ
diff --git a/poetry.lock b/poetry.lock
index 28e6d7d..b3e771c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -387,46 +387,6 @@ test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)",
toml = ["tomli (>=1.1.0)"]
yaml = ["PyYAML"]
-[[package]]
-name = "black"
-version = "23.10.1"
-description = "The uncompromising code formatter."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"},
- {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"},
- {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"},
- {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"},
- {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"},
- {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"},
- {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"},
- {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"},
- {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"},
- {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"},
- {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"},
- {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"},
- {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"},
- {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"},
- {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"},
- {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"},
- {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"},
- {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"},
-]
-
-[package.dependencies]
-click = ">=8.0.0"
-mypy-extensions = ">=0.4.3"
-packaging = ">=22.0"
-pathspec = ">=0.9.0"
-platformdirs = ">=2"
-
-[package.extras]
-colorama = ["colorama (>=0.4.3)"]
-d = ["aiohttp (>=3.7.4)"]
-jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
-uvloop = ["uvloop (>=0.15.2)"]
-
[[package]]
name = "cachetools"
version = "5.3.2"
@@ -559,20 +519,6 @@ files = [
{file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"},
]
-[[package]]
-name = "click"
-version = "8.1.7"
-description = "Composable command line interface toolkit"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
- {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
-]
-
-[package.dependencies]
-colorama = {version = "*", markers = "platform_system == \"Windows\""}
-
[[package]]
name = "colorama"
version = "0.4.6"
@@ -1284,17 +1230,6 @@ files = [
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
-[[package]]
-name = "pathspec"
-version = "0.11.2"
-description = "Utility library for gitignore style pattern matching of file paths."
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
- {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
-]
-
[[package]]
name = "pbr"
version = "5.11.1"
@@ -1849,6 +1784,32 @@ pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
+[[package]]
+name = "ruff"
+version = "0.1.11"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"},
+ {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"},
+ {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"},
+ {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"},
+ {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"},
+ {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"},
+ {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"},
+ {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"},
+ {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"},
+ {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"},
+ {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"},
+ {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"},
+ {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"},
+ {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"},
+ {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"},
+ {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"},
+ {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"},
+]
+
[[package]]
name = "setuptools"
version = "68.2.2"
@@ -2026,6 +1987,28 @@ files = [
{file = "types_pytz-2023.3.1.1-py3-none-any.whl", hash = "sha256:1999a123a3dc0e39a2ef6d19f3f8584211de9e6a77fe7a0259f04a524e90a5cf"},
]
+[[package]]
+name = "types-pyyaml"
+version = "6.0.12.12"
+description = "Typing stubs for PyYAML"
+optional = false
+python-versions = "*"
+files = [
+ {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"},
+ {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"},
+]
+
+[[package]]
+name = "types-ujson"
+version = "5.9.0.0"
+description = "Typing stubs for ujson"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "types-ujson-5.9.0.0.tar.gz", hash = "sha256:7e7042454dc7cd7f31b09c420d7caf36b93d30bdf4b8db93791bd0561713d017"},
+ {file = "types_ujson-5.9.0.0-py3-none-any.whl", hash = "sha256:f274fa604ed6317effcd1c424ef4cf292c3b0689cb118fb3180689d40ed1f4ed"},
+]
+
[[package]]
name = "typing-extensions"
version = "4.7.1"
@@ -2037,6 +2020,80 @@ files = [
{file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
]
+[[package]]
+name = "ujson"
+version = "5.9.0"
+description = "Ultra fast JSON encoder and decoder for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "ujson-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab71bf27b002eaf7d047c54a68e60230fbd5cd9da60de7ca0aa87d0bccead8fa"},
+ {file = "ujson-5.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a365eac66f5aa7a7fdf57e5066ada6226700884fc7dce2ba5483538bc16c8c5"},
+ {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e015122b337858dba5a3dc3533af2a8fc0410ee9e2374092f6a5b88b182e9fcc"},
+ {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779a2a88c53039bebfbccca934430dabb5c62cc179e09a9c27a322023f363e0d"},
+ {file = "ujson-5.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10ca3c41e80509fd9805f7c149068fa8dbee18872bbdc03d7cca928926a358d5"},
+ {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a566e465cb2fcfdf040c2447b7dd9718799d0d90134b37a20dff1e27c0e9096"},
+ {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f833c529e922577226a05bc25b6a8b3eb6c4fb155b72dd88d33de99d53113124"},
+ {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b68a0caab33f359b4cbbc10065c88e3758c9f73a11a65a91f024b2e7a1257106"},
+ {file = "ujson-5.9.0-cp310-cp310-win32.whl", hash = "sha256:7cc7e605d2aa6ae6b7321c3ae250d2e050f06082e71ab1a4200b4ae64d25863c"},
+ {file = "ujson-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6d3f10eb8ccba4316a6b5465b705ed70a06011c6f82418b59278fbc919bef6f"},
+ {file = "ujson-5.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b23bbb46334ce51ddb5dded60c662fbf7bb74a37b8f87221c5b0fec1ec6454b"},
+ {file = "ujson-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6974b3a7c17bbf829e6c3bfdc5823c67922e44ff169851a755eab79a3dd31ec0"},
+ {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5964ea916edfe24af1f4cc68488448fbb1ec27a3ddcddc2b236da575c12c8ae"},
+ {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ba7cac47dd65ff88571eceeff48bf30ed5eb9c67b34b88cb22869b7aa19600d"},
+ {file = "ujson-5.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bbd91a151a8f3358c29355a491e915eb203f607267a25e6ab10531b3b157c5e"},
+ {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:829a69d451a49c0de14a9fecb2a2d544a9b2c884c2b542adb243b683a6f15908"},
+ {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a807ae73c46ad5db161a7e883eec0fbe1bebc6a54890152ccc63072c4884823b"},
+ {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8fc2aa18b13d97b3c8ccecdf1a3c405f411a6e96adeee94233058c44ff92617d"},
+ {file = "ujson-5.9.0-cp311-cp311-win32.whl", hash = "sha256:70e06849dfeb2548be48fdd3ceb53300640bc8100c379d6e19d78045e9c26120"},
+ {file = "ujson-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7309d063cd392811acc49b5016728a5e1b46ab9907d321ebbe1c2156bc3c0b99"},
+ {file = "ujson-5.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:20509a8c9f775b3a511e308bbe0b72897ba6b800767a7c90c5cca59d20d7c42c"},
+ {file = "ujson-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b28407cfe315bd1b34f1ebe65d3bd735d6b36d409b334100be8cdffae2177b2f"},
+ {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d302bd17989b6bd90d49bade66943c78f9e3670407dbc53ebcf61271cadc399"},
+ {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f21315f51e0db8ee245e33a649dd2d9dce0594522de6f278d62f15f998e050e"},
+ {file = "ujson-5.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5635b78b636a54a86fdbf6f027e461aa6c6b948363bdf8d4fbb56a42b7388320"},
+ {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82b5a56609f1235d72835ee109163c7041b30920d70fe7dac9176c64df87c164"},
+ {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5ca35f484622fd208f55041b042d9d94f3b2c9c5add4e9af5ee9946d2d30db01"},
+ {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:829b824953ebad76d46e4ae709e940bb229e8999e40881338b3cc94c771b876c"},
+ {file = "ujson-5.9.0-cp312-cp312-win32.whl", hash = "sha256:25fa46e4ff0a2deecbcf7100af3a5d70090b461906f2299506485ff31d9ec437"},
+ {file = "ujson-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:60718f1720a61560618eff3b56fd517d107518d3c0160ca7a5a66ac949c6cf1c"},
+ {file = "ujson-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d581db9db9e41d8ea0b2705c90518ba623cbdc74f8d644d7eb0d107be0d85d9c"},
+ {file = "ujson-5.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ff741a5b4be2d08fceaab681c9d4bc89abf3c9db600ab435e20b9b6d4dfef12e"},
+ {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdcb02cabcb1e44381221840a7af04433c1dc3297af76fde924a50c3054c708c"},
+ {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e208d3bf02c6963e6ef7324dadf1d73239fb7008491fdf523208f60be6437402"},
+ {file = "ujson-5.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4b3917296630a075e04d3d07601ce2a176479c23af838b6cf90a2d6b39b0d95"},
+ {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0c4d6adb2c7bb9eb7c71ad6f6f612e13b264942e841f8cc3314a21a289a76c4e"},
+ {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0b159efece9ab5c01f70b9d10bbb77241ce111a45bc8d21a44c219a2aec8ddfd"},
+ {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0cb4a7814940ddd6619bdce6be637a4b37a8c4760de9373bac54bb7b229698b"},
+ {file = "ujson-5.9.0-cp38-cp38-win32.whl", hash = "sha256:dc80f0f5abf33bd7099f7ac94ab1206730a3c0a2d17549911ed2cb6b7aa36d2d"},
+ {file = "ujson-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:506a45e5fcbb2d46f1a51fead991c39529fc3737c0f5d47c9b4a1d762578fc30"},
+ {file = "ujson-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0fd2eba664a22447102062814bd13e63c6130540222c0aa620701dd01f4be81"},
+ {file = "ujson-5.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bdf7fc21a03bafe4ba208dafa84ae38e04e5d36c0e1c746726edf5392e9f9f36"},
+ {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f909bc08ce01f122fd9c24bc6f9876aa087188dfaf3c4116fe6e4daf7e194f"},
+ {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd4ea86c2afd41429751d22a3ccd03311c067bd6aeee2d054f83f97e41e11d8f"},
+ {file = "ujson-5.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:63fb2e6599d96fdffdb553af0ed3f76b85fda63281063f1cb5b1141a6fcd0617"},
+ {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:32bba5870c8fa2a97f4a68f6401038d3f1922e66c34280d710af00b14a3ca562"},
+ {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:37ef92e42535a81bf72179d0e252c9af42a4ed966dc6be6967ebfb929a87bc60"},
+ {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f69f16b8f1c69da00e38dc5f2d08a86b0e781d0ad3e4cc6a13ea033a439c4844"},
+ {file = "ujson-5.9.0-cp39-cp39-win32.whl", hash = "sha256:3382a3ce0ccc0558b1c1668950008cece9bf463ebb17463ebf6a8bfc060dae34"},
+ {file = "ujson-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:6adef377ed583477cf005b58c3025051b5faa6b8cc25876e594afbb772578f21"},
+ {file = "ujson-5.9.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ffdfebd819f492e48e4f31c97cb593b9c1a8251933d8f8972e81697f00326ff1"},
+ {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4eec2ddc046360d087cf35659c7ba0cbd101f32035e19047013162274e71fcf"},
+ {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbb90aa5c23cb3d4b803c12aa220d26778c31b6e4b7a13a1f49971f6c7d088e"},
+ {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0823cb70866f0d6a4ad48d998dd338dce7314598721bc1b7986d054d782dfd"},
+ {file = "ujson-5.9.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4e35d7885ed612feb6b3dd1b7de28e89baaba4011ecdf995e88be9ac614765e9"},
+ {file = "ujson-5.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b048aa93eace8571eedbd67b3766623e7f0acbf08ee291bef7d8106210432427"},
+ {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:323279e68c195110ef85cbe5edce885219e3d4a48705448720ad925d88c9f851"},
+ {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ac92d86ff34296f881e12aa955f7014d276895e0e4e868ba7fddebbde38e378"},
+ {file = "ujson-5.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6eecbd09b316cea1fd929b1e25f70382917542ab11b692cb46ec9b0a26c7427f"},
+ {file = "ujson-5.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:473fb8dff1d58f49912323d7cb0859df5585cfc932e4b9c053bf8cf7f2d7c5c4"},
+ {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f91719c6abafe429c1a144cfe27883eace9fb1c09a9c5ef1bcb3ae80a3076a4e"},
+ {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b1c0991c4fe256f5fdb19758f7eac7f47caac29a6c57d0de16a19048eb86bad"},
+ {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a8ea0f55a1396708e564595aaa6696c0d8af532340f477162ff6927ecc46e21"},
+ {file = "ujson-5.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:07e0cfdde5fd91f54cd2d7ffb3482c8ff1bf558abf32a8b953a5d169575ae1cd"},
+ {file = "ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532"},
+]
+
[[package]]
name = "uvloop"
version = "0.19.0"
@@ -2191,4 +2248,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
-content-hash = "4ed699093b3c5b50b7cacf5273f4f7cde9c4efb677d5470a47e2ccfc19b72b3f"
+content-hash = "9f8daf9792cda9be7e4181c4adf9c4801c0f9dc61a79e9e7621b0c21b37204a2"
diff --git a/pyproject.toml b/pyproject.toml
index 37907be..65cd90e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,11 +24,11 @@ aiomisc = {extras = ["uvloop"], version = "^17.3.23"}
aiomisc-dependency = "^0.1.20"
aiomisc-pytest = "^1.1.1"
configargparse = "^1.7"
+ujson = "^5.9.0"
[tool.poetry.group.dev.dependencies]
mypy = "^1.5.1"
pre-commit = "^3.4.0"
-black = "^23.9.1"
flake8 = "^6.1.0"
bandit = "^1.7.5"
types-pytz = "^2023.3.1.1"
@@ -37,6 +37,9 @@ pytest = "^7.4.2"
pytest-asyncio = "^0.21.1"
pytest-cov = "^4.1.0"
pytest-subtests = "^0.11.0"
+types-pyyaml = "^6.0.12.12"
+types-ujson = "^5.9.0.0"
+ruff = "^0.1.11"
[tool.poetry.scripts]
bot = "inclusive_dance_bot.__main__:main"
@@ -58,7 +61,7 @@ addopts = "-p no:cacheprovider"
target-version = ["py311"]
[tool.isort]
-known_local_folder = ["inclusive_dance_bot", "tests"]
+known_local_folder = ["idb", "tests"]
py_version = "311"
profile = "black"
@@ -92,4 +95,44 @@ ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "configargparse.*"
-ignore_missing_imports = true
\ No newline at end of file
+ignore_missing_imports = true
+
+[tool.ruff]
+exclude = [
+ ".git",
+ ".mypy_cache",
+ ".ruff_cache",
+ ".venv",
+]
+
+line-length = 88
+indent-width = 4
+
+target-version = "py311"
+
+[tool.ruff.lint]
+select = [
+ "BLE",
+ "C90",
+ "E",
+ "F",
+ "G",
+ "I",
+ "ICN",
+ "ISC",
+ "PLE",
+ "Q",
+ "RUF006",
+ "RUF100",
+ "T10",
+ "T20",
+ "TID",
+ "UP",
+ "W",
+]
+ignore = ["ISC001"]
+fixable = ["ALL"]
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index 0a9407d..bd1a258 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,110 +1,4 @@
-import asyncio
-import os
-from pathlib import Path
-from types import SimpleNamespace
-
-import pytest
-from alembic.config import Config as AlembicConfig
-from sqlalchemy import text
-from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
-
-from inclusive_dance_bot.db.base import Base
-from inclusive_dance_bot.db.utils import (
- create_engine,
- create_session_factory,
- make_alembic_config,
+pytest_plugins = (
+ "tests.plugins.cache",
+ "tests.plugins.database",
)
-from tests.factories import FACTORIES
-from tests.utils import prepare_new_database, run_async_migrations
-
-TABLES_FOR_TRUNCATE = (
- "submenu",
- "url",
- "users",
- "user_type",
- "user_type_user",
- "feedback",
-)
-
-PROJECT_PATH = Path(__file__).parent.parent.resolve()
-
-
-@pytest.fixture(scope="session")
-def event_loop():
- policy = asyncio.get_event_loop_policy()
- loop = policy.new_event_loop()
- yield loop
- loop.close()
-
-
-@pytest.fixture(scope="session")
-def db_name():
- default = "test_pgdb"
- return os.getenv("APP_PG_DB_NAME", default)
-
-
-@pytest.fixture(scope="session")
-def pg_dsn(localhost, db_name: str) -> str:
- default = f"postgresql+asyncpg://pguser:pguser@{localhost}:5432/{db_name}"
- return os.getenv("APP_PG_DSN", default)
-
-
-@pytest.fixture(scope="session")
-def base_pg_dsn(localhost) -> str:
- default = f"postgresql+asyncpg://pguser:pguser@{localhost}:5432/postgres"
- return os.getenv("APP_BASE_PG_DSN", default)
-
-
-@pytest.fixture(scope="session")
-def alembic_config(pg_dsn: str) -> AlembicConfig:
- cmd_options = SimpleNamespace(
- config="alembic.ini",
- name="alembic",
- pg_url=pg_dsn,
- raiseerr=False,
- x=None,
- )
- return make_alembic_config(cmd_options)
-
-
-@pytest.fixture(scope="session")
-async def async_engine(
- alembic_config: AlembicConfig,
- base_pg_dsn: str,
- pg_dsn: str,
- db_name: str,
-) -> AsyncEngine:
- await prepare_new_database(base_pg_dsn=base_pg_dsn, db_name=db_name)
- await run_async_migrations(alembic_config, Base.metadata, "head")
- engine = create_engine(pg_dsn)
- yield engine
- async with engine.begin() as conn:
- await conn.run_sync(Base.metadata.drop_all)
- await engine.dispose()
-
-
-@pytest.fixture(scope="session")
-def sessionmaker(async_engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
- yield create_session_factory(engine=async_engine)
-
-
-@pytest.fixture(autouse=True)
-async def session(
- sessionmaker: async_sessionmaker[AsyncSession], async_engine: AsyncEngine
-) -> AsyncSession:
- try:
- session: AsyncSession = sessionmaker()
- for factory in FACTORIES:
- factory.__async_session__ = session
- yield session
- finally:
- await session.close()
- await _clear_db(async_engine)
-
-
-async def _clear_db(engine: AsyncEngine) -> None:
- tables = ", ".join(TABLES_FOR_TRUNCATE)
- sql = f"TRUNCATE TABLE {tables} CASCADE"
- async with engine.connect() as conn:
- await conn.execute(text(sql))
- await conn.commit()
diff --git a/tests/factories.py b/tests/factories.py
index 5f39ab7..ab5d493 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -1,11 +1,35 @@
+from typing import TypedDict
+
from polyfactory import Use
+from polyfactory.factories import TypedDictFactory
from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory
from polyfactory.value_generators.constrained_strings import (
handle_constrained_string_or_bytes,
)
-from inclusive_dance_bot.db.models import Feedback, Submenu, Url, User, UserType
-from inclusive_dance_bot.enums import SubmenuType
+from idb.db.models import Feedback, Submenu, Url, User, UserType
+from idb.generals.enums import SubmenuType
+
+
+def _phone_number():
+ return handle_constrained_string_or_bytes(
+ random=SQLAlchemyFactory.__random__,
+ t_type=str,
+ min_length=8,
+ max_length=16,
+ )
+
+
+class Profile(TypedDict):
+ name: str
+ region: str
+ phone_number: str
+
+
+class ProfileFactory(TypedDictFactory[Profile]):
+ __model__ = Profile
+
+ phone_number = _phone_number
class UserFactory(SQLAlchemyFactory[User]):
@@ -13,9 +37,7 @@ class UserFactory(SQLAlchemyFactory[User]):
__set_foreign_keys__ = False
__set_relationships__ = True
- phone_number = lambda: handle_constrained_string_or_bytes(
- random=SQLAlchemyFactory.__random__, t_type=str, min_length=8, max_length=16
- )
+ profile = ProfileFactory
class UserTypeFactory(SQLAlchemyFactory[UserType]):
diff --git a/tests/plugins/__init__.py b/tests/plugins/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/plugins/cache.py b/tests/plugins/cache.py
new file mode 100644
index 0000000..82e99c7
--- /dev/null
+++ b/tests/plugins/cache.py
@@ -0,0 +1,27 @@
+import os
+
+import pytest
+
+from idb.utils.cache import KeyBuilder, MemoryCache, RedisCache
+
+
+@pytest.fixture
+async def memory_cache() -> MemoryCache:
+ return MemoryCache()
+
+
+@pytest.fixture(scope="session")
+def redis_dsn(localhost) -> str:
+ default = f"redis://{localhost}:6379/0"
+ return os.getenv("APP_REDIS_DSN", default)
+
+
+@pytest.fixture
+async def redis_cache(redis_dsn: str) -> RedisCache:
+ cache = RedisCache.from_url(url=redis_dsn, key_builder=KeyBuilder())
+ try:
+ await cache.flushall()
+ yield cache
+ finally:
+ await cache.flushall()
+ await cache.close()
diff --git a/tests/plugins/database.py b/tests/plugins/database.py
new file mode 100644
index 0000000..a34f78f
--- /dev/null
+++ b/tests/plugins/database.py
@@ -0,0 +1,117 @@
+import asyncio
+import os
+from pathlib import Path
+from types import SimpleNamespace
+
+import pytest
+from alembic.config import Config as AlembicConfig
+from sqlalchemy import text
+from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
+
+from idb.db.base import Base
+from idb.db.uow import UnitOfWork, uow_context
+from idb.db.utils import (
+ create_engine,
+ create_session_factory,
+ make_alembic_config,
+)
+from tests.factories import FACTORIES
+from tests.utils import prepare_new_database, run_async_migrations
+
+TABLES_FOR_TRUNCATE = (
+ "submenu",
+ "url",
+ "users",
+ "user_type",
+ "user_type_user",
+ "feedback",
+)
+
+PROJECT_PATH = Path(__file__).parent.parent.resolve()
+
+
+@pytest.fixture(scope="session")
+def event_loop():
+ policy = asyncio.get_event_loop_policy()
+ loop = policy.new_event_loop()
+ yield loop
+ loop.close()
+
+
+@pytest.fixture(scope="session")
+def db_name():
+ default = "test_pgdb"
+ return os.getenv("APP_PG_DB_NAME", default)
+
+
+@pytest.fixture(scope="session")
+def pg_dsn(localhost, db_name: str) -> str:
+ default = f"postgresql+asyncpg://pguser:pguser@{localhost}:5432/{db_name}"
+ return os.getenv("APP_PG_DSN", default)
+
+
+@pytest.fixture(scope="session")
+def base_pg_dsn(localhost) -> str:
+ default = f"postgresql+asyncpg://pguser:pguser@{localhost}:5432/postgres"
+ return os.getenv("APP_BASE_PG_DSN", default)
+
+
+@pytest.fixture(scope="session")
+def alembic_config(pg_dsn: str) -> AlembicConfig:
+ cmd_options = SimpleNamespace(
+ config="alembic.ini",
+ name="alembic",
+ pg_url=pg_dsn,
+ raiseerr=False,
+ x=None,
+ )
+ return make_alembic_config(cmd_options)
+
+
+@pytest.fixture(scope="session")
+async def async_engine(
+ alembic_config: AlembicConfig,
+ base_pg_dsn: str,
+ pg_dsn: str,
+ db_name: str,
+) -> AsyncEngine:
+ await prepare_new_database(base_pg_dsn=base_pg_dsn, db_name=db_name)
+ await run_async_migrations(alembic_config, Base.metadata, "head")
+ engine = create_engine(pg_dsn)
+ yield engine
+ async with engine.begin() as conn:
+ await conn.run_sync(Base.metadata.drop_all)
+ await engine.dispose()
+
+
+@pytest.fixture(scope="session")
+def sessionmaker(async_engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
+ yield create_session_factory(engine=async_engine)
+
+
+@pytest.fixture(autouse=True)
+async def session(
+ sessionmaker: async_sessionmaker[AsyncSession], async_engine: AsyncEngine
+) -> AsyncSession:
+ try:
+ session: AsyncSession = sessionmaker()
+ for factory in FACTORIES:
+ factory.__async_session__ = session
+ yield session
+ finally:
+ await session.close()
+ await _clear_db(async_engine)
+
+
+async def _clear_db(engine: AsyncEngine) -> None:
+ tables = ", ".join(TABLES_FOR_TRUNCATE)
+ sql = f"TRUNCATE TABLE {tables} CASCADE"
+ async with engine.connect() as conn:
+ await conn.execute(text(sql))
+ await conn.commit()
+
+
+@pytest.fixture
+async def uow(sessionmaker: async_sessionmaker[AsyncSession]) -> UnitOfWork:
+ async with uow_context(sessionmaker=sessionmaker) as uow:
+ yield uow
diff --git a/tests/test_database/test_migrations_up_to_date.py b/tests/test_database/test_migrations_up_to_date.py
index 1e20091..b35270a 100644
--- a/tests/test_database/test_migrations_up_to_date.py
+++ b/tests/test_database/test_migrations_up_to_date.py
@@ -1,6 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncEngine
-from inclusive_dance_bot.db.models import Base
+from idb.db.models import Base
from tests.utils import get_diff_db_metadata
diff --git a/tests/test_logic/__init__.py b/tests/test_logic/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_logic/test_feedback/__init__.py b/tests/test_logic/test_feedback/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_unit/test_services/test_feedback/test_create_feedback.py b/tests/test_logic/test_feedback/test_create_feedback.py
similarity index 78%
rename from tests/test_unit/test_services/test_feedback/test_create_feedback.py
rename to tests/test_logic/test_feedback/test_create_feedback.py
index 5b59ef1..2574b36 100644
--- a/tests/test_unit/test_services/test_feedback/test_create_feedback.py
+++ b/tests/test_logic/test_feedback/test_create_feedback.py
@@ -2,11 +2,11 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
-from inclusive_dance_bot.db.models import Feedback
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.enums import FeedbackType
-from inclusive_dance_bot.exceptions import InvalidUserIDError
-from inclusive_dance_bot.logic.feedback import create_feedback
+from idb.db.models import Feedback
+from idb.db.uow import UnitOfWork
+from idb.exceptions import InvalidUserIDError
+from idb.generals.enums import FeedbackType
+from idb.logic.feedback import create_feedback
from tests.factories import UserFactory
diff --git a/tests/test_logic/test_mailing/__init__.py b/tests/test_logic/test_mailing/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_logic/test_mailing/test_process_new_mailing.py b/tests/test_logic/test_mailing/test_process_new_mailing.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_logic/test_mailing/test_save_mailing.py b/tests/test_logic/test_mailing/test_save_mailing.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_logic/test_mailing/test_send_mailngs.py b/tests/test_logic/test_mailing/test_send_mailngs.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_logic/test_url/__init__.py b/tests/test_logic/test_url/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_logic/test_url/test_create_url.py b/tests/test_logic/test_url/test_create_url.py
new file mode 100644
index 0000000..6fa7b9d
--- /dev/null
+++ b/tests/test_logic/test_url/test_create_url.py
@@ -0,0 +1,33 @@
+import pytest
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from idb.db.models import Url as UrlDb
+from idb.db.uow import UnitOfWork
+from idb.exceptions.url import UrlSlugAlreadyExistsError
+from idb.generals.models.url import Url
+from idb.logic.url import create_url
+from idb.utils.cache import MemoryCache
+from tests.factories import UrlFactory
+
+
+async def test_create_successful(
+ uow: UnitOfWork,
+ memory_cache: MemoryCache,
+ session: AsyncSession,
+) -> None:
+ slug = "new_url_slug"
+ value = "https://example.com"
+
+ url = await create_url(uow=uow, cache=memory_cache, slug=slug, value=value)
+
+ loaded_url = await session.get(UrlDb, url.id)
+ assert Url.model_validate(loaded_url) == url
+
+
+async def test_error_url_slug_already_exists(
+ uow: UnitOfWork,
+ memory_cache: MemoryCache,
+) -> None:
+ url = await UrlFactory.create_async()
+ with pytest.raises(UrlSlugAlreadyExistsError):
+ await create_url(uow=uow, cache=memory_cache, slug=url.slug, value="somevalue")
diff --git a/tests/test_logic/test_url/test_delete_url_by_slug.py b/tests/test_logic/test_url/test_delete_url_by_slug.py
new file mode 100644
index 0000000..0a7f7f5
--- /dev/null
+++ b/tests/test_logic/test_url/test_delete_url_by_slug.py
@@ -0,0 +1,20 @@
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from idb.db.models import Url
+from idb.db.uow import UnitOfWork
+from idb.logic.url import delete_url_by_slug
+from idb.utils.cache import MemoryCache
+from tests.factories import UrlFactory
+
+
+async def test_delete_successful(
+ uow: UnitOfWork, memory_cache: MemoryCache, session: AsyncSession
+) -> None:
+ await UrlFactory.create_async(id=1, slug="new_url")
+
+ await delete_url_by_slug(uow=uow, cache=memory_cache, url_slug="new_url")
+ assert await session.get(Url, 1) is None
+
+
+async def test_delete_unknown_slug(uow: UnitOfWork, memory_cache: MemoryCache) -> None:
+ await delete_url_by_slug(uow=uow, cache=memory_cache, url_slug="unknown")
diff --git a/tests/test_logic/test_url/test_update_url_by_slug.py b/tests/test_logic/test_url/test_update_url_by_slug.py
new file mode 100644
index 0000000..a2d49e1
--- /dev/null
+++ b/tests/test_logic/test_url/test_update_url_by_slug.py
@@ -0,0 +1,47 @@
+import pytest
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from idb.db.uow import UnitOfWork
+from idb.exceptions.url import (
+ UrlNotFoundError,
+ UrlSlugAlreadyExistsError,
+)
+from idb.generals.models.url import Url
+from idb.logic.url import update_url_by_slug
+from idb.utils.cache import MemoryCache
+from tests.factories import UrlFactory
+
+
+async def test_update_successful(
+ uow: UnitOfWork, memory_cache: MemoryCache, session: AsyncSession
+) -> None:
+ url = await UrlFactory.create_async(slug="slug")
+ updated_url = await update_url_by_slug(
+ uow=uow, cache=memory_cache, url_slug="slug", value="https://vk.com"
+ )
+
+ await session.refresh(url)
+ assert url.value == "https://vk.com"
+
+ assert updated_url == Url.model_validate(url)
+
+
+async def test_error_url_not_found(uow: UnitOfWork, memory_cache: MemoryCache) -> None:
+ with pytest.raises(UrlNotFoundError):
+ await update_url_by_slug(
+ uow=uow, cache=memory_cache, url_slug="unknown", value=""
+ )
+
+
+async def test_error_url_slug_already_exists(
+ uow: UnitOfWork, memory_cache: MemoryCache
+) -> None:
+ await UrlFactory.create_async(slug="first_url")
+ await UrlFactory.create_async(slug="second_url")
+ with pytest.raises(UrlSlugAlreadyExistsError):
+ await update_url_by_slug(
+ uow=uow,
+ cache=memory_cache,
+ url_slug="second_url",
+ slug="first_url",
+ )
diff --git a/tests/test_logic/test_user/__init__.py b/tests/test_logic/test_user/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_logic/test_user/test_create_user.py b/tests/test_logic/test_user/test_create_user.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_unit/conftest.py b/tests/test_unit/conftest.py
deleted file mode 100644
index ce0ae9a..0000000
--- a/tests/test_unit/conftest.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import pytest
-from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
-
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-
-
-@pytest.fixture
-async def uow(sessionmaker: async_sessionmaker[AsyncSession]) -> UnitOfWork:
- uow = UnitOfWork(sessionmaker=sessionmaker)
- async with uow:
- yield uow
-
-
-@pytest.fixture
-async def storage(uow: UnitOfWork) -> Storage:
- return Storage(uow=uow)
diff --git a/tests/test_unit/test_cache/__init__.py b/tests/test_unit/test_cache/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_unit/test_cache/test_memory.py b/tests/test_unit/test_cache/test_memory.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_unit/test_cache/test_redis.py b/tests/test_unit/test_cache/test_redis.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_unit/test_init_data.py b/tests/test_unit/test_init_data.py
index 1c7d1c7..c880f53 100644
--- a/tests/test_unit/test_init_data.py
+++ b/tests/test_unit/test_init_data.py
@@ -1,12 +1,48 @@
-from sqlalchemy.ext.asyncio import AsyncSession
+from idb.db.uow import UnitOfWork
+from idb.generals.enums import SubmenuType
+from inutils.init_data import write_submenus, write_urls, write_user_types
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.init_data import SUBMENUS, URLS, USER_TYPES, init_data
+# async def test_init_data_from_scratch(uow: UnitOfWork) -> None:
+# await write_data(uow=uow, filename=PROJECT_FOLDER / "inutils/init_data.yaml")
+# assert len(await uow.urls.list()) > 0
+# assert len(await uow.submenus.list()) > 0
+# assert len(await uow.user_types.list()) > 0
-async def test_init_data_from_scratch(uow: UnitOfWork, session: AsyncSession) -> None:
- await init_data(uow=uow)
- assert len(await uow.urls.get_list()) == len(URLS)
- assert len(await uow.submenus.get_list()) == len(SUBMENUS)
- assert len(await uow.user_types.get_list()) == len(USER_TYPES)
+async def test_write_urls_from_scratch(uow: UnitOfWork) -> None:
+ urls = [
+ {
+ "id": 1,
+ "slug": "some_url",
+ "value": "https://example.com",
+ }
+ ]
+
+ await write_urls(uow, urls)
+ assert len(await uow.urls.list()) == 1
+
+
+async def test_write_submenus_from_scratch(uow: UnitOfWork) -> None:
+ submenus = [
+ {
+ "id": 1,
+ "type": SubmenuType.CHARITY,
+ "button_text": "something",
+ "message": "Hello",
+ "weight": 1.0,
+ }
+ ]
+ await write_submenus(uow, submenus)
+ assert len(await uow.submenus.list()) == 1
+
+
+async def test_write_user_types_from_scratch(uow: UnitOfWork) -> None:
+ user_types = [
+ {
+ "id": 1,
+ "name": "Somebody",
+ }
+ ]
+ await write_user_types(uow, user_types)
+ assert len(await uow.user_types.list()) == 1
diff --git a/tests/test_unit/test_repositories/conftest.py b/tests/test_unit/test_repositories/conftest.py
index 83794cc..852af51 100644
--- a/tests/test_unit/test_repositories/conftest.py
+++ b/tests/test_unit/test_repositories/conftest.py
@@ -1,12 +1,12 @@
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
-from inclusive_dance_bot.db.repositories.feedback import FeedbackRepository
-from inclusive_dance_bot.db.repositories.submenu import SubmenuRepository
-from inclusive_dance_bot.db.repositories.url import UrlRepository
-from inclusive_dance_bot.db.repositories.user import UserRepository
-from inclusive_dance_bot.db.repositories.user_type import UserTypeRepository
-from inclusive_dance_bot.db.repositories.user_type_user import UserTypeUserRepository
+from idb.db.repositories.feedback import FeedbackRepository
+from idb.db.repositories.submenu import SubmenuRepository
+from idb.db.repositories.url import UrlRepository
+from idb.db.repositories.user import UserRepository
+from idb.db.repositories.user_type import UserTypeRepository
+from idb.db.repositories.user_type_user import UserTypeUserRepository
@pytest.fixture
diff --git a/tests/test_unit/test_repositories/test_feedback.py b/tests/test_unit/test_repositories/test_feedback.py
index bc1459c..2b0f1dc 100644
--- a/tests/test_unit/test_repositories/test_feedback.py
+++ b/tests/test_unit/test_repositories/test_feedback.py
@@ -1,11 +1,11 @@
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
-from inclusive_dance_bot.db.models import Feedback
-from inclusive_dance_bot.db.repositories.feedback import FeedbackRepository
-from inclusive_dance_bot.dto import FeedbackDto
-from inclusive_dance_bot.enums import FeedbackType
-from inclusive_dance_bot.exceptions import InvalidUserIDError
+from idb.db.models import Feedback as FeedbackDb
+from idb.db.repositories.feedback import FeedbackRepository
+from idb.exceptions import InvalidUserIDError
+from idb.generals.enums import FeedbackType
+from idb.generals.models.feedback import Feedback
from tests.factories import UserFactory
@@ -17,8 +17,8 @@ async def test_create(feedback_repo: FeedbackRepository, session: AsyncSession)
title="Some question",
text="Very important question",
)
- loaded_feedback = await session.get(Feedback, feedback.id)
- assert feedback == FeedbackDto.from_orm(loaded_feedback)
+ loaded_feedback = await session.get(FeedbackDb, feedback.id)
+ assert feedback == Feedback.model_validate(loaded_feedback)
async def test_invalid_user_id(feedback_repo: FeedbackRepository) -> None:
diff --git a/tests/test_unit/test_repositories/test_submenu.py b/tests/test_unit/test_repositories/test_submenu.py
index bcbaf18..426a3fe 100644
--- a/tests/test_unit/test_repositories/test_submenu.py
+++ b/tests/test_unit/test_repositories/test_submenu.py
@@ -1,14 +1,14 @@
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
-from inclusive_dance_bot.db.models import Submenu
-from inclusive_dance_bot.db.repositories.submenu import SubmenuRepository
-from inclusive_dance_bot.dto import SubmenuDto
-from inclusive_dance_bot.enums import SubmenuType
-from inclusive_dance_bot.exceptions import (
+from idb.db.models import Submenu as SubmenuDb
+from idb.db.repositories.submenu import SubmenuRepository
+from idb.exceptions import (
SubmenuAlreadyExistsError,
SubmenuNotFoundError,
)
+from idb.generals.enums import SubmenuType
+from idb.generals.models.submenu import Submenu
from tests.factories import SubmenuFactory
@@ -19,8 +19,8 @@ async def test_create(submenu_repo: SubmenuRepository, session: AsyncSession) ->
message="Very long message message",
)
await session.commit()
- saved_submenu = await session.get(Submenu, submenu.id)
- assert submenu == SubmenuDto.from_orm(saved_submenu)
+ saved_submenu = await session.get(SubmenuDb, submenu.id)
+ assert submenu == Submenu.model_validate(saved_submenu)
async def test_invalid_double_create(
@@ -45,7 +45,7 @@ async def test_invalid_double_create(
async def test_get_by_id(submenu_repo: SubmenuRepository) -> None:
submenu = await SubmenuFactory.create_async()
loaded_submenu = await submenu_repo.get_by_id(submenu.id)
- assert SubmenuDto.from_orm(submenu) == loaded_submenu
+ assert Submenu.model_validate(submenu) == loaded_submenu
async def test_not_found_by_id(submenu_repo: SubmenuRepository) -> None:
@@ -54,14 +54,15 @@ async def test_not_found_by_id(submenu_repo: SubmenuRepository) -> None:
async def test_get_list_emtpy(submenu_repo: SubmenuRepository) -> None:
- empty = await submenu_repo.get_list()
+ empty = await submenu_repo.list()
assert empty == tuple()
async def test_get_list(submenu_repo: SubmenuRepository) -> None:
submenus = await SubmenuFactory.create_batch_async(size=5)
- loaded_submenus = await submenu_repo.get_list()
- assert {SubmenuDto.from_orm(s) for s in submenus} == set(loaded_submenus)
+ submenus.sort(key=lambda x: (-x.weight, x.id))
+ loaded_submenus = await submenu_repo.list()
+ assert loaded_submenus == tuple(Submenu.model_validate(s) for s in submenus)
async def test_get_list_order_by_weight(submenu_repo: SubmenuRepository) -> None:
@@ -69,22 +70,22 @@ async def test_get_list_order_by_weight(submenu_repo: SubmenuRepository) -> None
first = await SubmenuFactory.create_async(weight=100)
second = await SubmenuFactory.create_async(weight=30)
- loaded_submenus = await submenu_repo.get_list()
+ loaded_submenus = await submenu_repo.list()
assert loaded_submenus == tuple(
- SubmenuDto.from_orm(e) for e in (first, second, third)
+ Submenu.model_validate(e) for e in (first, second, third)
)
-async def test_get_list_by_type(submenu_repo: SubmenuRepository) -> None:
+async def test_get_list_by_correct_type(submenu_repo: SubmenuRepository) -> None:
target_type = await SubmenuFactory.create_async(type=SubmenuType.CHARITY)
charities = await submenu_repo.get_list_by_type(SubmenuType.CHARITY)
- assert set(charities) == {SubmenuDto.from_orm(target_type)}
+ assert charities == [Submenu.model_validate(target_type)]
-async def test_get_list_by_type(submenu_repo: SubmenuRepository) -> None:
+async def test_get_list_by_incorrect_type(submenu_repo: SubmenuRepository) -> None:
await SubmenuFactory.create_async(type=SubmenuType.EDUCATION)
charities = await submenu_repo.get_list_by_type(SubmenuType.CHARITY)
- assert set(charities) == set()
+ assert charities == list()
diff --git a/tests/test_unit/test_repositories/test_url.py b/tests/test_unit/test_repositories/test_url.py
index ac4ef82..76f48ec 100644
--- a/tests/test_unit/test_repositories/test_url.py
+++ b/tests/test_unit/test_repositories/test_url.py
@@ -1,13 +1,13 @@
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
-from inclusive_dance_bot.db.models import Url
-from inclusive_dance_bot.db.repositories.url import UrlRepository
-from inclusive_dance_bot.dto import UrlDto
-from inclusive_dance_bot.exceptions import (
+from idb.db.models import Url as UrlDb
+from idb.db.repositories.url import UrlRepository
+from idb.exceptions import (
UrlAlreadyExistsError,
UrlSlugAlreadyExistsError,
)
+from idb.generals.models.url import Url
from tests.factories import UrlFactory
@@ -17,8 +17,8 @@ async def test_create_url(url_repo: UrlRepository, session: AsyncSession) -> Non
value="https://yandex.ru",
)
await session.commit()
- saved_url = await session.get(Url, url.id)
- assert url == UrlDto.from_orm(saved_url)
+ saved_url = await session.get(UrlDb, url.id)
+ assert url == Url.model_validate(saved_url)
async def test_invalid_double_create_by_id(
@@ -53,12 +53,12 @@ async def test_invalid_double_create_by_slug(
async def test_get_list_empty(url_repo: UrlRepository) -> None:
- loaded_urls = await url_repo.get_list()
+ loaded_urls = await url_repo.list()
assert loaded_urls == tuple()
async def test_get_list(url_repo: UrlRepository) -> None:
urls = await UrlFactory.create_batch_async(size=5)
-
- loaded_urls = await url_repo.get_list()
- assert set(loaded_urls) == {UrlDto.from_orm(u) for u in urls}
+ urls.sort(key=lambda x: x.id)
+ loaded_urls = await url_repo.list()
+ assert loaded_urls == tuple(Url.model_validate(u) for u in urls)
diff --git a/tests/test_unit/test_repositories/test_user.py b/tests/test_unit/test_repositories/test_user.py
index ba1701b..c53e761 100644
--- a/tests/test_unit/test_repositories/test_user.py
+++ b/tests/test_unit/test_repositories/test_user.py
@@ -1,50 +1,63 @@
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
-from inclusive_dance_bot.db.models import User
-from inclusive_dance_bot.db.repositories.user import UserRepository
-from inclusive_dance_bot.dto import ANONYMOUS_USER, UserDto
-from inclusive_dance_bot.exceptions import UserAlreadyExistsError
+from idb.db.models import User as UserDb
+from idb.db.repositories.user import UserRepository
+from idb.exceptions import UserAlreadyExistsError
+from idb.exceptions.user import UserNotFoundError
+from idb.generals.models.user import User
from tests.factories import UserFactory
async def test_create_user(user_repo: UserRepository, session: AsyncSession) -> None:
user = await user_repo.create(
username="username",
- user_id=1,
- name="user name",
- region="Tatuin",
- phone_number="+77777777",
+ id=1,
+ profile={
+ "name": "user name",
+ "region": "Tatuin",
+ "phone_number": "+77777777777",
+ },
+ is_admin=False,
+ is_superuser=False,
)
await session.commit()
- loaded_user = await session.get(User, user.id)
- assert user == UserDto.from_orm(loaded_user)
+ loaded_user = await session.get(UserDb, user.id)
+ assert user == User.model_validate(loaded_user)
async def test_invalid_double_create(user_repo: UserRepository) -> None:
await user_repo.create(
- user_id=1,
username="username",
- name="user name",
- region="Tatuin",
- phone_number="+77777777",
+ id=1,
+ is_admin=False,
+ is_superuser=False,
+ profile={
+ "name": "user name",
+ "region": "Tatuin",
+ "phone_number": "+77777777777",
+ },
)
with pytest.raises(UserAlreadyExistsError):
await user_repo.create(
- user_id=1,
username="username",
- name="user name",
- region="Tatuin",
- phone_number="+77777777",
+ id=1,
+ is_admin=False,
+ is_superuser=False,
+ profile={
+ "name": "user name",
+ "region": "Tatuin",
+ "phone_number": "+77777777777",
+ },
)
async def test_get_by_id(user_repo: UserRepository) -> None:
user = await UserFactory.create_async()
loaded_user = await user_repo.get_by_id(user.id)
- assert loaded_user == UserDto.from_orm(user)
+ assert loaded_user == User.model_validate(user)
async def test_get_anonymous(user_repo: UserRepository) -> None:
- anonymous = await user_repo.get_by_id(-1)
- assert anonymous == ANONYMOUS_USER
+ with pytest.raises(UserNotFoundError):
+ await user_repo.get_by_id(-1)
diff --git a/tests/test_unit/test_repositories/test_user_type.py b/tests/test_unit/test_repositories/test_user_type.py
index a15dfa7..53bad1e 100644
--- a/tests/test_unit/test_repositories/test_user_type.py
+++ b/tests/test_unit/test_repositories/test_user_type.py
@@ -1,10 +1,10 @@
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
-from inclusive_dance_bot.db.models import UserType
-from inclusive_dance_bot.db.repositories.user_type import UserTypeRepository
-from inclusive_dance_bot.dto import UserTypeDto
-from inclusive_dance_bot.exceptions import UserTypeAlreadyExistsError
+from idb.db.models import UserType as UserTypeDb
+from idb.db.repositories.user_type import UserTypeRepository
+from idb.exceptions import UserTypeAlreadyExistsError
+from idb.generals.models.user_type import UserType
from tests.factories import UserTypeFactory
@@ -13,8 +13,8 @@ async def test_create_user_type(
) -> None:
user_type = await user_type_repo.create(name="New user type")
await session.commit()
- saved_user_type = await session.get(UserType, user_type.id)
- assert user_type == UserTypeDto.from_orm(saved_user_type)
+ saved_user_type = await session.get(UserTypeDb, user_type.id)
+ assert user_type == UserType.model_validate(saved_user_type)
async def test_invalid_double_create(user_type_repo: UserTypeRepository) -> None:
@@ -24,12 +24,12 @@ async def test_invalid_double_create(user_type_repo: UserTypeRepository) -> None
async def test_get_list_empty(user_type_repo: UserTypeRepository) -> None:
- user_types = await user_type_repo.get_list()
+ user_types = await user_type_repo.list()
assert user_types == tuple()
async def test_get_list(user_type_repo: UserTypeRepository) -> None:
user_types = await UserTypeFactory.create_batch_async(size=5)
-
- loaded_user_types = await user_type_repo.get_list()
- assert set(loaded_user_types) == {UserTypeDto.from_orm(ut) for ut in user_types}
+ user_types.sort(key=lambda x: x.id)
+ loaded_user_types = await user_type_repo.list()
+ assert loaded_user_types == tuple(UserType.model_validate(ut) for ut in user_types)
diff --git a/tests/test_unit/test_repositories/test_user_type_user.py b/tests/test_unit/test_repositories/test_user_type_user.py
index bf5b355..bfcd265 100644
--- a/tests/test_unit/test_repositories/test_user_type_user.py
+++ b/tests/test_unit/test_repositories/test_user_type_user.py
@@ -1,9 +1,9 @@
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
-from inclusive_dance_bot.db.models import UserTypeUser
-from inclusive_dance_bot.db.repositories.user_type_user import UserTypeUserRepository
-from inclusive_dance_bot.exceptions import (
+from idb.db.models import UserTypeUser
+from idb.db.repositories.user_type_user import UserTypeUserRepository
+from idb.exceptions import (
InvalidUserIDError,
InvalidUserTypeIDError,
UserTypeUserAlreadyExistsError,
diff --git a/tests/test_unit/test_services/test_storage.py b/tests/test_unit/test_services/test_storage.py
deleted file mode 100644
index 70d8ac9..0000000
--- a/tests/test_unit/test_services/test_storage.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from inclusive_dance_bot.dto import UrlDto
-from inclusive_dance_bot.logic.storage import Storage
-from tests.factories import UrlFactory
-
-
-def test_initial_storage_is_empty(storage: Storage) -> None:
- assert len(storage._cache) == 0
-
-
-async def test_get_urls(storage: Storage) -> None:
- first_url = await UrlFactory.create_async()
- second_url = await UrlFactory.create_async()
- urls = await storage.get_urls()
- assert urls == {
- first_url.slug: UrlDto.from_orm(first_url),
- second_url.slug: UrlDto.from_orm(second_url),
- }
-
-
-async def test_cache_get_urls(storage: Storage, session: AsyncSession) -> None:
- first_url = await UrlFactory.create_async()
- second_url = await UrlFactory.create_async()
- await storage.get_urls()
- await session.delete(second_url)
- await session.delete(first_url)
- await session.commit()
- urls = await storage.get_urls()
- assert urls == {
- first_url.slug: UrlDto.from_orm(first_url),
- second_url.slug: UrlDto.from_orm(second_url),
- }
-
-
-async def test_get_url_by_slug(storage: Storage) -> None:
- new_url = await UrlFactory.create_async(slug="my_url")
- url = await storage.get_url_by_slug("my_url")
- assert UrlDto.from_orm(new_url) == url
-
-
-async def test_cache_get_url_by_slug(storage: Storage, session: AsyncSession) -> None:
- url = await UrlFactory.create_async(slug="my_slug")
- await storage.get_url_by_slug("my_slug")
-
- await session.delete(url)
-
- cached_url = await storage.get_url_by_slug("my_slug")
- assert cached_url.slug == "my_slug"
-
-
-async def test_get_user_types(storage: Storage) -> None:
- pass
-
-
-async def test_cache_get_user_types(storage: Storage, session: AsyncSession) -> None:
- pass
-
-
-async def test_get_submenus(storage: Storage) -> None:
- pass
-
-
-async def test_cache_get_submenus(storage: Storage, session: AsyncSession) -> None:
- pass
-
-
-async def test_refresh_all(storage: Storage, session: AsyncSession) -> None:
- pass
-
-
-async def test_refresh_urls(storage: Storage, session: AsyncSession) -> None:
- pass
diff --git a/tests/test_unit/test_services/test_url/test_create_url.py b/tests/test_unit/test_services/test_url/test_create_url.py
deleted file mode 100644
index 7ad39fe..0000000
--- a/tests/test_unit/test_services/test_url/test_create_url.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import pytest
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from inclusive_dance_bot.db.models import Url
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.dto import UrlDto
-from inclusive_dance_bot.exceptions.url import UrlSlugAlreadyExistsError
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.url import create_url
-from tests.factories import UrlFactory
-
-
-async def test_create_successful(
- uow: UnitOfWork,
- storage: Storage,
- session: AsyncSession,
-) -> None:
- slug = "new_url_slug"
- value = "https://example.com"
-
- url = await create_url(uow=uow, storage=storage, slug=slug, value=value)
-
- loaded_url = await session.get(Url, url.id)
- assert UrlDto.from_orm(loaded_url) == url
-
-
-async def test_error_url_slug_already_exists(uow: UnitOfWork, storage: Storage) -> None:
- url = await UrlFactory.create_async()
- with pytest.raises(UrlSlugAlreadyExistsError):
- await create_url(uow=uow, storage=storage, slug=url.slug, value="somevalue")
diff --git a/tests/test_unit/test_services/test_url/test_delete_url_by_slug.py b/tests/test_unit/test_services/test_url/test_delete_url_by_slug.py
deleted file mode 100644
index 500794c..0000000
--- a/tests/test_unit/test_services/test_url/test_delete_url_by_slug.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from inclusive_dance_bot.db.models import Url
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.url import delete_url_by_slug
-from tests.factories import UrlFactory
-
-
-async def test_delete_successful(
- uow: UnitOfWork, storage: Storage, session: AsyncSession
-) -> None:
- await UrlFactory.create_async(id=1, slug="new_url")
-
- await delete_url_by_slug(uow=uow, storage=storage, url_slug="new_url")
- assert await session.get(Url, 1) is None
-
-
-async def test_delete_unknown_slug(uow: UnitOfWork, storage: Storage) -> None:
- await delete_url_by_slug(uow=uow, storage=storage, url_slug="unknown")
diff --git a/tests/test_unit/test_services/test_url/test_update_url_by_slug.py b/tests/test_unit/test_services/test_url/test_update_url_by_slug.py
deleted file mode 100644
index b2140f2..0000000
--- a/tests/test_unit/test_services/test_url/test_update_url_by_slug.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import pytest
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.dto import UrlDto
-from inclusive_dance_bot.exceptions.url import (
- UrlNotFoundError,
- UrlSlugAlreadyExistsError,
-)
-from inclusive_dance_bot.logic.storage import Storage
-from inclusive_dance_bot.logic.url import update_url_by_slug
-from tests.factories import UrlFactory
-
-
-async def test_update_successful(
- uow: UnitOfWork, storage: Storage, session: AsyncSession
-) -> None:
- url = await UrlFactory.create_async(slug="slug")
- updated_url = await update_url_by_slug(
- uow=uow, storage=storage, url_slug="slug", value="https://vk.com"
- )
-
- await session.refresh(url)
- assert url.value == "https://vk.com"
-
- assert updated_url == UrlDto.from_orm(url)
-
-
-async def test_error_url_not_found(uow: UnitOfWork, storage: Storage) -> None:
- with pytest.raises(UrlNotFoundError):
- await update_url_by_slug(uow=uow, storage=storage, url_slug="unknown", value="")
-
-
-async def test_error_url_slug_already_exists(uow: UnitOfWork, storage: Storage) -> None:
- await UrlFactory.create_async(slug="first_url")
- await UrlFactory.create_async(slug="second_url")
- with pytest.raises(UrlSlugAlreadyExistsError):
- await update_url_by_slug(
- uow=uow,
- storage=storage,
- url_slug="second_url",
- slug="first_url",
- )
diff --git a/tests/test_unit/test_services/test_user/test_create_user.py b/tests/test_unit/test_services/test_user/test_create_user.py
deleted file mode 100644
index 98b9dc6..0000000
--- a/tests/test_unit/test_services/test_user/test_create_user.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import pytest
-from sqlalchemy import select
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from inclusive_dance_bot.db.models import User, UserTypeUser
-from inclusive_dance_bot.db.uow.main import UnitOfWork
-from inclusive_dance_bot.exceptions import (
- InvalidUserTypeIDError,
- UserAlreadyExistsError,
-)
-from inclusive_dance_bot.logic.user import create_user
-from tests.factories import UserFactory, UserTypeFactory
-
-
-async def test_create_successful(uow: UnitOfWork, session: AsyncSession) -> None:
- user_types = await UserTypeFactory.create_batch_async(size=3)
- user_id = 1
- await create_user(
- uow=uow,
- username="username",
- user_id=user_id,
- name="New user",
- region="Some region",
- phone_number="+79999999",
- user_type_ids=tuple(ut.id for ut in user_types),
- )
-
- user = await session.get(User, user_id)
- assert user is not None
-
- user_type_users = (
- await session.scalars(
- select(UserTypeUser).where(UserTypeUser.user_id == user_id)
- )
- ).all()
- assert {(utu.user_id, utu.user_type_id) for utu in user_type_users} == {
- (user_id, ut.id) for ut in user_types
- }
-
-
-async def test_error_user_type(uow: UnitOfWork, session: AsyncSession) -> None:
- user_id = 1
- with pytest.raises(InvalidUserTypeIDError):
- await create_user(
- uow=uow,
- user_id=user_id,
- username="username",
- name="New user",
- region="Some region",
- phone_number="+79999999",
- user_type_ids=(-1,),
- )
- user = await session.get(User, user_id)
- assert user is None
-
-
-async def test_error_user_id_already_exists(uow: UnitOfWork) -> None:
- user = await UserFactory.create_async()
- with pytest.raises(UserAlreadyExistsError):
- await create_user(
- uow=uow,
- user_id=user.id,
- username="username",
- name="New user",
- region="Some region",
- phone_number="+79999999",
- user_type_ids=[],
- )