From e28595580a6efd4abd10d7c08de5495b41c0a119 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Sun, 22 Sep 2024 18:35:08 +0300
Subject: [PATCH 01/75] write fsm, handlers, keyboards, setup, rewrite main
---
app/bot/callbacks.py | 64 +++++++++++++++++++++++++++++++++++
app/bot/fsm_context.py | 77 ++++++++++++++++++++++++++++++++++++++++++
app/bot/handlers.py | 28 +++++++++------
app/bot/keyborads.py | 74 +++++++++++++++++++++++++++++++++++-----
app/bot_setup.py | 14 ++++++++
app/main.py | 29 +++++++---------
bot.py.bak | 29 ----------------
7 files changed, 252 insertions(+), 63 deletions(-)
create mode 100644 app/bot/callbacks.py
create mode 100644 app/bot/fsm_context.py
create mode 100644 app/bot_setup.py
delete mode 100644 bot.py.bak
diff --git a/app/bot/callbacks.py b/app/bot/callbacks.py
new file mode 100644
index 0000000..6688f3c
--- /dev/null
+++ b/app/bot/callbacks.py
@@ -0,0 +1,64 @@
+from aiogram import F, Router
+from aiogram.types import CallbackQuery
+
+from bot.keyborads import (
+ list_of_projects_keyboard, main_keyboard
+)
+
+router = Router()
+
+
+@router.callback_query(F.data == 'show_projects')
+async def show_projects(callback: CallbackQuery):
+ """Выводит список проектов компании."""
+
+ await callback.answer()
+
+ if callback.message:
+ await callback.message.answer(
+ 'Вот некоторые из наших проектов. '
+ 'Выберите, чтобы узнать больше о каждом из них: ',
+ reply_markup=await list_of_projects_keyboard()
+ )
+
+ # тут надо дописать блок else, если случилась ошибка
+ # писать через (clause guard) ( * )
+
+
+@router.callback_query(F.data == 'back_to_main_menu')
+async def previous_choice(callback: CallbackQuery) -> None:
+ """Возвращает в основное меню."""
+
+ await callback.answer()
+
+ if callback.message:
+ await callback.message.answer(
+ 'Вы вернулись в оснвное меню. '
+ 'Как я могу помочь вам дальше? ',
+ reply_markup=main_keyboard
+ )
+ # ( * )
+
+
+@router.callback_query(F.data == 'get_faq')
+async def get_faq(callback: CallbackQuery) -> None:
+ """Инлайн вывод ответов на часто задаваемые вопросы."""
+
+ pass
+ # ( * )
+
+
+@router.callback_query(F.data == 'get_problems_with_products')
+async def get_problems_with_products(callback: CallbackQuery) -> None:
+ """Инлайн вывод проблем с продуктами."""
+
+ pass
+ # ( * )
+
+
+@router.callback_query(F.data == 'callback_request')
+async def callback_request(callback: CallbackQuery) -> None:
+ """Инлайн вывод запроса на обратный звонок."""
+
+ pass
+ # ( * )
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
new file mode 100644
index 0000000..b29196c
--- /dev/null
+++ b/app/bot/fsm_context.py
@@ -0,0 +1,77 @@
+from aiogram import F, Router
+from aiogram.fsm.state import StatesGroup, State
+from aiogram.fsm.context import FSMContext
+from aiogram.types import Message
+
+router = Router()
+
+
+class Form(StatesGroup):
+ """Форма для связи с менеджером."""
+
+ first_name = State()
+ last_name = State()
+ middle_name = State()
+ phone_number = State()
+
+
+QUESTIONS = {
+ Form.first_name: "Введите ваше имя:",
+ Form.last_name: "Введите вашу фамилию:",
+ Form.middle_name: "Введите ваше отчество (или 'нет', если отсутствует):",
+ Form.phone_number: "Введите ваш номер телефона:"
+}
+
+
+@router.message(F.text == 'Связаться с менеджером.')
+async def contact_with_manager(message: Message, state: FSMContext) -> None:
+ """Выводит связь с менеджером."""
+
+ await message.answer(
+ 'Пожалуйста, оставьте ваше имя и '
+ 'контактный номер, и наш менеджер '
+ 'свяжется с вами.'
+ )
+
+ await ask_next_question(message, state, Form.first_name)
+
+
+async def ask_next_question(
+ message: Message, state: FSMContext, next_state: State
+):
+ await state.set_state(next_state)
+ await message.answer(QUESTIONS[next_state])
+
+
+@router.message(Form.first_name)
+async def process_first_name(message: Message, state: FSMContext):
+ await state.update_data(first_name=message.text)
+ await ask_next_question(message, state, Form.last_name)
+
+
+@router.message(Form.last_name)
+async def process_last_name(message: Message, state: FSMContext):
+ await state.update_data(last_name=message.text)
+ await ask_next_question(message, state, Form.middle_name)
+
+
+@router.message(Form.middle_name)
+async def process_middle_name(message: Message, state: FSMContext):
+ await state.update_data(middle_name=message.text)
+ await ask_next_question(message, state, Form.phone_number)
+
+
+@router.message(Form.phone_number)
+async def process_phone_number(message: Message, state: FSMContext):
+ await state.update_data(phone_number=message.text)
+
+ user_data = await state.get_data()
+ await message.answer(
+ f"Форма заполнена!\n"
+ f"Имя: {user_data['first_name']}\n"
+ f"Фамилия: {user_data['last_name']}\n"
+ f"Отчество: {user_data['middle_name']}\n"
+ f"Номер телефона: {user_data['phone_number']}"
+ ) # вынести кнопку выхода в главное меню и добавить ее тут
+
+ await state.clear()
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index b0f7502..8a72957 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -1,10 +1,11 @@
from aiogram import F, Router
from aiogram.filters import CommandStart
-from aiogram.types import Message, CallbackQuery
+from aiogram.types import Message
from bot.keyborads import (
main_keyboard, company_information_keyboard,
- inline_products_and_services
+ inline_products_and_services, company_portfolio_choice,
+ support_keyboard
)
@@ -13,7 +14,7 @@
@router.message(CommandStart())
async def cmd_start(message: Message) -> None:
- """Приветствие пользователя."""
+ """Выводит приветствие пользователя."""
await message.answer(
'Здравстуйте! Я ваш виртуальный помошник.'
@@ -24,18 +25,19 @@ async def cmd_start(message: Message) -> None:
@router.message(F.text == 'Посмотреть портфолио.')
async def view_profile(message: Message) -> None:
- """Портфолио компании."""
+ """Выводит портфолио компании."""
await message.answer(
'Вот ссылка на на наше портофолио: [здесь url]. '
'Хотите узнать больше о конкретных проектах '
- 'или услугах?'
+ 'или услугах?',
+ reply_markup=company_portfolio_choice
)
@router.message(F.text == 'Получить информацию о компании.')
async def get_information_about_company(message: Message) -> None:
- """Информация о компнии."""
+ """Выводит информацию о компнии."""
await message.answer(
'Вот несколько вариантов информации о нашей '
@@ -46,7 +48,7 @@ async def get_information_about_company(message: Message) -> None:
@router.message(F.text == 'Узнать о продуктах и услугах.')
async def get_information_about_products_and_services(message: Message) -> None:
- """Информация о продуктах и услугах."""
+ """Выводит информация о продуктах и услугах."""
await message.answer(
'Мы предлагаем следующие продукты и услуги.'
@@ -55,6 +57,12 @@ async def get_information_about_products_and_services(message: Message) -> None:
)
-@router.callback_query(lambda call: call == 'previous_choice')
-def previous_choice(callback_query: CallbackQuery) -> None:
- pass
+@router.message(F.text == 'Получить техническую поддержку.')
+async def get_support(message: Message) -> None:
+ """Выводит виды тех. поддержки."""
+
+ await message.answer(
+ 'Какой вид технической поддержки '
+ 'вам нужен? ',
+ reply_markup=support_keyboard
+ )
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index d887517..974b2c0 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -10,7 +10,9 @@
'Разработка сайтов', 'Создание порталов',
'Разработка мобильных приложений', 'Консультация по КИОСК365',
'"НБП ЕЖА"', 'Хостинг',
-] # моделирую результат запроса из бд
+] # моделирую результат запроса из бд ( * )
+
+LIST_OF_PROJECTS = ['Проект1', 'Проект2', 'Проект3'] # ( * )
main_keyboard = ReplyKeyboardMarkup(
keyboard=[
@@ -41,8 +43,8 @@
],
[
InlineKeyboardButton(
- text='Назад к основным вариантам.',
- callback_data='previous_choice'
+ text='Вернуться к основным вариантам.',
+ callback_data='back_to_main_menu'
)
]
]
@@ -63,8 +65,8 @@ async def inline_products_and_services(): # тут будем брать дан
keyboard.add(
InlineKeyboardButton(
- text='Назад к основным вариантам.',
- callback_data='previous_choice'
+ text='Вернуться к основным вариантам.',
+ callback_data='back_to_main_menu'
)
)
@@ -73,11 +75,67 @@ async def inline_products_and_services(): # тут будем брать дан
company_portfolio_choice = InlineKeyboardMarkup(
inline_keyboard=[
- [InlineKeyboardButton(text='Да', '''TODO: бот отвечает и выводит проектов''')],
[
InlineKeyboardButton(
- text='Назад к основным вариантам.',
- callback_data='previous_choice'
+ text='Перейти к проектам.',
+ callback_data='show_projects'
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text='Вернуться к основным вариантам.',
+ callback_data='back_to_main_menu'
+ )
+ ]
+ ]
+)
+
+
+async def list_of_projects_keyboard(): # данные будут в бд
+ """Инлайн вывод проектов."""
+
+ keyboard = InlineKeyboardBuilder()
+
+ for project in LIST_OF_PROJECTS:
+ keyboard.add(InlineKeyboardButton(
+ text=project,
+ url='https://github.com/' # тут будут ссылки, берем из бд
+ ))
+
+ keyboard.add(
+ InlineKeyboardButton(
+ text='Вернуться к основным вариантам.',
+ callback_data='back_to_main_menu'
+ )
+ )
+
+ return keyboard.adjust(1).as_markup()
+
+
+support_keyboard = InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text='F.A.Q',
+ callback_data='get_faq'
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text='Проблемы с продуктами',
+ callback_data='get_problems_with_products'
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text='Запрос на обратный звонок',
+ callback_data='callback_request'
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text='Вернуться к основным вариантам.',
+ callback_data='back_to_main_menu'
)
]
]
diff --git a/app/bot_setup.py b/app/bot_setup.py
new file mode 100644
index 0000000..964c9a6
--- /dev/null
+++ b/app/bot_setup.py
@@ -0,0 +1,14 @@
+import os
+from aiogram import Bot, Dispatcher
+from aiogram.fsm.storage.memory import MemoryStorage
+from dotenv import load_dotenv
+
+load_dotenv()
+
+bot = Bot(token=os.getenv('BOT_TOKEN'))
+dispatcher = Dispatcher(storage=MemoryStorage())
+
+
+def check_token() -> None:
+ if os.getenv('BOT_TOKEN') is None:
+ raise ValueError('Отсутствуют необходимые токены.')
diff --git a/app/main.py b/app/main.py
index f7bca2e..5e5aab2 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,28 +1,25 @@
-import os
import logging
import asyncio
-import sys
-from aiogram import Bot, Dispatcher
-from dotenv import load_dotenv
-
-from bot.handlers import router
-
-
-load_dotenv()
-
-
-bot = Bot(token=os.getenv('BOT_TOKEN'))
-dispatcher = Dispatcher()
+from bot_setup import bot, dispatcher, check_token
+from bot.handlers import router as message_router
+from bot.callbacks import router as callback_router
+from bot.fsm_context import router as fsm_context_router
async def main() -> None:
"""Запуск SCID бота."""
- if os.getenv('BOT_TOKEN') is None: # нарушает solid - принцип ед. наследсвенности
- sys.exit('Отсутсвуют необходимые токены.')
+ try:
+ check_token()
+ except ValueError as e:
+ print(e)
+ return
+
+ dispatcher.include_router(message_router)
+ dispatcher.include_router(callback_router)
+ dispatcher.include_router(fsm_context_router)
- dispatcher.include_router(router)
await dispatcher.start_polling(bot)
if __name__ == "__main__":
diff --git a/bot.py.bak b/bot.py.bak
deleted file mode 100644
index 86da734..0000000
--- a/bot.py.bak
+++ /dev/null
@@ -1,29 +0,0 @@
-import os
-import sys
-
-from dotenv import load_dotenv
-
-load_dotenv()
-
-
-BOT_TOKEN = os.getenv('BOT_TOKEN')
-
-
-def check_tokens():
- """Проверяет наличие необходимых для работы программы токенов."""
- tokens_bool = True
- for token in (BOT_TOKEN):
- if not token:
- tokens_bool = False
- return tokens_bool
-
-
-def main():
- """Основная логика работы бота."""
- if not check_tokens():
- sys.exit('Отсутствует токен!')
-
-
-if __name__ == '__main__':
- """Запуск работы бота."""
- main()
From fbf39dcf5ffc7c64a961e4066223337a4a6b70d5 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Mon, 23 Sep 2024 14:52:09 +0300
Subject: [PATCH 02/75] add validation in fsm_context
---
.idea/.gitignore | 3 +
.../inspectionProfiles/profiles_settings.xml | 6 ++
.idea/misc.xml | 4 ++
.idea/modules.xml | 8 +++
.idea/scid_bot_3.iml | 12 ++++
.idea/vcs.xml | 6 ++
app/bot/fsm_context.py | 65 +++++++++++++++++--
app/bot/handlers.py | 4 +-
app/bot/keyborads.py | 2 +-
app/bot/validators.py | 18 +++++
10 files changed, 121 insertions(+), 7 deletions(-)
create mode 100644 .idea/.gitignore
create mode 100644 .idea/inspectionProfiles/profiles_settings.xml
create mode 100644 .idea/misc.xml
create mode 100644 .idea/modules.xml
create mode 100644 .idea/scid_bot_3.iml
create mode 100644 .idea/vcs.xml
create mode 100644 app/bot/validators.py
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..a971a2c
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..11d10c4
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/scid_bot_3.iml b/.idea/scid_bot_3.iml
new file mode 100644
index 0000000..b5ad51a
--- /dev/null
+++ b/.idea/scid_bot_3.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index b29196c..be17273 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -1,7 +1,13 @@
from aiogram import F, Router
from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.context import FSMContext
-from aiogram.types import Message
+from aiogram.types import Message, InlineKeyboardButton
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from bot.validators import (
+ is_valid_name, is_valid_phone_number, format_phone_number
+)
+
router = Router()
@@ -19,13 +25,15 @@ class Form(StatesGroup):
Form.first_name: "Введите ваше имя:",
Form.last_name: "Введите вашу фамилию:",
Form.middle_name: "Введите ваше отчество (или 'нет', если отсутствует):",
- Form.phone_number: "Введите ваш номер телефона:"
+ Form.phone_number: (
+ "Введите ваш номер телефона (в формате +7XXXXXXXXXX или 8XXXXXXXXXX):"
+ )
}
@router.message(F.text == 'Связаться с менеджером.')
async def contact_with_manager(message: Message, state: FSMContext) -> None:
- """Выводит связь с менеджером."""
+ """Выводит форму для связи с менеджером."""
await message.answer(
'Пожалуйста, оставьте ваше имя и '
@@ -39,31 +47,69 @@ async def contact_with_manager(message: Message, state: FSMContext) -> None:
async def ask_next_question(
message: Message, state: FSMContext, next_state: State
):
+ """Переход к следующему вопросу."""
+
await state.set_state(next_state)
await message.answer(QUESTIONS[next_state])
@router.message(Form.first_name)
async def process_first_name(message: Message, state: FSMContext):
+
+ if not is_valid_name(message.text):
+ await message.answer(
+ "Имя должно содержать только буквы. Попробуйте снова."
+ )
+ await ask_next_question(message, state, Form.first_name)
+ return
+
await state.update_data(first_name=message.text)
await ask_next_question(message, state, Form.last_name)
@router.message(Form.last_name)
async def process_last_name(message: Message, state: FSMContext):
+
+ if not is_valid_name(message.text):
+ await message.answer(
+ "Фамилия должна содержать только буквы. Попробуйте снова."
+ )
+ await ask_next_question(message, state, Form.last_name)
+ return
+
await state.update_data(last_name=message.text)
await ask_next_question(message, state, Form.middle_name)
@router.message(Form.middle_name)
async def process_middle_name(message: Message, state: FSMContext):
+
+ if message.text.lower() != "нет" and not is_valid_name(message.text):
+ await message.answer(
+ "Отчество должно содержать только буквы или быть 'нет'. "
+ "Попробуйте снова."
+ )
+ await ask_next_question(message, state, Form.middle_name)
+ return
+
await state.update_data(middle_name=message.text)
await ask_next_question(message, state, Form.phone_number)
@router.message(Form.phone_number)
async def process_phone_number(message: Message, state: FSMContext):
- await state.update_data(phone_number=message.text)
+
+ if not is_valid_phone_number(message.text):
+ await message.answer(
+ "Номер телефона должен быть в формате +7XXXXXXXXXX "
+ " или 8XXXXXXXXXX. Попробуйте снова."
+ )
+ await ask_next_question(message, state, Form.phone_number)
+ return
+
+ formatted_phone_number = format_phone_number(message.text)
+
+ await state.update_data(phone_number=formatted_phone_number)
user_data = await state.get_data()
await message.answer(
@@ -72,6 +118,15 @@ async def process_phone_number(message: Message, state: FSMContext):
f"Фамилия: {user_data['last_name']}\n"
f"Отчество: {user_data['middle_name']}\n"
f"Номер телефона: {user_data['phone_number']}"
- ) # вынести кнопку выхода в главное меню и добавить ее тут
+ )
+
+ keyboard = InlineKeyboardBuilder()
+ keyboard.add(InlineKeyboardButton(
+ text="Вернуться в главное меню", callback_data="back_to_main_menu"
+ ))
+ await message.answer(
+ "Вы можете вернуться в главное меню.",
+ reply_markup=keyboard.as_markup()
+ )
await state.clear()
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index 8a72957..fb8be3a 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -47,7 +47,9 @@ async def get_information_about_company(message: Message) -> None:
@router.message(F.text == 'Узнать о продуктах и услугах.')
-async def get_information_about_products_and_services(message: Message) -> None:
+async def get_information_about_products_and_services(
+ message: Message
+) -> None:
"""Выводит информация о продуктах и услугах."""
await message.answer(
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index 974b2c0..24b8089 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -1,6 +1,6 @@
from aiogram.types import (
ReplyKeyboardMarkup, KeyboardButton,
- InlineKeyboardMarkup, InlineKeyboardButton,
+ InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
diff --git a/app/bot/validators.py b/app/bot/validators.py
new file mode 100644
index 0000000..92cdf71
--- /dev/null
+++ b/app/bot/validators.py
@@ -0,0 +1,18 @@
+from re import match
+
+
+def is_valid_name(name: str) -> bool:
+ """Проверяет, что имя содержит только буквы."""
+ return bool(match(r"^[A-Za-zА-Яа-яЁё]+$", name))
+
+
+def is_valid_phone_number(phone_number: str) -> bool:
+ """Проверяет, что номер телефона соответствует шаблону +7XXXXXXXXXX или 8XXXXXXXXXX."""
+ return bool(match(r"^(\+7|8)\d{10}$", phone_number))
+
+
+def format_phone_number(phone_number: str) -> str:
+ """Преобразует номер телефона, начинающийся с 8, в формат +7."""
+ if phone_number.startswith('8'):
+ return '+7' + phone_number[1:]
+ return phone_number
From a73a3154a161eac5d0688ee8e68ad36fe0fd2fa6 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Mon, 23 Sep 2024 14:54:13 +0300
Subject: [PATCH 03/75] add validation in fsm, update gitignore
---
.gitignore | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/.gitignore b/.gitignore
index 82f9275..c3959f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -155,8 +155,4 @@ dmypy.json
cython_debug/
# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# 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
From 1656c91efd98056b63259c9d92899e5e159c35e5 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Mon, 23 Sep 2024 14:57:02 +0300
Subject: [PATCH 04/75] Delete .idea
---
.idea/.gitignore | 3 ---
.idea/inspectionProfiles/profiles_settings.xml | 6 ------
.idea/misc.xml | 4 ----
.idea/modules.xml | 8 --------
.idea/scid_bot_3.iml | 12 ------------
.idea/vcs.xml | 6 ------
6 files changed, 39 deletions(-)
delete mode 100644 .idea/.gitignore
delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml
delete mode 100644 .idea/misc.xml
delete mode 100644 .idea/modules.xml
delete mode 100644 .idea/scid_bot_3.iml
delete mode 100644 .idea/vcs.xml
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index a971a2c..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 11d10c4..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/scid_bot_3.iml b/.idea/scid_bot_3.iml
deleted file mode 100644
index b5ad51a..0000000
--- a/.idea/scid_bot_3.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
From a498d2379480e1d2643fc023a10776c4656b1ffa Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Mon, 23 Sep 2024 19:40:29 +0300
Subject: [PATCH 05/75] some fix
---
.gitignore | 2 +-
app/bot/keyborads.py | 3 +--
app/bot/validators.py | 10 +++++++++-
3 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/.gitignore b/.gitignore
index c3959f9..eff8c02 100644
--- a/.gitignore
+++ b/.gitignore
@@ -155,4 +155,4 @@ dmypy.json
cython_debug/
# PyCharm
-.idea
+.idea/
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index 24b8089..0f55392 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -1,6 +1,6 @@
from aiogram.types import (
ReplyKeyboardMarkup, KeyboardButton,
- InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
+ InlineKeyboardMarkup, InlineKeyboardButton
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -57,7 +57,6 @@ async def inline_products_and_services(): # тут будем брать дан
keyboard = InlineKeyboardBuilder()
for index, product_and_service in enumerate(PRODUCTS_AND_SERVICES):
- # callback_data = f"service_{index}"
keyboard.add(InlineKeyboardButton(
text=product_and_service,
callback_data=f'service_{index}'
diff --git a/app/bot/validators.py b/app/bot/validators.py
index 92cdf71..dc34d8b 100644
--- a/app/bot/validators.py
+++ b/app/bot/validators.py
@@ -3,16 +3,24 @@
def is_valid_name(name: str) -> bool:
"""Проверяет, что имя содержит только буквы."""
+
return bool(match(r"^[A-Za-zА-Яа-яЁё]+$", name))
def is_valid_phone_number(phone_number: str) -> bool:
- """Проверяет, что номер телефона соответствует шаблону +7XXXXXXXXXX или 8XXXXXXXXXX."""
+ """
+ Валидация номера телефона.
+
+ Проверяет, что номер телефона соответствует
+ шаблону +7XXXXXXXXXX или 8XXXXXXXXXX.
+ """
+
return bool(match(r"^(\+7|8)\d{10}$", phone_number))
def format_phone_number(phone_number: str) -> str:
"""Преобразует номер телефона, начинающийся с 8, в формат +7."""
+
if phone_number.startswith('8'):
return '+7' + phone_number[1:]
return phone_number
From 53b46afae94a6b9cbdf583e77e3124b49e8f79c4 Mon Sep 17 00:00:00 2001
From: Alex Guzey
Date: Mon, 23 Sep 2024 22:01:40 +0300
Subject: [PATCH 06/75] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?=
=?UTF-8?q?=D0=BB=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=87=D0=B8?=
=?UTF-8?q?=D0=BA=20=D0=B8=20=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D0=BE=D0=B2?=
=?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/bot/fsm_context.py | 209 ++++++++++++++++++++++++++---------------
app/bot/handlers.py | 141 ++++++++++++++++-----------
app/bot_setup.py | 8 ++
app/main.py | 27 +++++-
4 files changed, 250 insertions(+), 135 deletions(-)
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index be17273..8b80eb4 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -1,3 +1,5 @@
+import logging
+
from aiogram import F, Router
from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.context import FSMContext
@@ -8,13 +10,12 @@
is_valid_name, is_valid_phone_number, format_phone_number
)
-
router = Router()
+logger = logging.getLogger(__name__)
class Form(StatesGroup):
"""Форма для связи с менеджером."""
-
first_name = State()
last_name = State()
middle_name = State()
@@ -24,9 +25,12 @@ class Form(StatesGroup):
QUESTIONS = {
Form.first_name: "Введите ваше имя:",
Form.last_name: "Введите вашу фамилию:",
- Form.middle_name: "Введите ваше отчество (или 'нет', если отсутствует):",
+ Form.middle_name: (
+ "Введите ваше отчество (или 'нет', если отсутствует):"
+ ),
Form.phone_number: (
- "Введите ваш номер телефона (в формате +7XXXXXXXXXX или 8XXXXXXXXXX):"
+ "Введите ваш номер телефона (в формате +7XXXXXXXXXX "
+ "или 8XXXXXXXXXX):"
)
}
@@ -34,99 +38,152 @@ class Form(StatesGroup):
@router.message(F.text == 'Связаться с менеджером.')
async def contact_with_manager(message: Message, state: FSMContext) -> None:
"""Выводит форму для связи с менеджером."""
-
- await message.answer(
- 'Пожалуйста, оставьте ваше имя и '
- 'контактный номер, и наш менеджер '
- 'свяжется с вами.'
- )
-
- await ask_next_question(message, state, Form.first_name)
+ try:
+ await message.answer(
+ 'Пожалуйста, оставьте ваше имя и контактный номер, '
+ 'и наш менеджер свяжется с вами.'
+ )
+ await ask_next_question(message, state, Form.first_name)
+ logger.info(
+ f"Пользователь {message.from_user.id} начал процесс."
+ )
+ except Exception as e:
+ logger.error(
+ f"Ошибка при выводе формы для пользователя "
+ f"{message.from_user.id}: {e}"
+ )
+ await message.answer("Произошла ошибка. Попробуйте снова.")
async def ask_next_question(
message: Message, state: FSMContext, next_state: State
-):
+) -> None:
"""Переход к следующему вопросу."""
-
- await state.set_state(next_state)
- await message.answer(QUESTIONS[next_state])
+ try:
+ await state.set_state(next_state)
+ await message.answer(QUESTIONS[next_state])
+ logger.info(
+ f"Переход к следующему вопросу: {next_state}"
+ )
+ except Exception as e:
+ logger.error(
+ f"Ошибка при переходе к следующему вопросу для пользователя "
+ f"{message.from_user.id}: {e}"
+ )
+ await message.answer("Произошла ошибка. Попробуйте снова.")
@router.message(Form.first_name)
-async def process_first_name(message: Message, state: FSMContext):
-
- if not is_valid_name(message.text):
- await message.answer(
- "Имя должно содержать только буквы. Попробуйте снова."
+async def process_first_name(message: Message, state: FSMContext) -> None:
+ try:
+ if not is_valid_name(message.text):
+ await message.answer(
+ "Имя должно содержать только буквы. Попробуйте снова."
+ )
+ await ask_next_question(message, state, Form.first_name)
+ return
+
+ await state.update_data(first_name=message.text)
+ logger.info(
+ f"Пользователь {message.from_user.id} ввёл имя: {message.text}"
)
- await ask_next_question(message, state, Form.first_name)
- return
-
- await state.update_data(first_name=message.text)
- await ask_next_question(message, state, Form.last_name)
+ await ask_next_question(message, state, Form.last_name)
+ except Exception as e:
+ logger.error(
+ f"Ошибка при обработке имени пользователя "
+ f"{message.from_user.id}: {e}"
+ )
+ await message.answer("Произошла ошибка. Попробуйте снова.")
@router.message(Form.last_name)
-async def process_last_name(message: Message, state: FSMContext):
-
- if not is_valid_name(message.text):
- await message.answer(
- "Фамилия должна содержать только буквы. Попробуйте снова."
+async def process_last_name(message: Message, state: FSMContext) -> None:
+ try:
+ if not is_valid_name(message.text):
+ await message.answer(
+ "Фамилия должна содержать только буквы. Попробуйте снова."
+ )
+ await ask_next_question(message, state, Form.last_name)
+ return
+
+ await state.update_data(last_name=message.text)
+ logger.info(
+ f"Пользователь {message.from_user.id} ввёл фамилию: "
+ f"{message.text}"
)
- await ask_next_question(message, state, Form.last_name)
- return
-
- await state.update_data(last_name=message.text)
- await ask_next_question(message, state, Form.middle_name)
+ await ask_next_question(message, state, Form.middle_name)
+ except Exception as e:
+ logger.error(
+ f"Ошибка при обработке фамилии пользователя "
+ f"{message.from_user.id}: {e}"
+ )
+ await message.answer("Произошла ошибка. Попробуйте снова.")
@router.message(Form.middle_name)
-async def process_middle_name(message: Message, state: FSMContext):
-
- if message.text.lower() != "нет" and not is_valid_name(message.text):
- await message.answer(
- "Отчество должно содержать только буквы или быть 'нет'. "
- "Попробуйте снова."
+async def process_middle_name(message: Message, state: FSMContext) -> None:
+ try:
+ if message.text.lower() != "нет" and not is_valid_name(message.text):
+ await message.answer(
+ "Отчество должно содержать только буквы или быть 'нет'. "
+ "Попробуйте снова."
+ )
+ await ask_next_question(message, state, Form.middle_name)
+ return
+
+ await state.update_data(middle_name=message.text)
+ logger.info(
+ f"Пользователь {message.from_user.id} ввёл отчество: "
+ f"{message.text}"
)
- await ask_next_question(message, state, Form.middle_name)
- return
-
- await state.update_data(middle_name=message.text)
- await ask_next_question(message, state, Form.phone_number)
+ await ask_next_question(message, state, Form.phone_number)
+ except Exception as e:
+ logger.error(
+ f"Ошибка при обработке отчества пользователя "
+ f"{message.from_user.id}: {e}"
+ )
+ await message.answer("Произошла ошибка. Попробуйте снова.")
@router.message(Form.phone_number)
-async def process_phone_number(message: Message, state: FSMContext):
+async def process_phone_number(message: Message, state: FSMContext) -> None:
+ try:
+ if not is_valid_phone_number(message.text):
+ await message.answer(
+ "Номер телефона должен быть в формате +7XXXXXXXXXX "
+ "или 8XXXXXXXXXX. Попробуйте снова."
+ )
+ await ask_next_question(message, state, Form.phone_number)
+ return
+
+ formatted_phone_number = format_phone_number(message.text)
+ await state.update_data(phone_number=formatted_phone_number)
+ logger.info(
+ f"Пользователь {message.from_user.id} ввёл телефон: "
+ f"{formatted_phone_number}"
+ )
- if not is_valid_phone_number(message.text):
+ user_data = await state.get_data()
await message.answer(
- "Номер телефона должен быть в формате +7XXXXXXXXXX "
- " или 8XXXXXXXXXX. Попробуйте снова."
+ f"Форма заполнена!\n"
+ f"Имя: {user_data['first_name']}\n"
+ f"Фамилия: {user_data['last_name']}\n"
+ f"Отчество: {user_data['middle_name']}\n"
+ f"Номер телефона: {user_data['phone_number']}"
)
- await ask_next_question(message, state, Form.phone_number)
- return
-
- formatted_phone_number = format_phone_number(message.text)
-
- await state.update_data(phone_number=formatted_phone_number)
-
- user_data = await state.get_data()
- await message.answer(
- f"Форма заполнена!\n"
- f"Имя: {user_data['first_name']}\n"
- f"Фамилия: {user_data['last_name']}\n"
- f"Отчество: {user_data['middle_name']}\n"
- f"Номер телефона: {user_data['phone_number']}"
- )
- keyboard = InlineKeyboardBuilder()
- keyboard.add(InlineKeyboardButton(
- text="Вернуться в главное меню", callback_data="back_to_main_menu"
- ))
- await message.answer(
- "Вы можете вернуться в главное меню.",
- reply_markup=keyboard.as_markup()
- )
-
- await state.clear()
+ keyboard = InlineKeyboardBuilder()
+ keyboard.add(InlineKeyboardButton(
+ text="Вернуться в главное меню", callback_data="back_to_main_menu"
+ ))
+ await message.answer(
+ "Вы можете вернуться в главное меню.",
+ reply_markup=keyboard.as_markup()
+ )
+ await state.clear()
+ except Exception as e:
+ logger.error(
+ f"Ошибка при обработке номера телефона пользователя "
+ f"{message.from_user.id}: {e}"
+ )
+ await message.answer("Произошла ошибка. Попробуйте снова.")
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index fb8be3a..398b806 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -1,70 +1,101 @@
+import logging
from aiogram import F, Router
from aiogram.filters import CommandStart
from aiogram.types import Message
+
from bot.keyborads import (
main_keyboard, company_information_keyboard,
inline_products_and_services, company_portfolio_choice,
- support_keyboard
)
-
router = Router()
+logger = logging.getLogger(__name__)
+
@router.message(CommandStart())
async def cmd_start(message: Message) -> None:
"""Выводит приветствие пользователя."""
-
- await message.answer(
- 'Здравстуйте! Я ваш виртуальный помошник.'
- 'Как я могу помочь вам сегодня?',
- reply_markup=main_keyboard
- )
-
-
-@router.message(F.text == 'Посмотреть портфолио.')
-async def view_profile(message: Message) -> None:
- """Выводит портфолио компании."""
-
- await message.answer(
- 'Вот ссылка на на наше портофолио: [здесь url]. '
- 'Хотите узнать больше о конкретных проектах '
- 'или услугах?',
- reply_markup=company_portfolio_choice
- )
-
-
-@router.message(F.text == 'Получить информацию о компании.')
-async def get_information_about_company(message: Message) -> None:
- """Выводит информацию о компнии."""
-
- await message.answer(
- 'Вот несколько вариантов информации о нашей '
- 'компании. Что именно вас интересует? ',
- reply_markup=company_information_keyboard
- )
-
-
-@router.message(F.text == 'Узнать о продуктах и услугах.')
-async def get_information_about_products_and_services(
- message: Message
-) -> None:
- """Выводит информация о продуктах и услугах."""
-
- await message.answer(
- 'Мы предлагаем следующие продукты и услуги.'
- 'Какой из низ вас интересует?',
- reply_markup=await inline_products_and_services()
- )
-
-
-@router.message(F.text == 'Получить техническую поддержку.')
-async def get_support(message: Message) -> None:
- """Выводит виды тех. поддержки."""
-
- await message.answer(
- 'Какой вид технической поддержки '
- 'вам нужен? ',
- reply_markup=support_keyboard
- )
+ try:
+ await message.answer(
+ 'Здравстуйте! Я ваш виртуальный помощник. '
+ 'Как я могу помочь вам сегодня?',
+ reply_markup=main_keyboard()
+ )
+ logger.info(
+ f"Пользователь {message.from_user.id} вызвал команду /start"
+ )
+ except Exception as e:
+ logger.error(
+ f"Ошибка при обработке команды /start для пользователя "
+ f"{message.from_user.id}: {e}"
+ )
+ await message.answer(
+ "Произошла ошибка. Пожалуйста, попробуйте позже."
+ )
+
+
+@router.message(F.text == 'Посмотреть портфолио')
+async def view_portfolio(message: Message) -> None:
+ """Показ портфолио компании."""
+ try:
+ await message.answer(
+ 'Вот наше портфолио:',
+ reply_markup=company_portfolio_choice
+ )
+ logger.info(
+ f"Пользователь {message.from_user.id} запросил портфолио"
+ )
+ except Exception as e:
+ logger.error(
+ f"Ошибка при показе портфолио для пользователя "
+ f"{message.from_user.id}: {e}"
+ )
+ await message.answer(
+ "Произошла ошибка. Пожалуйста, попробуйте позже."
+ )
+
+
+@router.message(F.text == 'Получить информацию о компании')
+async def company_info(message: Message) -> None:
+ """Информация о компании."""
+ try:
+ await message.answer(
+ 'Информация о компании:',
+ reply_markup=company_information_keyboard
+ )
+ logger.info(
+ f"Пользователь {message.from_user.id} запросил информацию "
+ f"о компании"
+ )
+ except Exception as e:
+ logger.error(
+ f"Ошибка при запросе информации о компании для пользователя "
+ f"{message.from_user.id}: {e}"
+ )
+ await message.answer(
+ "Произошла ошибка. Пожалуйста, попробуйте позже."
+ )
+
+
+@router.message(F.text == 'Узнать о продуктах и услугах')
+async def products_services(message: Message) -> None:
+ """Информация о продуктах и услугах."""
+ try:
+ await message.answer(
+ 'Вот наши продукты и услуги:',
+ reply_markup=await inline_products_and_services()
+ )
+ logger.info(
+ f"Пользователь {message.from_user.id} запросил информацию "
+ f"о продуктах и услугах"
+ )
+ except Exception as e:
+ logger.error(
+ f"Ошибка при запросе информации о продуктах и услугах "
+ f"для пользователя {message.from_user.id}: {e}"
+ )
+ await message.answer(
+ "Произошла ошибка. Пожалуйста, попробуйте позже."
+ )
diff --git a/app/bot_setup.py b/app/bot_setup.py
index 964c9a6..0f39896 100644
--- a/app/bot_setup.py
+++ b/app/bot_setup.py
@@ -1,14 +1,22 @@
import os
+import logging
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from dotenv import load_dotenv
load_dotenv()
+# Настройка логирования
+logger = logging.getLogger(__name__)
+
bot = Bot(token=os.getenv('BOT_TOKEN'))
dispatcher = Dispatcher(storage=MemoryStorage())
def check_token() -> None:
+ """Проверка наличия токена бота."""
if os.getenv('BOT_TOKEN') is None:
+ logger.error("Токен бота не найден.")
raise ValueError('Отсутствуют необходимые токены.')
+ else:
+ logger.info("Токен бота успешно загружен.")
diff --git a/app/main.py b/app/main.py
index 5e5aab2..1cc7a57 100644
--- a/app/main.py
+++ b/app/main.py
@@ -7,24 +7,43 @@
from bot.fsm_context import router as fsm_context_router
+# Настройка логирования
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ handlers=[
+ logging.StreamHandler(), # Логирование в консоль
+ logging.FileHandler("bot.log", encoding='utf-8') # Логирование в файл
+ ]
+)
+
+logger = logging.getLogger(__name__)
+
+
async def main() -> None:
"""Запуск SCID бота."""
try:
check_token()
except ValueError as e:
- print(e)
+ logger.error(f"Ошибка проверки токена: {e}")
return
dispatcher.include_router(message_router)
dispatcher.include_router(callback_router)
dispatcher.include_router(fsm_context_router)
- await dispatcher.start_polling(bot)
+ try:
+ logger.info("Запуск бота...")
+ await dispatcher.start_polling(bot)
+ except Exception as e:
+ logger.error(f"Критическая ошибка в работе бота: {e}")
+
if __name__ == "__main__":
- logging.basicConfig(level=logging.INFO)
try:
asyncio.run(main())
except KeyboardInterrupt:
- print('Exit')
+ logger.info("Бот остановлен пользователем.")
+ except Exception as e:
+ logger.error(f"Произошла непредвиденная ошибка: {e}")
From a7381341d972e0066f245cae9586b3c5a67adf52 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Tue, 24 Sep 2024 11:30:51 +0300
Subject: [PATCH 07/75] add callback
---
app/bot/callbacks.py | 11 +++++++++--
app/bot/fsm_context.py | 17 +++++++++++------
2 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/app/bot/callbacks.py b/app/bot/callbacks.py
index 6688f3c..0803b5a 100644
--- a/app/bot/callbacks.py
+++ b/app/bot/callbacks.py
@@ -45,7 +45,7 @@ async def get_faq(callback: CallbackQuery) -> None:
"""Инлайн вывод ответов на часто задаваемые вопросы."""
pass
- # ( * )
+ # ( * ) кнопка с вопросами - ответ отдельным сообщением
@router.callback_query(F.data == 'get_problems_with_products')
@@ -53,7 +53,7 @@ async def get_problems_with_products(callback: CallbackQuery) -> None:
"""Инлайн вывод проблем с продуктами."""
pass
- # ( * )
+ # ( * ) кнопка с вопросами - ответ отдельным сообщением
@router.callback_query(F.data == 'callback_request')
@@ -62,3 +62,10 @@ async def callback_request(callback: CallbackQuery) -> None:
pass
# ( * )
+
+
+@router.callback_data(F.data == 'back_to_products')
+async def back_to_products(callback: CallbackQuery) -> None:
+ """Возвращает к выюору продуктов."""
+
+ pass
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index be17273..73bccd4 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -46,7 +46,7 @@ async def contact_with_manager(message: Message, state: FSMContext) -> None:
async def ask_next_question(
message: Message, state: FSMContext, next_state: State
-):
+) -> None:
"""Переход к следующему вопросу."""
await state.set_state(next_state)
@@ -54,7 +54,8 @@ async def ask_next_question(
@router.message(Form.first_name)
-async def process_first_name(message: Message, state: FSMContext):
+async def process_first_name(message: Message, state: FSMContext) -> None:
+ """Состояние: ввод имени."""
if not is_valid_name(message.text):
await message.answer(
@@ -68,8 +69,8 @@ async def process_first_name(message: Message, state: FSMContext):
@router.message(Form.last_name)
-async def process_last_name(message: Message, state: FSMContext):
-
+async def process_last_name(message: Message, state: FSMContext) -> None:
+ """Состояние: ввод фамилии."""
if not is_valid_name(message.text):
await message.answer(
"Фамилия должна содержать только буквы. Попробуйте снова."
@@ -82,7 +83,8 @@ async def process_last_name(message: Message, state: FSMContext):
@router.message(Form.middle_name)
-async def process_middle_name(message: Message, state: FSMContext):
+async def process_middle_name(message: Message, state: FSMContext) -> None:
+ """Состояние: ввод отчества."""
if message.text.lower() != "нет" and not is_valid_name(message.text):
await message.answer(
@@ -97,7 +99,8 @@ async def process_middle_name(message: Message, state: FSMContext):
@router.message(Form.phone_number)
-async def process_phone_number(message: Message, state: FSMContext):
+async def process_phone_number(message: Message, state: FSMContext) -> None:
+ """Состояние: ввод номера телефона."""
if not is_valid_phone_number(message.text):
await message.answer(
@@ -120,6 +123,8 @@ async def process_phone_number(message: Message, state: FSMContext):
f"Номер телефона: {user_data['phone_number']}"
)
+ # данные попадают в бд
+
keyboard = InlineKeyboardBuilder()
keyboard.add(InlineKeyboardButton(
text="Вернуться в главное меню", callback_data="back_to_main_menu"
From 94b3579388e5333a652e55c86d9c7c685a22c160 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Tue, 24 Sep 2024 21:11:04 +0300
Subject: [PATCH 08/75] fix dockstring in fsm_context.py
---
app/bot/fsm_context.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index 73bccd4..e2e120a 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -71,6 +71,7 @@ async def process_first_name(message: Message, state: FSMContext) -> None:
@router.message(Form.last_name)
async def process_last_name(message: Message, state: FSMContext) -> None:
"""Состояние: ввод фамилии."""
+
if not is_valid_name(message.text):
await message.answer(
"Фамилия должна содержать только буквы. Попробуйте снова."
From 8353c189d40e0685ee68cf6b352c98756025dc06 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Tue, 24 Sep 2024 21:49:17 +0300
Subject: [PATCH 09/75] fix collback
---
app/bot/callbacks.py | 2 +-
app/bot_setup.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/bot/callbacks.py b/app/bot/callbacks.py
index 0803b5a..a02a52f 100644
--- a/app/bot/callbacks.py
+++ b/app/bot/callbacks.py
@@ -64,7 +64,7 @@ async def callback_request(callback: CallbackQuery) -> None:
# ( * )
-@router.callback_data(F.data == 'back_to_products')
+@router.callback_query(F.data == 'back_to_products')
async def back_to_products(callback: CallbackQuery) -> None:
"""Возвращает к выюору продуктов."""
diff --git a/app/bot_setup.py b/app/bot_setup.py
index 964c9a6..f20ea7c 100644
--- a/app/bot_setup.py
+++ b/app/bot_setup.py
@@ -5,7 +5,7 @@
load_dotenv()
-bot = Bot(token=os.getenv('BOT_TOKEN'))
+bot = Bot(token=os.getenv('MY_TOKEN')) # использую своего бота
dispatcher = Dispatcher(storage=MemoryStorage())
From 10734419a77999662773f4fd275467c870e2336a Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Thu, 26 Sep 2024 19:51:15 +0300
Subject: [PATCH 10/75] architecture update, alembic configuration
---
.gitignore | 1 +
db/alembic.ini => alembic.ini | 4 +-
alembic/README | 1 +
{db/alembic => alembic}/env.py | 41 +++++----
{db/alembic => alembic}/script.py.mako | 0
.../versions/a204e877ef3b_first_migration.py | 40 +++++++++
app/bot/__init__.py | 0
app/{ => bot}/exceptions.py | 0
app/bot/fsm_context.py | 12 +++
app/bot/handlers.py | 6 +-
app/bot/validators.py | 1 +
app/core/__init__.py | 0
app/core/base.py | 3 +
app/{ => core}/bot_setup.py | 47 +++++-----
app/core/db.py | 37 ++++++++
app/main.py | 5 +-
app/models.py | 86 -------------------
app/models/__init__.py | 0
app/models/models.py | 51 +++++++++++
db/alembic/README | 1 -
poetry.lock | 40 ++++++++-
pyproject.toml | 4 +
22 files changed, 247 insertions(+), 133 deletions(-)
rename db/alembic.ini => alembic.ini (96%)
create mode 100644 alembic/README
rename {db/alembic => alembic}/env.py (70%)
rename {db/alembic => alembic}/script.py.mako (100%)
create mode 100644 alembic/versions/a204e877ef3b_first_migration.py
create mode 100644 app/bot/__init__.py
rename app/{ => bot}/exceptions.py (100%)
create mode 100644 app/core/__init__.py
create mode 100644 app/core/base.py
rename app/{ => core}/bot_setup.py (96%)
create mode 100644 app/core/db.py
delete mode 100644 app/models.py
create mode 100644 app/models/__init__.py
create mode 100644 app/models/models.py
delete mode 100644 db/alembic/README
diff --git a/.gitignore b/.gitignore
index eff8c02..c9b9398 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,6 +60,7 @@ cover/
local_settings.py
db.sqlite3
db.sqlite3-journal
+*.db
# Flask stuff:
instance/
diff --git a/db/alembic.ini b/alembic.ini
similarity index 96%
rename from db/alembic.ini
rename to alembic.ini
index 72cc699..d4ab1d7 100644
--- a/db/alembic.ini
+++ b/alembic.ini
@@ -1,14 +1,12 @@
# A generic, single database configuration.
[alembic]
-# path to migration scripts
+# path to migration scripts.
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
-# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
-# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
diff --git a/alembic/README b/alembic/README
new file mode 100644
index 0000000..e0d0858
--- /dev/null
+++ b/alembic/README
@@ -0,0 +1 @@
+Generic single-database configuration with an async dbapi.
\ No newline at end of file
diff --git a/db/alembic/env.py b/alembic/env.py
similarity index 70%
rename from db/alembic/env.py
rename to alembic/env.py
index 838502a..409cbc5 100644
--- a/db/alembic/env.py
+++ b/alembic/env.py
@@ -3,13 +3,16 @@
from logging.config import fileConfig
from dotenv import load_dotenv
-from sqlalchemy import engine_from_config, pool
-from sqlalchemy.ext.asyncio import AsyncEngine
+from sqlalchemy import pool
+from sqlalchemy.engine import Connection
+from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
-load_dotenv('.env')
+from app.core.base import Base
+
+load_dotenv('.env')
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
@@ -24,8 +27,7 @@
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
-target_metadata = None
-
+target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
@@ -56,26 +58,35 @@ def run_migrations_offline() -> None:
context.run_migrations()
-def run_migrations_online() -> None:
- """Run migrations in 'online' mode.
+def do_run_migrations(connection: Connection) -> None:
+ context.configure(connection=connection, target_metadata=target_metadata)
+
+ with context.begin_transaction():
+ context.run_migrations()
+
- In this scenario we need to create an Engine
+async def run_async_migrations() -> None:
+ """In this scenario we need to create an Engine
and associate a connection with the context.
"""
- connectable = engine_from_config(
+
+ connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
- with connectable.connect() as connection:
- context.configure(
- connection=connection, target_metadata=target_metadata
- )
+ async with connectable.connect() as connection:
+ await connection.run_sync(do_run_migrations)
+
+ await connectable.dispose()
+
+
+def run_migrations_online() -> None:
+ """Run migrations in 'online' mode."""
- with context.begin_transaction():
- context.run_migrations()
+ asyncio.run(run_async_migrations())
if context.is_offline_mode():
diff --git a/db/alembic/script.py.mako b/alembic/script.py.mako
similarity index 100%
rename from db/alembic/script.py.mako
rename to alembic/script.py.mako
diff --git a/alembic/versions/a204e877ef3b_first_migration.py b/alembic/versions/a204e877ef3b_first_migration.py
new file mode 100644
index 0000000..0500083
--- /dev/null
+++ b/alembic/versions/a204e877ef3b_first_migration.py
@@ -0,0 +1,40 @@
+"""First migration
+
+Revision ID: a204e877ef3b
+Revises:
+Create Date: 2024-09-26 19:43:49.889912
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision: str = 'a204e877ef3b'
+down_revision: Union[str, None] = None
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('users',
+ sa.Column('tg_id', sa.INTEGER(), nullable=False),
+ sa.Column('username', sa.VARCHAR(length=150), nullable=False),
+ sa.Column('full_name', sa.VARCHAR(length=150), nullable=False),
+ sa.Column('phone', sa.VARCHAR(length=25), nullable=False),
+ sa.Column('role', postgresql.ENUM('U', 'A', 'M'), nullable=False),
+ sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('tg_id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('users')
+ # ### end Alembic commands ###
diff --git a/app/bot/__init__.py b/app/bot/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/exceptions.py b/app/bot/exceptions.py
similarity index 100%
rename from app/exceptions.py
rename to app/bot/exceptions.py
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index 8aa3438..76fb7ec 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -45,15 +45,19 @@ async def contact_with_manager(message: Message, state: FSMContext) -> None:
'Пожалуйста, оставьте ваше имя и контактный номер, '
'и наш менеджер свяжется с вами.'
)
+
await ask_next_question(message, state, Form.first_name)
+
logger.info(
f"Пользователь {message.from_user.id} начал процесс."
)
+
except Exception as e:
logger.error(
f"Ошибка при выводе формы для пользователя "
f"{message.from_user.id}: {e}"
)
+
await message.answer("Произошла ошибка. Попробуйте снова.")
@@ -68,11 +72,13 @@ async def ask_next_question(
logger.info(
f"Переход к следующему вопросу: {next_state}"
)
+
except Exception as e:
logger.error(
f"Ошибка при переходе к следующему вопросу для пользователя "
f"{message.from_user.id}: {e}"
)
+
await message.answer("Произошла ошибка. Попробуйте снова.")
@@ -89,15 +95,19 @@ async def process_first_name(message: Message, state: FSMContext) -> None:
return
await state.update_data(first_name=message.text)
+
logger.info(
f"Пользователь {message.from_user.id} ввёл имя: {message.text}"
)
+
await ask_next_question(message, state, Form.last_name)
+
except Exception as e:
logger.error(
f"Ошибка при обработке имени пользователя "
f"{message.from_user.id}: {e}"
)
+
await message.answer("Произошла ошибка. Попробуйте снова.")
@@ -141,6 +151,7 @@ async def process_middle_name(message: Message, state: FSMContext) -> None:
"Отчество должно содержать только буквы или быть 'нет'. "
"Попробуйте снова."
)
+
await ask_next_question(message, state, Form.middle_name)
return
@@ -172,6 +183,7 @@ async def process_phone_number(message: Message, state: FSMContext) -> None:
"Номер телефона должен быть в формате +7XXXXXXXXXX "
"или 8XXXXXXXXXX. Попробуйте снова."
)
+
await ask_next_question(message, state, Form.phone_number)
return
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index ab3d91c..82376d5 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -40,7 +40,7 @@ async def cmd_start(message: Message) -> None:
)
-@router.message(F.text == 'Посмотреть портфолио')
+@router.message(F.text == 'Посмотреть портфолио.')
async def view_portfolio(message: Message) -> None:
"""Показ портфолио компании."""
@@ -64,7 +64,7 @@ async def view_portfolio(message: Message) -> None:
)
-@router.message(F.text == 'Получить информацию о компании')
+@router.message(F.text == 'Получить информацию о компании.')
async def company_info(message: Message) -> None:
"""Информация о компании."""
@@ -90,7 +90,7 @@ async def company_info(message: Message) -> None:
)
-@router.message(F.text == 'Узнать о продуктах и услугах')
+@router.message(F.text == 'Узнать о продуктах и услугах.')
async def products_services(message: Message) -> None:
"""Информация о продуктах и услугах."""
diff --git a/app/bot/validators.py b/app/bot/validators.py
index dc34d8b..38908fa 100644
--- a/app/bot/validators.py
+++ b/app/bot/validators.py
@@ -23,4 +23,5 @@ def format_phone_number(phone_number: str) -> str:
if phone_number.startswith('8'):
return '+7' + phone_number[1:]
+
return phone_number
diff --git a/app/core/__init__.py b/app/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/base.py b/app/core/base.py
new file mode 100644
index 0000000..b794672
--- /dev/null
+++ b/app/core/base.py
@@ -0,0 +1,3 @@
+"""Импорты класса Base и всех моделей для Alembic."""
+from app.core.db import Base # noqa
+from app.models import models # noqa
diff --git a/app/bot_setup.py b/app/core/bot_setup.py
similarity index 96%
rename from app/bot_setup.py
rename to app/core/bot_setup.py
index 933c68a..b692bc5 100644
--- a/app/bot_setup.py
+++ b/app/core/bot_setup.py
@@ -1,23 +1,24 @@
-import os
-import logging
-from aiogram import Bot, Dispatcher
-from aiogram.fsm.storage.memory import MemoryStorage
-from dotenv import load_dotenv
-
-load_dotenv()
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-bot = Bot(token=os.getenv('BOT_TOKEN'))
-dispatcher = Dispatcher(storage=MemoryStorage())
-
-
-def check_token() -> None:
- """Проверка наличия токена бота."""
-
- if os.getenv('BOT_TOKEN') is None:
- logger.error("Токен бота не найден.")
- raise ValueError('Отсутствуют необходимые токены.')
- else:
- logger.info("Токен бота успешно загружен.")
+import os
+import logging
+from aiogram import Bot, Dispatcher
+from aiogram.fsm.storage.memory import MemoryStorage
+from dotenv import load_dotenv
+
+load_dotenv()
+
+# Настройка логирования
+logger = logging.getLogger(__name__)
+
+bot = Bot(token=os.getenv('BOT_TOKEN'))
+dispatcher = Dispatcher(storage=MemoryStorage())
+
+
+def check_token() -> None:
+ """Проверка наличия токена бота."""
+
+ if os.getenv('BOT_TOKEN') is None:
+ logger.error("Токен бота не найден.")
+ raise ValueError('Отсутствуют необходимые токены.')
+
+ else:
+ logger.info("Токен бота успешно загружен.")
diff --git a/app/core/db.py b/app/core/db.py
new file mode 100644
index 0000000..6c650a4
--- /dev/null
+++ b/app/core/db.py
@@ -0,0 +1,37 @@
+from sqlalchemy import Integer
+from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
+from sqlalchemy.orm import (
+ declarative_base, declared_attr, sessionmaker, Mapped, mapped_column
+)
+
+from pydantic_settings import BaseSettings
+
+
+class Settings(BaseSettings):
+ database_url: str = 'sqlite+aiosqlite:///./dev.db'
+ bot_token: str
+
+ class Config:
+ env_file = '.env'
+
+
+settings = Settings()
+
+
+class PreBase:
+
+ @declared_attr
+ def __tablename__(cls):
+ return cls.__name__.lower()
+
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+
+
+Base = declarative_base(cls=PreBase)
+engine = create_async_engine(settings.database_url)
+AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession)
+
+
+async def get_async_session():
+ async with AsyncSessionLocal() as async_session:
+ yield async_session
diff --git a/app/main.py b/app/main.py
index de51cf2..fc795bd 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,7 +1,7 @@
import logging
import asyncio
-from bot_setup import bot, dispatcher, check_token
+from core.bot_setup import bot, dispatcher, check_token
from bot.handlers import router as message_router
from bot.callbacks import router as callback_router
from bot.fsm_context import router as fsm_context_router
@@ -36,6 +36,7 @@ async def main() -> None:
try:
logger.info("Запуск бота...")
await dispatcher.start_polling(bot)
+
except Exception as e:
logger.error(f"Критическая ошибка в работе бота: {e}")
@@ -43,7 +44,9 @@ async def main() -> None:
if __name__ == "__main__":
try:
asyncio.run(main())
+
except KeyboardInterrupt:
logger.info("Бот остановлен пользователем.")
+
except Exception as e:
logger.error(f"Произошла непредвиденная ошибка: {e}")
diff --git a/app/models.py b/app/models.py
deleted file mode 100644
index 10c1342..0000000
--- a/app/models.py
+++ /dev/null
@@ -1,86 +0,0 @@
-from datetime import datetime
-
-from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Integer, String,
- Text)
-
-from db.db_base import Base
-
-
-class User(Base):
- tg_id = Column(Integer, nullable=False)
- username = Column(String(150))
- full_name = Column(String(150))
- phone = Column(Integer)
- join_date = Column(DateTime(default=datetime.now))
-
-
-class CallRequest(Base):
- user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
- question_type = Column(String(150))
- arrival_time = Column(DateTime(default=datetime.now))
-
-
-class CompanyInfo(Base):
- info_type = Column(String(150), nullable=False)
- description = Column(Text)
- media = Column(String(150))
-
-
-class FAQ(Base):
- question = Column(Text, nullable=False)
- answer = Column(Text)
-
-
-class ProductProblems(Base):
- question = Column(Text, nullable=False)
- answer = Column(Text)
-
-
-class ProjectsInfo(Base):
- project_name = Column(String(150), nullable=False)
- project_url = Column(String(250))
- media = Column(String(150))
-
-
-class Websites(Base):
- website_type = Column(String(150), nullable=False)
- type_url = Column(String(250))
- description = Column(Text)
- media = Column(String(150))
-
-
-class Portals(Base):
- portal_type = Column(String(150), nullable=False)
- type_url = Column(String(250))
- description = Column(Text)
- media = Column(String(150))
-
-
-class MobileApps(Base):
- app_type = Column(String(150), nullable=False)
- type_url = Column(String(250))
- description = Column(Text)
- media = Column(String(150))
-
-
-class LoyaltyPrograms(Base):
- loyalty_type = Column(String(150), nullable=False)
- is_active = Column(Boolean, default=True)
- description = Column(Text)
-
-
-class Kiosk365(Base):
- info_type = Column(String(150), nullable=False)
- description = Column(Text)
- media = Column(String(150))
-
-
-class NBPEJA(Base):
- info_type = Column(String(150), nullable=False)
- description = Column(Text)
- media = Column(String(150))
-
-
-class Hosting(Base):
- info_type = Column(String(150), nullable=False)
- description = Column(Text)
diff --git a/app/models/__init__.py b/app/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/models.py b/app/models/models.py
new file mode 100644
index 0000000..50af12c
--- /dev/null
+++ b/app/models/models.py
@@ -0,0 +1,51 @@
+from datetime import datetime
+
+from sqlalchemy import ForeignKey
+from sqlalchemy.orm import Mapped, mapped_column
+from sqlalchemy.sql import func
+import sqlalchemy.dialects.postgresql as pgsql_types
+
+from enum import Enum
+
+from app.core.db import Base
+
+
+class RoleEnum(str, Enum):
+ USER = 'U'
+ ADMIN = 'A'
+ MANAGER = 'M'
+
+
+class User(Base):
+ """БД модель пользователя."""
+
+ __tablename__ = 'users'
+
+ tg_id: Mapped[int] = mapped_column(
+ pgsql_types.INTEGER,
+ nullable=False,
+ unique=True
+ )
+
+ username: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(150)
+ )
+
+ full_name: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(150)
+ )
+
+ phone: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(25)
+ )
+
+ role: Mapped[RoleEnum] = mapped_column(
+ pgsql_types.ENUM('U', 'A', 'M'),
+ default=RoleEnum.USER
+ )
+
+ join_date: Mapped[datetime] = mapped_column(
+ pgsql_types.TIMESTAMP(timezone=True),
+ server_default=func.now(),
+ nullable=False
+ )
diff --git a/db/alembic/README b/db/alembic/README
deleted file mode 100644
index 98e4f9c..0000000
--- a/db/alembic/README
+++ /dev/null
@@ -1 +0,0 @@
-Generic single-database configuration.
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 76fa211..055c575 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -177,6 +177,24 @@ files = [
[package.dependencies]
frozenlist = ">=1.1.0"
+[[package]]
+name = "aiosqlite"
+version = "0.20.0"
+description = "asyncio bridge to the standard sqlite3 module"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"},
+ {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"},
+]
+
+[package.dependencies]
+typing_extensions = ">=4.0"
+
+[package.extras]
+dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"]
+docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"]
+
[[package]]
name = "alembic"
version = "1.13.2"
@@ -750,6 +768,26 @@ files = [
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+[[package]]
+name = "pydantic-settings"
+version = "2.5.2"
+description = "Settings management using Pydantic"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907"},
+ {file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"},
+]
+
+[package.dependencies]
+pydantic = ">=2.7.0"
+python-dotenv = ">=0.21.0"
+
+[package.extras]
+azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
+toml = ["tomli (>=2.0.1)"]
+yaml = ["pyyaml (>=6.0.1)"]
+
[[package]]
name = "python-dotenv"
version = "1.0.1"
@@ -970,4 +1008,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
-content-hash = "969285a8ec2fbcf397bddc446022fc94009b95b770ed54cbd552da823ed9f4b2"
+content-hash = "7b57a1736b849752344d14e4a583255ec1bbc92ca77c761f55aac608cb922267"
diff --git a/pyproject.toml b/pyproject.toml
index ee6952b..c6cd441 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,6 +12,10 @@ pydantic = "^2.9.2"
python-dotenv = "^1.0.1"
alembic = "^1.13.2"
sqlalchemy = "^2.0.35"
+pydantic-settings = "^2.5.2"
+
+[tool.poetry.group.dev.dependencies]
+aiosqlite = "^0.20.0"
[build-system]
requires = ["poetry-core"]
From 12b2e47961d3e2e503af85030a907788b724aac4 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Thu, 26 Sep 2024 23:52:07 +0300
Subject: [PATCH 11/75] sqlite -> postgres
---
.../versions/a204e877ef3b_first_migration.py | 40 -----
.../versions/e15690914928_first_migration.py | 81 +++++++++++
app/core/db.py | 4 +-
app/models/models.py | 109 +++++++++++++-
poetry.lock | 137 +++++++++++++++++-
pyproject.toml | 2 +
6 files changed, 323 insertions(+), 50 deletions(-)
delete mode 100644 alembic/versions/a204e877ef3b_first_migration.py
create mode 100644 alembic/versions/e15690914928_first_migration.py
diff --git a/alembic/versions/a204e877ef3b_first_migration.py b/alembic/versions/a204e877ef3b_first_migration.py
deleted file mode 100644
index 0500083..0000000
--- a/alembic/versions/a204e877ef3b_first_migration.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""First migration
-
-Revision ID: a204e877ef3b
-Revises:
-Create Date: 2024-09-26 19:43:49.889912
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = 'a204e877ef3b'
-down_revision: Union[str, None] = None
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('users',
- sa.Column('tg_id', sa.INTEGER(), nullable=False),
- sa.Column('username', sa.VARCHAR(length=150), nullable=False),
- sa.Column('full_name', sa.VARCHAR(length=150), nullable=False),
- sa.Column('phone', sa.VARCHAR(length=25), nullable=False),
- sa.Column('role', postgresql.ENUM('U', 'A', 'M'), nullable=False),
- sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('tg_id')
- )
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('users')
- # ### end Alembic commands ###
diff --git a/alembic/versions/e15690914928_first_migration.py b/alembic/versions/e15690914928_first_migration.py
new file mode 100644
index 0000000..a4596b3
--- /dev/null
+++ b/alembic/versions/e15690914928_first_migration.py
@@ -0,0 +1,81 @@
+"""First migration
+
+Revision ID: e15690914928
+Revises:
+Create Date: 2024-09-26 23:48:22.126438
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision: str = 'e15690914928'
+down_revision: Union[str, None] = None
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('checkcompanyportfolio',
+ sa.Column('project_name', sa.VARCHAR(length=48), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('info',
+ sa.Column('question_type', postgresql.ENUM('TOPIC_1', 'TOPIC_2', 'TOPIC_3', name='question_enum'), nullable=False),
+ sa.Column('question', sa.TEXT(), nullable=False),
+ sa.Column('answer', sa.TEXT(), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('question')
+ )
+ op.create_table('informationaboutcompany',
+ sa.Column('name', sa.VARCHAR(length=48), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('productcategory',
+ sa.Column('title', sa.VARCHAR(length=150), nullable=False),
+ sa.Column('response', sa.TEXT(), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('user',
+ sa.Column('tg_id', sa.INTEGER(), nullable=False),
+ sa.Column('username', sa.VARCHAR(length=150), nullable=False),
+ sa.Column('phone', sa.VARCHAR(length=25), nullable=False),
+ sa.Column('role', postgresql.ENUM('USER', 'ADMIN', 'MANAGER', name='role_enum'), nullable=False),
+ sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('tg_id')
+ )
+ op.create_table('categorytype',
+ sa.Column('name', sa.VARCHAR(length=150), nullable=False),
+ sa.Column('product_id', sa.Integer(), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=64), nullable=False),
+ sa.Column('media', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_categorytype_product_id'), 'categorytype', ['product_id'], unique=False)
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f('ix_categorytype_product_id'), table_name='categorytype')
+ op.drop_table('categorytype')
+ op.drop_table('user')
+ op.drop_table('productcategory')
+ op.drop_table('informationaboutcompany')
+ op.drop_table('info')
+ op.drop_table('checkcompanyportfolio')
+ # ### end Alembic commands ###
diff --git a/app/core/db.py b/app/core/db.py
index 6c650a4..e6a31e7 100644
--- a/app/core/db.py
+++ b/app/core/db.py
@@ -7,8 +7,8 @@
from pydantic_settings import BaseSettings
-class Settings(BaseSettings):
- database_url: str = 'sqlite+aiosqlite:///./dev.db'
+class Settings(BaseSettings): # создать файл settings.py, перенести туда класс
+ database_url: str
bot_token: str
class Config:
diff --git a/app/models/models.py b/app/models/models.py
index 50af12c..f2b9aff 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -16,11 +16,15 @@ class RoleEnum(str, Enum):
MANAGER = 'M'
+class QuestionEnum(str, Enum):
+ TOPIC_1 = 'ANSWER_1'
+ TOPIC_2 = 'ANSWER_2'
+ TOPIC_3 = 'ANSWER_3'
+
+
class User(Base):
"""БД модель пользователя."""
- __tablename__ = 'users'
-
tg_id: Mapped[int] = mapped_column(
pgsql_types.INTEGER,
nullable=False,
@@ -31,17 +35,21 @@ class User(Base):
pgsql_types.VARCHAR(150)
)
- full_name: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(150)
- )
+ # full_name: Mapped[str] = mapped_column(
+ # pgsql_types.VARCHAR(150)
+ # )
phone: Mapped[str] = mapped_column(
pgsql_types.VARCHAR(25)
)
role: Mapped[RoleEnum] = mapped_column(
- pgsql_types.ENUM('U', 'A', 'M'),
- default=RoleEnum.USER
+ pgsql_types.ENUM(
+ RoleEnum,
+ name="role_enum",
+ create_type=False
+ ),
+ default=RoleEnum.USER,
)
join_date: Mapped[datetime] = mapped_column(
@@ -49,3 +57,90 @@ class User(Base):
server_default=func.now(),
nullable=False
)
+
+
+class ProductCategory(Base):
+ """БД модель о продуктах и услугах."""
+
+ title: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(150)
+ )
+
+ response: Mapped[str] = mapped_column(
+ pgsql_types.TEXT
+ )
+
+
+class CategoryType(Base):
+ """БД модель типов категорий."""
+
+ name: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(150),
+ nullable=False
+ )
+
+ product_id: Mapped[int] = mapped_column(
+ ForeignKey('productcategory.id'),
+ index=True
+ )
+
+ url: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(64)
+ )
+
+ media: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(128)
+ )
+
+
+class InformationAboutCompany(Base):
+ """Бд модель информации о компании."""
+
+ name: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(48),
+ nullable=False
+ )
+
+ url: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(128)
+ )
+
+
+class CheckCompanyPortfolio(Base):
+ """Бд модель информации о проектах."""
+
+ project_name: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(48),
+ nullable=False
+ )
+
+ url: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(128)
+ )
+
+
+class Info(Base):
+ """Бд модель F.A.Q."""
+
+ question_type: Mapped[QuestionEnum] = mapped_column(
+ pgsql_types.ENUM(
+ QuestionEnum,
+ name='question_enum',
+ create_type=False
+ ),
+ nullable=False
+ )
+
+ question: Mapped[str] = mapped_column(
+ pgsql_types.TEXT,
+ unique=True
+ )
+
+ answer: Mapped[str] = mapped_column(
+ pgsql_types.TEXT,
+ nullable=False
+ )
+
+ # media: Mapped[str] = mapped_column(
+ # pgsql_types.VARCHAR(256),
+ # )
diff --git a/poetry.lock b/poetry.lock
index 055c575..a680364 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -225,6 +225,60 @@ files = [
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
+[[package]]
+name = "asyncpg"
+version = "0.29.0"
+description = "An asyncio PostgreSQL driver"
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"},
+ {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"},
+ {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"},
+ {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"},
+ {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"},
+ {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"},
+ {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"},
+ {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"},
+ {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"},
+ {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"},
+ {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"},
+ {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"},
+ {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"},
+ {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"},
+ {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"},
+ {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"},
+ {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"},
+ {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"},
+ {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"},
+ {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"},
+ {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"},
+ {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"},
+ {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"},
+ {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"},
+ {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"},
+ {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"},
+ {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"},
+ {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"},
+ {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"},
+ {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"},
+ {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"},
+ {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"},
+ {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"},
+ {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"},
+ {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"},
+ {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"},
+ {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"},
+ {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"},
+ {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"},
+ {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"},
+ {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"},
+]
+
+[package.extras]
+docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
+test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
+
[[package]]
name = "attrs"
version = "24.2.0"
@@ -644,6 +698,87 @@ files = [
{file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"},
]
+[[package]]
+name = "psycopg2-binary"
+version = "2.9.9"
+description = "psycopg2 - Python-PostgreSQL Database Adapter"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"},
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"},
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"},
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"},
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
+]
+
[[package]]
name = "pydantic"
version = "2.9.2"
@@ -1008,4 +1143,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
-content-hash = "7b57a1736b849752344d14e4a583255ec1bbc92ca77c761f55aac608cb922267"
+content-hash = "90db1da4a85768319a24a7ef08be47bdc8d66328bc97ac041a05bc38eb1bea6c"
diff --git a/pyproject.toml b/pyproject.toml
index c6cd441..c79104c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,6 +13,8 @@ python-dotenv = "^1.0.1"
alembic = "^1.13.2"
sqlalchemy = "^2.0.35"
pydantic-settings = "^2.5.2"
+asyncpg = "^0.29.0"
+psycopg2-binary = "^2.9.9"
[tool.poetry.group.dev.dependencies]
aiosqlite = "^0.20.0"
From 5057284555d2b5990d2ac9e06a29ff098daa3239 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Fri, 27 Sep 2024 14:45:46 +0300
Subject: [PATCH 12/75] new migration, branch: contact manager ready
---
...fdfebab65_add_new_model_contact_manager.py | 37 +++++++++++++++++++
app/bot/fsm_context.py | 11 +++++-
app/core/db.py | 4 +-
app/crud/__init__.py | 0
app/crud/base_crud.py | 0
app/crud/request_to_manager.py | 17 +++++++++
app/models/models.py | 23 +++++++++++-
7 files changed, 88 insertions(+), 4 deletions(-)
create mode 100644 alembic/versions/389fdfebab65_add_new_model_contact_manager.py
create mode 100644 app/crud/__init__.py
create mode 100644 app/crud/base_crud.py
create mode 100644 app/crud/request_to_manager.py
diff --git a/alembic/versions/389fdfebab65_add_new_model_contact_manager.py b/alembic/versions/389fdfebab65_add_new_model_contact_manager.py
new file mode 100644
index 0000000..33f51b3
--- /dev/null
+++ b/alembic/versions/389fdfebab65_add_new_model_contact_manager.py
@@ -0,0 +1,37 @@
+"""Add new model: contact_manager
+
+Revision ID: 389fdfebab65
+Revises: e15690914928
+Create Date: 2024-09-27 13:45:33.565833
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '389fdfebab65'
+down_revision: Union[str, None] = 'e15690914928'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('contactmanager',
+ sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
+ sa.Column('last_name', sa.VARCHAR(length=32), nullable=False),
+ sa.Column('middle_name', sa.VARCHAR(length=32), nullable=False),
+ sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('contactmanager')
+ # ### end Alembic commands ###
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index 76fb7ec..f216a40 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -5,7 +5,7 @@
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
-
+from crud.request_to_manager import create_request_to_manager
from bot.validators import (
is_valid_name, is_valid_phone_number, format_phone_number
)
@@ -197,8 +197,15 @@ async def process_phone_number(message: Message, state: FSMContext) -> None:
)
user_data = await state.get_data()
+
+ new_request = await create_request_to_manager(user_data)
+
+ logger.info(f"Запись создана в БД с ID: {new_request.id}")
+
await message.answer(
- f"Форма заполнена!\n"
+ f'Спасибо! Наш менеджер свяжется '
+ f'с вами в ближайшее время.\n'
+ f"Отправленная форма:\n"
f"Имя: {user_data['first_name']}\n"
f"Фамилия: {user_data['last_name']}\n"
f"Отчество: {user_data['middle_name']}\n"
diff --git a/app/core/db.py b/app/core/db.py
index e6a31e7..2f9c330 100644
--- a/app/core/db.py
+++ b/app/core/db.py
@@ -3,6 +3,7 @@
from sqlalchemy.orm import (
declarative_base, declared_attr, sessionmaker, Mapped, mapped_column
)
+from contextlib import asynccontextmanager
from pydantic_settings import BaseSettings
@@ -32,6 +33,7 @@ def __tablename__(cls):
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession)
-async def get_async_session():
+@asynccontextmanager
+async def get_async_session() -> AsyncSession:
async with AsyncSessionLocal() as async_session:
yield async_session
diff --git a/app/crud/__init__.py b/app/crud/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/crud/base_crud.py b/app/crud/base_crud.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/crud/request_to_manager.py b/app/crud/request_to_manager.py
new file mode 100644
index 0000000..afdd980
--- /dev/null
+++ b/app/crud/request_to_manager.py
@@ -0,0 +1,17 @@
+from core.db import get_async_session
+from models.models import ContactManager
+
+
+async def create_request_to_manager(
+ user_data: dict
+):
+ """Создание заявки на связь с менеджером."""
+
+ async with get_async_session() as session:
+ data_to_db = ContactManager(**user_data)
+
+ session.add(data_to_db)
+ await session.commit()
+ await session.refresh(data_to_db)
+
+ return data_to_db
diff --git a/app/models/models.py b/app/models/models.py
index f2b9aff..ce3fb7f 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -7,7 +7,7 @@
from enum import Enum
-from app.core.db import Base
+from core.db import Base
class RoleEnum(str, Enum):
@@ -144,3 +144,24 @@ class Info(Base):
# media: Mapped[str] = mapped_column(
# pgsql_types.VARCHAR(256),
# )
+
+
+class ContactManager(Base):
+ """Бд модель для заявки к менеджеру."""
+
+ first_name: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(32),
+ nullable=False
+ )
+ last_name: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(32),
+ nullable=False
+ )
+ middle_name: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(32),
+ nullable=False
+ )
+ phone_number: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(25),
+ nullable=False
+ )
From 43b2a8aefe16a769368a810d804244279b5880f5 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Fri, 27 Sep 2024 15:41:02 +0300
Subject: [PATCH 13/75] branch: check portfolio done
---
app/bot/keyborads.py | 22 ++++++++++++++--------
app/crud/request_to_manager.py | 12 +++++++++++-
2 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index 0f55392..ac2dfb7 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -3,6 +3,8 @@
InlineKeyboardMarkup, InlineKeyboardButton
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
+from crud.request_to_manager import get_all_prtfolio_projects
+
# кнопку вернуться назад можно вынести отдельно, чтобы не дублировать код
@@ -12,7 +14,7 @@
'"НБП ЕЖА"', 'Хостинг',
] # моделирую результат запроса из бд ( * )
-LIST_OF_PROJECTS = ['Проект1', 'Проект2', 'Проект3'] # ( * )
+# LIST_OF_PROJECTS = ['Проект1', 'Проект2', 'Проект3'] # ( * )
main_keyboard = ReplyKeyboardMarkup(
keyboard=[
@@ -90,16 +92,20 @@ async def inline_products_and_services(): # тут будем брать дан
)
-async def list_of_projects_keyboard(): # данные будут в бд
- """Инлайн вывод проектов."""
+async def list_of_projects_keyboard():
+ """Инлайн вывод проектов с данными из БД."""
+
+ projects = await get_all_prtfolio_projects()
keyboard = InlineKeyboardBuilder()
- for project in LIST_OF_PROJECTS:
- keyboard.add(InlineKeyboardButton(
- text=project,
- url='https://github.com/' # тут будут ссылки, берем из бд
- ))
+ for project in projects:
+ keyboard.add(
+ InlineKeyboardButton(
+ text=project.project_name,
+ url=project.url
+ )
+ )
keyboard.add(
InlineKeyboardButton(
diff --git a/app/crud/request_to_manager.py b/app/crud/request_to_manager.py
index afdd980..b06dc5d 100644
--- a/app/crud/request_to_manager.py
+++ b/app/crud/request_to_manager.py
@@ -1,5 +1,6 @@
from core.db import get_async_session
-from models.models import ContactManager
+from models.models import ContactManager, CheckCompanyPortfolio
+from sqlalchemy import select
async def create_request_to_manager(
@@ -15,3 +16,12 @@ async def create_request_to_manager(
await session.refresh(data_to_db)
return data_to_db
+
+
+async def get_all_prtfolio_projects():
+ """Получение всех проектов-портфолио."""
+
+ async with get_async_session() as session:
+ result = await session.execute(select(CheckCompanyPortfolio))
+
+ return result.scalars().all()
From 891470cee079bd3b2e1510d61bef98be6d8434f2 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Tue, 1 Oct 2024 03:25:04 +0300
Subject: [PATCH 14/75] new feature
---
DockerFile | 28 ++++++++++++
.../0a73f874352d_update_user_model.py | 44 +++++++++++++++++++
app/bot/handlers.py | 23 +++++++++-
app/bot/keyborads.py | 5 +--
app/constants.py | 11 +++++
app/crud/request_to_manager.py | 40 +++++++++++++++--
app/models/models.py | 11 ++---
7 files changed, 148 insertions(+), 14 deletions(-)
create mode 100644 DockerFile
create mode 100644 alembic/versions/0a73f874352d_update_user_model.py
create mode 100644 app/constants.py
diff --git a/DockerFile b/DockerFile
new file mode 100644
index 0000000..498fa3f
--- /dev/null
+++ b/DockerFile
@@ -0,0 +1,28 @@
+FROM python:3.12.0-slim
+
+# Установка необходимых системных зависимостей (если нужно)
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ curl \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Установка Poetry
+RUN curl -sSL https://install.python-poetry.org | python3 -
+
+# Установка пути для Poetry
+ENV PATH="/root/.local/bin:$PATH"
+
+# Установка рабочего каталога
+WORKDIR /app
+
+# Копирование только pyproject.toml и poetry.lock (если есть) для кэширования зависимостей
+COPY pyproject.toml poetry.lock* ./
+
+# Установка зависимостей
+RUN poetry install --no-root
+
+# Копирование остальных файлов, включая alembic
+COPY . .
+
+# Команда для запуска приложения
+CMD ["poetry", "run", "python", "main.py"]
diff --git a/alembic/versions/0a73f874352d_update_user_model.py b/alembic/versions/0a73f874352d_update_user_model.py
new file mode 100644
index 0000000..75278f3
--- /dev/null
+++ b/alembic/versions/0a73f874352d_update_user_model.py
@@ -0,0 +1,44 @@
+"""Update user model
+
+Revision ID: 0a73f874352d
+Revises: 389fdfebab65
+Create Date: 2024-09-30 14:57:29.065680
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '0a73f874352d'
+down_revision: Union[str, None] = '389fdfebab65'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.alter_column('user', 'tg_id',
+ existing_type=sa.INTEGER(),
+ type_=sa.BIGINT(),
+ existing_nullable=False)
+ op.alter_column('user', 'phone',
+ existing_type=sa.VARCHAR(length=25),
+ nullable=True)
+ op.drop_column('user', 'username')
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('user', sa.Column('username', sa.VARCHAR(length=150), autoincrement=False, nullable=False))
+ op.alter_column('user', 'phone',
+ existing_type=sa.VARCHAR(length=25),
+ nullable=False)
+ op.alter_column('user', 'tg_id',
+ existing_type=sa.BIGINT(),
+ type_=sa.INTEGER(),
+ existing_nullable=False)
+ # ### end Alembic commands ###
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index 82376d5..fe4fdaa 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -1,9 +1,10 @@
import logging
from aiogram import F, Router
-from aiogram.filters import CommandStart
+from aiogram.filters import CommandStart, Command
from aiogram.types import Message
-
+from models.models import RoleEnum
+from crud.request_to_manager import create_user_id, get_role_by_tg_id
from bot.keyborads import (
main_keyboard, company_information_keyboard,
inline_products_and_services, company_portfolio_choice,
@@ -14,10 +15,28 @@
logger = logging.getLogger(__name__)
+# TODO: данные из message нужно достать один раз,
+# сейчас в каждой фукнции дергаем
+
+@router.message(Command('admin'))
+async def cmd_admin(message: Message) -> None:
+ """Вход в админку."""
+
+ role = await get_role_by_tg_id(message.from_user.id)
+
+ response = 'Добро пожаловать в админку' if role in (
+ RoleEnum.ADMIN, RoleEnum.MANAGER
+ ) else '403: Forbidden'
+
+ await message.answer(response)
+
+
@router.message(CommandStart())
async def cmd_start(message: Message) -> None:
"""Выводит приветствие пользователя."""
+ await create_user_id(message.from_user.id)
+
try:
await message.answer(
'Здравстуйте! Я ваш виртуальный помощник. '
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index ac2dfb7..de584f0 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -5,6 +5,7 @@
from aiogram.utils.keyboard import InlineKeyboardBuilder
from crud.request_to_manager import get_all_prtfolio_projects
+from models.models import CheckCompanyPortfolio
# кнопку вернуться назад можно вынести отдельно, чтобы не дублировать код
@@ -14,8 +15,6 @@
'"НБП ЕЖА"', 'Хостинг',
] # моделирую результат запроса из бд ( * )
-# LIST_OF_PROJECTS = ['Проект1', 'Проект2', 'Проект3'] # ( * )
-
main_keyboard = ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text='Посмотреть портфолио.')],
@@ -95,7 +94,7 @@ async def inline_products_and_services(): # тут будем брать дан
async def list_of_projects_keyboard():
"""Инлайн вывод проектов с данными из БД."""
- projects = await get_all_prtfolio_projects()
+ projects = await get_all_prtfolio_projects(CheckCompanyPortfolio)
keyboard = InlineKeyboardBuilder()
diff --git a/app/constants.py b/app/constants.py
new file mode 100644
index 0000000..6e28009
--- /dev/null
+++ b/app/constants.py
@@ -0,0 +1,11 @@
+'''
+Скрипт для тестов:
+
+INSERT INTO ProductCategory (title, response) VALUES
+ ('Разработка сайтов', 'Текст для разработки сайтов'),
+ ('Создание порталов', 'Текст для создания порталов'),
+ ('Разработка мобильных приложений', 'Текст для мобильных приложений'),
+ ('Консультация по КИОСК365', 'Текст для консультации по КИОСК365'),
+ ('"НБП ЕЖА"', 'Текст для НБП ЕЖА'),
+ ('Хостинг', 'Текст для хостинга');
+'''
diff --git a/app/crud/request_to_manager.py b/app/crud/request_to_manager.py
index b06dc5d..b9be679 100644
--- a/app/crud/request_to_manager.py
+++ b/app/crud/request_to_manager.py
@@ -1,11 +1,13 @@
from core.db import get_async_session
-from models.models import ContactManager, CheckCompanyPortfolio
+from models.models import (
+ ContactManager, CheckCompanyPortfolio, User, ProductCategory
+)
from sqlalchemy import select
async def create_request_to_manager(
user_data: dict
-):
+) -> ContactManager:
"""Создание заявки на связь с менеджером."""
async with get_async_session() as session:
@@ -18,10 +20,40 @@ async def create_request_to_manager(
return data_to_db
-async def get_all_prtfolio_projects():
+async def get_all_prtfolio_projects(
+ object_model: CheckCompanyPortfolio | ProductCategory
+) -> list[CheckCompanyPortfolio | ProductCategory]:
"""Получение всех проектов-портфолио."""
async with get_async_session() as session:
- result = await session.execute(select(CheckCompanyPortfolio))
+ result = await session.execute(select(object_model))
return result.scalars().all()
+
+
+async def create_user_id(tg_id: int) -> User:
+ """Запись tg_id в таблицу user."""
+
+ async with get_async_session() as session:
+
+ data_to_db = User(tg_id=tg_id)
+
+ session.add(data_to_db)
+ await session.commit()
+ await session.refresh(data_to_db)
+
+ return data_to_db
+
+
+async def get_role_by_tg_id(tg_id: int) -> User:
+ """Получаем роль пользователя по его tg_id."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(User.role).where(User.tg_id == tg_id)
+ )
+
+ role = result.scalar()
+
+ return role
diff --git a/app/models/models.py b/app/models/models.py
index ce3fb7f..8066640 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -26,21 +26,22 @@ class User(Base):
"""БД модель пользователя."""
tg_id: Mapped[int] = mapped_column(
- pgsql_types.INTEGER,
+ pgsql_types.BIGINT,
nullable=False,
unique=True
)
- username: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(150)
- )
+ # username: Mapped[str] = mapped_column(
+ # pgsql_types.VARCHAR(150)
+ # )
# full_name: Mapped[str] = mapped_column(
# pgsql_types.VARCHAR(150)
# )
phone: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(25)
+ pgsql_types.VARCHAR(25),
+ nullable=True
)
role: Mapped[RoleEnum] = mapped_column(
From c33cc60efeb3a3726e5529e4a2e489a88d7a0f6c Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Wed, 2 Oct 2024 18:30:02 +0300
Subject: [PATCH 15/75] write branch: get support
---
alembic/versions/3aef62a6d6a1_fix.py | 37 +++++++++++
.../da985c622924_update_question_enum.py | 30 +++++++++
app/bot/callbacks.py | 66 +++++++++++++++----
app/bot/handlers.py | 17 ++++-
app/bot/keyborads.py | 33 ++++++++--
app/constants.py | 12 +++-
app/crud/request_to_manager.py | 41 +++++++++++-
app/models/models.py | 5 +-
8 files changed, 214 insertions(+), 27 deletions(-)
create mode 100644 alembic/versions/3aef62a6d6a1_fix.py
create mode 100644 alembic/versions/da985c622924_update_question_enum.py
diff --git a/alembic/versions/3aef62a6d6a1_fix.py b/alembic/versions/3aef62a6d6a1_fix.py
new file mode 100644
index 0000000..d6df5e7
--- /dev/null
+++ b/alembic/versions/3aef62a6d6a1_fix.py
@@ -0,0 +1,37 @@
+"""Fix
+
+Revision ID: 3aef62a6d6a1
+Revises: da985c622924
+Create Date: 2024-10-01 22:55:56.943639
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision: str = '3aef62a6d6a1'
+down_revision: Union[str, None] = 'da985c622924'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('info',
+ sa.Column('question_type', postgresql.ENUM('GENERAL_QUESTIONS', 'PROBLEMS_WITH_PRODUCTS', name='question_enum'), nullable=False),
+ sa.Column('question', sa.TEXT(), nullable=False),
+ sa.Column('answer', sa.TEXT(), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('question')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('info')
+ # ### end Alembic commands ###
diff --git a/alembic/versions/da985c622924_update_question_enum.py b/alembic/versions/da985c622924_update_question_enum.py
new file mode 100644
index 0000000..53b75f4
--- /dev/null
+++ b/alembic/versions/da985c622924_update_question_enum.py
@@ -0,0 +1,30 @@
+"""Update question enum
+
+Revision ID: da985c622924
+Revises: 0a73f874352d
+Create Date: 2024-10-01 22:27:23.413080
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'da985c622924'
+down_revision: Union[str, None] = '0a73f874352d'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
diff --git a/app/bot/callbacks.py b/app/bot/callbacks.py
index a02a52f..00fab7a 100644
--- a/app/bot/callbacks.py
+++ b/app/bot/callbacks.py
@@ -2,8 +2,14 @@
from aiogram.types import CallbackQuery
from bot.keyborads import (
- list_of_projects_keyboard, main_keyboard
+ list_of_projects_keyboard, main_keyboard,
+ faq_or_problems_with_products_inline_keyboard
)
+from crud.request_to_manager import (
+ get_question_by_id, response_text_by_id,
+ get_question_by_title
+)
+
router = Router()
@@ -40,20 +46,40 @@ async def previous_choice(callback: CallbackQuery) -> None:
# ( * )
-@router.callback_query(F.data == 'get_faq')
-async def get_faq(callback: CallbackQuery) -> None:
- """Инлайн вывод ответов на часто задаваемые вопросы."""
+@router.callback_query(F.data.in_(['get_faq', 'get_problems_with_products']))
+async def get_questions(callback: CallbackQuery) -> None:
+ """Инлайн вывод общих вопросов и проблем с продуктами."""
- pass
- # ( * ) кнопка с вопросами - ответ отдельным сообщением
+ await callback.answer()
+ question_type = (
+ 'GENERAL_QUESTIONS' if callback.data == 'get_faq'
+ else 'PROBLEMS_WITH_PRODUCTS'
+ )
-@router.callback_query(F.data == 'get_problems_with_products')
-async def get_problems_with_products(callback: CallbackQuery) -> None:
- """Инлайн вывод проблем с продуктами."""
+ # Получаем клавиатуру с вопросами
+ keyboard = await faq_or_problems_with_products_inline_keyboard(question_type)
- pass
- # ( * ) кнопка с вопросами - ответ отдельным сообщением
+ if callback.message:
+ await callback.message.answer(
+ "Выберите вопрос:",
+ reply_markup=keyboard
+ )
+
+
+@router.callback_query(F.data.startswith('answer:'))
+async def get_faq_answer(callback: CallbackQuery) -> None:
+ """Вывод ответа на выбранный вопрос."""
+
+ await callback.answer()
+
+ question_id = int(callback.data.split(':')[1])
+ question = await get_question_by_id(question_id)
+
+ if question:
+ await callback.message.answer(f"{question.answer}")
+ else:
+ await callback.message.answer("Вопрос не найден.")
@router.callback_query(F.data == 'callback_request')
@@ -61,11 +87,27 @@ async def callback_request(callback: CallbackQuery) -> None:
"""Инлайн вывод запроса на обратный звонок."""
pass
+
# ( * )
@router.callback_query(F.data == 'back_to_products')
async def back_to_products(callback: CallbackQuery) -> None:
- """Возвращает к выюору продуктов."""
+ """Возвращает к выбору продуктов."""
pass
+
+
+@router.callback_query(F.data.startswith('category_'))
+async def get_response_by_title(callback: CallbackQuery) -> None:
+ """Возвращает заготовленный ответ на выбранную категорию."""
+
+ await callback.answer()
+
+ if callback.message:
+
+ category_id = int(callback.data.split('_')[1])
+
+ await callback.message.answer(
+ await response_text_by_id(category_id)
+ )
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index fe4fdaa..cc87348 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -8,6 +8,7 @@
from bot.keyborads import (
main_keyboard, company_information_keyboard,
inline_products_and_services, company_portfolio_choice,
+ support_keyboard
)
router = Router()
@@ -109,13 +110,27 @@ async def company_info(message: Message) -> None:
)
+@router.message(F.text == 'Получить техническую поддержку.')
+async def get_support(message: Message) -> None:
+ """Выводит виды тех. поддержки."""
+
+ await message.answer(
+ 'Какой вид технической поддержки '
+ 'вам нужен? ',
+ reply_markup=support_keyboard
+ )
+
+
@router.message(F.text == 'Узнать о продуктах и услугах.')
async def products_services(message: Message) -> None:
"""Информация о продуктах и услугах."""
try:
await message.answer(
- 'Вот наши продукты и услуги:',
+ (
+ 'Мы предлагаем следуюющие продукты и услуги. '
+ 'Какой тз них вас интересеует?'
+ ),
reply_markup=await inline_products_and_services()
)
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index de584f0..8feb9bf 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -3,9 +3,9 @@
InlineKeyboardMarkup, InlineKeyboardButton
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from crud.request_to_manager import get_all_prtfolio_projects
+from crud.request_to_manager import get_all_prtfolio_projects, get_question_by_title
-from models.models import CheckCompanyPortfolio
+from models.models import CheckCompanyPortfolio, ProductCategory
# кнопку вернуться назад можно вынести отдельно, чтобы не дублировать код
@@ -52,15 +52,17 @@
)
-async def inline_products_and_services(): # тут будем брать данные из бд
- """Инлайн клавиатура для продуктов и услуг.""" # аннатацию тоже не пишу пока
+async def inline_products_and_services():
+ """Инлайн клавиатура для продуктов и услуг."""
keyboard = InlineKeyboardBuilder()
- for index, product_and_service in enumerate(PRODUCTS_AND_SERVICES):
+ objects_in_db = await get_all_prtfolio_projects(ProductCategory)
+
+ for obj in objects_in_db:
keyboard.add(InlineKeyboardButton(
- text=product_and_service,
- callback_data=f'service_{index}'
+ text=obj.title,
+ callback_data=f'category_{obj.id}'
))
keyboard.add(
@@ -144,3 +146,20 @@ async def list_of_projects_keyboard():
]
]
)
+
+
+async def faq_or_problems_with_products_inline_keyboard(question_type) -> InlineKeyboardMarkup:
+ """Создание инлайн-клавиатуры для частозадаваемых вопросов или проблем с продуктами."""
+
+ questions = await get_question_by_title(question_type)
+
+ keyboard = InlineKeyboardBuilder()
+ for question in questions:
+ keyboard.add(
+ InlineKeyboardButton(
+ text=question.question,
+ callback_data=f"answer:{question.id}"
+ )
+ )
+
+ return keyboard.adjust(1).as_markup()
diff --git a/app/constants.py b/app/constants.py
index 6e28009..8b55036 100644
--- a/app/constants.py
+++ b/app/constants.py
@@ -1,5 +1,5 @@
'''
-Скрипт для тестов:
+Скрипт для ветки узнать о продуктах и услугах:
INSERT INTO ProductCategory (title, response) VALUES
('Разработка сайтов', 'Текст для разработки сайтов'),
@@ -9,3 +9,13 @@
('"НБП ЕЖА"', 'Текст для НБП ЕЖА'),
('Хостинг', 'Текст для хостинга');
'''
+
+'''
+Скрипт для тестовых данных в ветку получить техю поддержку
+
+INSERT INTO Info (question_type, question, answer) VALUES
+('GENERAL_QUESTIONS', 'Какие способы оплаты доступны?', 'Мы принимаем кредитные карты, PayPal и банковские переводы.'),
+('GENERAL_QUESTIONS', 'Как я могу связаться с поддержкой?', 'Вы можете связаться с поддержкой через нашу форму обратной связи или по телефону.'),
+('PROBLEMS_WITH_PRODUCTS', 'Что делать, если продукт неисправен?', 'Если продукт неисправен, пожалуйста, свяжитесь с поддержкой, и мы организуем замену или возврат.'),
+('PROBLEMS_WITH_PRODUCTS', 'Почему мой продукт не включается?', 'Убедитесь, что устройство заряжено, и проверьте кнопку включения.');
+'''
diff --git a/app/crud/request_to_manager.py b/app/crud/request_to_manager.py
index b9be679..383efae 100644
--- a/app/crud/request_to_manager.py
+++ b/app/crud/request_to_manager.py
@@ -1,6 +1,7 @@
from core.db import get_async_session
from models.models import (
- ContactManager, CheckCompanyPortfolio, User, ProductCategory
+ ContactManager, CheckCompanyPortfolio, User, ProductCategory,
+ Info
)
from sqlalchemy import select
@@ -54,6 +55,40 @@ async def get_role_by_tg_id(tg_id: int) -> User:
select(User.role).where(User.tg_id == tg_id)
)
- role = result.scalar()
+ return result.scalar()
- return role
+
+async def response_text_by_id(id: int) -> str:
+ """Возвращает ответ на выбранную категорию."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(ProductCategory.response).where(ProductCategory.id == id)
+ )
+
+ return result.scalar()
+
+
+async def get_question_by_title(question_type) -> list[Info]:
+ """Получаем все вопросы по категории."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(Info).where(Info.question_type == question_type)
+ )
+
+ return result.scalars().all()
+
+
+async def get_question_by_id(question_id: int) -> Info:
+ """Получает вопрос по его ID."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(Info).where(Info.id == question_id)
+ )
+
+ return result.scalar_one_or_none()
diff --git a/app/models/models.py b/app/models/models.py
index 8066640..71c8326 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -17,9 +17,8 @@ class RoleEnum(str, Enum):
class QuestionEnum(str, Enum):
- TOPIC_1 = 'ANSWER_1'
- TOPIC_2 = 'ANSWER_2'
- TOPIC_3 = 'ANSWER_3'
+ GENERAL_QUESTIONS = 'Общие вопросы'
+ PROBLEMS_WITH_PRODUCTS = 'Проблемы с продуктами'
class User(Base):
From 99c7a24b67e28676d25f9b5a22faf08284cef284 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Fri, 4 Oct 2024 02:06:10 +0300
Subject: [PATCH 16/75] rewrite fsm, keyboards, models, add callbacks,
middleware, request to db, create settings
---
alembic/README | 10 +-
alembic/env.py | 14 +-
.../0a73f874352d_update_user_model.py | 44 ------
...n.py => 182d5908e82a_update_user_model.py} | 30 ++--
...fdfebab65_add_new_model_contact_manager.py | 37 -----
alembic/versions/3aef62a6d6a1_fix.py | 37 -----
.../da985c622924_update_question_enum.py | 30 ----
app/{bot/admin.py => admin/__init__.py} | 0
app/bot/callbacks.py | 147 ++++++++++++++++--
app/bot/fsm_context.py | 101 ++----------
app/bot/handlers.py | 103 +-----------
app/bot/keyborads.py | 138 +++++++++-------
app/core/db.py | 13 +-
app/core/settings.py | 12 ++
app/crud/request_to_manager.py | 56 ++++++-
app/middlewares/__init__.py | 0
app/middlewares/middleware.py | 21 +++
app/models/models.py | 51 +++---
app/{constants.py => scripts_for_db.py} | 13 ++
19 files changed, 392 insertions(+), 465 deletions(-)
delete mode 100644 alembic/versions/0a73f874352d_update_user_model.py
rename alembic/versions/{e15690914928_first_migration.py => 182d5908e82a_update_user_model.py} (70%)
delete mode 100644 alembic/versions/389fdfebab65_add_new_model_contact_manager.py
delete mode 100644 alembic/versions/3aef62a6d6a1_fix.py
delete mode 100644 alembic/versions/da985c622924_update_question_enum.py
rename app/{bot/admin.py => admin/__init__.py} (100%)
create mode 100644 app/core/settings.py
create mode 100644 app/middlewares/__init__.py
create mode 100644 app/middlewares/middleware.py
rename app/{constants.py => scripts_for_db.py} (71%)
diff --git a/alembic/README b/alembic/README
index e0d0858..9b8ca67 100644
--- a/alembic/README
+++ b/alembic/README
@@ -1 +1,9 @@
-Generic single-database configuration with an async dbapi.
\ No newline at end of file
+Примените миграции
+```bash
+alembic upgrade head
+```
+
+Создание миграций, если внесены изменения в /models
+```bash
+alembic revision --autogenerate -m "Your commit"
+```
\ No newline at end of file
diff --git a/alembic/env.py b/alembic/env.py
index 409cbc5..c557683 100644
--- a/alembic/env.py
+++ b/alembic/env.py
@@ -13,25 +13,15 @@
from app.core.base import Base
load_dotenv('.env')
-# this is the Alembic Config object, which provides
-# access to the values within the .ini file in use.
+
config = context.config
config.set_main_option('sqlalchemy.url', os.environ['DATABASE_URL'])
-# Interpret the config file for Python logging.
-# This line sets up loggers basically.
+
if config.config_file_name is not None:
fileConfig(config.config_file_name)
-# add your model's MetaData object here
-# for 'autogenerate' support
-# from myapp import mymodel
-# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
-# other values from the config, defined by the needs of env.py,
-# can be acquired:
-# my_important_option = config.get_main_option("my_important_option")
-# ... etc.
def run_migrations_offline() -> None:
diff --git a/alembic/versions/0a73f874352d_update_user_model.py b/alembic/versions/0a73f874352d_update_user_model.py
deleted file mode 100644
index 75278f3..0000000
--- a/alembic/versions/0a73f874352d_update_user_model.py
+++ /dev/null
@@ -1,44 +0,0 @@
-"""Update user model
-
-Revision ID: 0a73f874352d
-Revises: 389fdfebab65
-Create Date: 2024-09-30 14:57:29.065680
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision: str = '0a73f874352d'
-down_revision: Union[str, None] = '389fdfebab65'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.alter_column('user', 'tg_id',
- existing_type=sa.INTEGER(),
- type_=sa.BIGINT(),
- existing_nullable=False)
- op.alter_column('user', 'phone',
- existing_type=sa.VARCHAR(length=25),
- nullable=True)
- op.drop_column('user', 'username')
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.add_column('user', sa.Column('username', sa.VARCHAR(length=150), autoincrement=False, nullable=False))
- op.alter_column('user', 'phone',
- existing_type=sa.VARCHAR(length=25),
- nullable=False)
- op.alter_column('user', 'tg_id',
- existing_type=sa.BIGINT(),
- type_=sa.INTEGER(),
- existing_nullable=False)
- # ### end Alembic commands ###
diff --git a/alembic/versions/e15690914928_first_migration.py b/alembic/versions/182d5908e82a_update_user_model.py
similarity index 70%
rename from alembic/versions/e15690914928_first_migration.py
rename to alembic/versions/182d5908e82a_update_user_model.py
index a4596b3..836cd07 100644
--- a/alembic/versions/e15690914928_first_migration.py
+++ b/alembic/versions/182d5908e82a_update_user_model.py
@@ -1,8 +1,8 @@
-"""First migration
+"""Update user model
-Revision ID: e15690914928
+Revision ID: 182d5908e82a
Revises:
-Create Date: 2024-09-26 23:48:22.126438
+Create Date: 2024-10-04 01:00:56.159813
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
-revision: str = 'e15690914928'
+revision: str = '182d5908e82a'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -26,8 +26,14 @@ def upgrade() -> None:
sa.Column('id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
+ op.create_table('contactmanager',
+ sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
+ sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
op.create_table('info',
- sa.Column('question_type', postgresql.ENUM('TOPIC_1', 'TOPIC_2', 'TOPIC_3', name='question_enum'), nullable=False),
+ sa.Column('question_type', postgresql.ENUM('GENERAL_QUESTIONS', 'PROBLEMS_WITH_PRODUCTS', name='question_enum'), nullable=False),
sa.Column('question', sa.TEXT(), nullable=False),
sa.Column('answer', sa.TEXT(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
@@ -47,11 +53,14 @@ def upgrade() -> None:
sa.PrimaryKeyConstraint('id')
)
op.create_table('user',
- sa.Column('tg_id', sa.INTEGER(), nullable=False),
- sa.Column('username', sa.VARCHAR(length=150), nullable=False),
- sa.Column('phone', sa.VARCHAR(length=25), nullable=False),
+ sa.Column('tg_id', sa.BIGINT(), nullable=False),
+ sa.Column('name', sa.VARCHAR(length=32), nullable=True),
+ sa.Column('phone', sa.VARCHAR(length=25), nullable=True),
+ sa.Column('need_support', sa.BOOLEAN(), nullable=False),
+ sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
sa.Column('role', postgresql.ENUM('USER', 'ADMIN', 'MANAGER', name='role_enum'), nullable=False),
sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('tg_id')
@@ -59,8 +68,8 @@ def upgrade() -> None:
op.create_table('categorytype',
sa.Column('name', sa.VARCHAR(length=150), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
- sa.Column('url', sa.VARCHAR(length=64), nullable=False),
- sa.Column('media', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('media', sa.VARCHAR(length=128), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ),
sa.PrimaryKeyConstraint('id')
@@ -77,5 +86,6 @@ def downgrade() -> None:
op.drop_table('productcategory')
op.drop_table('informationaboutcompany')
op.drop_table('info')
+ op.drop_table('contactmanager')
op.drop_table('checkcompanyportfolio')
# ### end Alembic commands ###
diff --git a/alembic/versions/389fdfebab65_add_new_model_contact_manager.py b/alembic/versions/389fdfebab65_add_new_model_contact_manager.py
deleted file mode 100644
index 33f51b3..0000000
--- a/alembic/versions/389fdfebab65_add_new_model_contact_manager.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""Add new model: contact_manager
-
-Revision ID: 389fdfebab65
-Revises: e15690914928
-Create Date: 2024-09-27 13:45:33.565833
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision: str = '389fdfebab65'
-down_revision: Union[str, None] = 'e15690914928'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('contactmanager',
- sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
- sa.Column('last_name', sa.VARCHAR(length=32), nullable=False),
- sa.Column('middle_name', sa.VARCHAR(length=32), nullable=False),
- sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('contactmanager')
- # ### end Alembic commands ###
diff --git a/alembic/versions/3aef62a6d6a1_fix.py b/alembic/versions/3aef62a6d6a1_fix.py
deleted file mode 100644
index d6df5e7..0000000
--- a/alembic/versions/3aef62a6d6a1_fix.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""Fix
-
-Revision ID: 3aef62a6d6a1
-Revises: da985c622924
-Create Date: 2024-10-01 22:55:56.943639
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = '3aef62a6d6a1'
-down_revision: Union[str, None] = 'da985c622924'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('info',
- sa.Column('question_type', postgresql.ENUM('GENERAL_QUESTIONS', 'PROBLEMS_WITH_PRODUCTS', name='question_enum'), nullable=False),
- sa.Column('question', sa.TEXT(), nullable=False),
- sa.Column('answer', sa.TEXT(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('question')
- )
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('info')
- # ### end Alembic commands ###
diff --git a/alembic/versions/da985c622924_update_question_enum.py b/alembic/versions/da985c622924_update_question_enum.py
deleted file mode 100644
index 53b75f4..0000000
--- a/alembic/versions/da985c622924_update_question_enum.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Update question enum
-
-Revision ID: da985c622924
-Revises: 0a73f874352d
-Create Date: 2024-10-01 22:27:23.413080
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision: str = 'da985c622924'
-down_revision: Union[str, None] = '0a73f874352d'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- pass
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- pass
- # ### end Alembic commands ###
diff --git a/app/bot/admin.py b/app/admin/__init__.py
similarity index 100%
rename from app/bot/admin.py
rename to app/admin/__init__.py
diff --git a/app/bot/callbacks.py b/app/bot/callbacks.py
index 00fab7a..d6bb538 100644
--- a/app/bot/callbacks.py
+++ b/app/bot/callbacks.py
@@ -1,18 +1,25 @@
+import logging
+
from aiogram import F, Router
from aiogram.types import CallbackQuery
+from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.keyborads import (
list_of_projects_keyboard, main_keyboard,
- faq_or_problems_with_products_inline_keyboard
+ faq_or_problems_with_products_inline_keyboard,
+ category_type_inline_keyboard, inline_products_and_services,
+ company_information_keyboard, company_portfolio_choice,
+ support_keyboard, back_to_main_menu
)
from crud.request_to_manager import (
- get_question_by_id, response_text_by_id,
- get_question_by_title
+ get_question_by_id, response_text_by_id, get_title_by_id
)
router = Router()
+logger = logging.getLogger(__name__)
+
@router.callback_query(F.data == 'show_projects')
async def show_projects(callback: CallbackQuery):
@@ -21,7 +28,7 @@ async def show_projects(callback: CallbackQuery):
await callback.answer()
if callback.message:
- await callback.message.answer(
+ await callback.message.edit_text(
'Вот некоторые из наших проектов. '
'Выберите, чтобы узнать больше о каждом из них: ',
reply_markup=await list_of_projects_keyboard()
@@ -38,7 +45,7 @@ async def previous_choice(callback: CallbackQuery) -> None:
await callback.answer()
if callback.message:
- await callback.message.answer(
+ await callback.message.edit_text(
'Вы вернулись в оснвное меню. '
'Как я могу помочь вам дальше? ',
reply_markup=main_keyboard
@@ -57,13 +64,12 @@ async def get_questions(callback: CallbackQuery) -> None:
else 'PROBLEMS_WITH_PRODUCTS'
)
- # Получаем клавиатуру с вопросами
- keyboard = await faq_or_problems_with_products_inline_keyboard(question_type)
-
if callback.message:
- await callback.message.answer(
+ await callback.message.edit_text(
"Выберите вопрос:",
- reply_markup=keyboard
+ reply_markup=await faq_or_problems_with_products_inline_keyboard(
+ question_type
+ )
)
@@ -73,13 +79,18 @@ async def get_faq_answer(callback: CallbackQuery) -> None:
await callback.answer()
- question_id = int(callback.data.split(':')[1])
- question = await get_question_by_id(question_id)
+ question = await get_question_by_id(callback.data.split(':')[1])
if question:
- await callback.message.answer(f"{question.answer}")
+ await callback.message.edit_text(
+ text=f"{question.answer}",
+ reply_markup=InlineKeyboardBuilder().add(
+ back_to_main_menu
+ ).as_markup()
+ )
+
else:
- await callback.message.answer("Вопрос не найден.")
+ await callback.message.edit_text("Вопрос не найден.")
@router.callback_query(F.data == 'callback_request')
@@ -91,11 +102,18 @@ async def callback_request(callback: CallbackQuery) -> None:
# ( * )
-@router.callback_query(F.data == 'back_to_products')
+@router.callback_query(F.data == 'back_to_previous_menu')
async def back_to_products(callback: CallbackQuery) -> None:
"""Возвращает к выбору продуктов."""
- pass
+ await callback.answer()
+
+ if callback.message:
+
+ await callback.message.edit_text(
+ text='Вы вернулись к списку продуктов и услуг:',
+ reply_markup=await inline_products_and_services()
+ )
@router.callback_query(F.data.startswith('category_'))
@@ -108,6 +126,101 @@ async def get_response_by_title(callback: CallbackQuery) -> None:
category_id = int(callback.data.split('_')[1])
+ await callback.message.edit_text(
+ text=await response_text_by_id(category_id),
+ reply_markup=await category_type_inline_keyboard(
+ await get_title_by_id(category_id)
+ )
+ )
+
+
+@router.callback_query(F.data == 'view_portfolio')
+async def view_portfolio(callback: CallbackQuery) -> None:
+ """Показ портфолио компании."""
+
+ try:
+ await callback.answer()
+
+ await callback.message.edit_text(
+ 'Вот наше портфолио:',
+ reply_markup=company_portfolio_choice
+ )
+
+ logger.info(f"Пользователь {callback.from_user.id} запросил портфолио")
+
+ except Exception as e:
+ logger.error(
+ f"Ошибка при показе портфолио для "
+ f"пользователя {callback.from_user.id}: {e}"
+ )
+
+ await callback.message.answer(
+ "Произошла ошибка. Пожалуйста, попробуйте позже."
+ )
+
+
+@router.callback_query(F.data == 'company_info')
+async def company_info(callback: CallbackQuery) -> None:
+ """Информация о компании."""
+
+ try:
+ await callback.answer()
+
+ await callback.message.edit_text(
+ 'Информация о компании:',
+ reply_markup=company_information_keyboard
+ )
+
+ logger.info(
+ f"Пользователь {callback.from_user.id} "
+ f"запросил информацию о компании"
+ )
+
+ except Exception as e:
+ logger.error(
+ f"Ошибка при запросе информации о компании "
+ f"для пользователя {callback.from_user.id}: {e}"
+ )
+
+ await callback.message.answer(
+ "Произошла ошибка. Пожалуйста, попробуйте позже."
+ )
+
+
+@router.callback_query(F.data == 'tech_support')
+async def get_support(callback: CallbackQuery) -> None:
+ """Выводит виды тех. поддержки."""
+
+ await callback.answer()
+
+ await callback.message.edit_text(
+ 'Какой вид технической поддержки вам нужен?',
+ reply_markup=support_keyboard
+ )
+
+
+@router.callback_query(F.data == 'products_services')
+async def products_services(callback: CallbackQuery) -> None:
+ """Информация о продуктах и услугах."""
+
+ try:
+ await callback.message.edit_text(
+ 'Мы предлагаем следующие продукты и услуги. '
+ 'Какой из них вас интересует?',
+ reply_markup=await inline_products_and_services()
+ )
+ await callback.answer()
+ logger.info(
+ f"Пользователь {callback.from_user.id} запросил "
+ f"информацию о продуктах и услугах"
+ )
+
+ except Exception as e:
+ logger.error(
+ f"Ошибка при запросе информации о продуктах и услугах "
+ f"для пользователя {callback.from_user.id}: {e}"
+ )
+
await callback.message.answer(
- await response_text_by_id(category_id)
+ "Произошла ошибка. Пожалуйста, попробуйте позже."
)
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index f216a40..4d64e6b 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -3,8 +3,10 @@
from aiogram import F, Router
from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.context import FSMContext
-from aiogram.types import Message, InlineKeyboardButton
+from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from bot.keyborads import back_to_main_menu
from crud.request_to_manager import create_request_to_manager
from bot.validators import (
is_valid_name, is_valid_phone_number, format_phone_number
@@ -18,17 +20,11 @@ class Form(StatesGroup):
"""Форма для связи с менеджером."""
first_name = State()
- last_name = State()
- middle_name = State()
phone_number = State()
QUESTIONS = {
Form.first_name: "Введите ваше имя:",
- Form.last_name: "Введите вашу фамилию:",
- Form.middle_name: (
- "Введите ваше отчество (или 'нет', если отсутствует):"
- ),
Form.phone_number: (
"Введите ваш номер телефона (в формате +7XXXXXXXXXX "
"или 8XXXXXXXXXX):"
@@ -36,29 +32,31 @@ class Form(StatesGroup):
}
-@router.message(F.text == 'Связаться с менеджером.')
-async def contact_with_manager(message: Message, state: FSMContext) -> None:
+@router.callback_query(F.data == 'contact_manager')
+async def contact_with_manager(
+ callback: CallbackQuery, state: FSMContext
+) -> None:
"""Выводит форму для связи с менеджером."""
try:
- await message.answer(
+ await callback.message.answer(
'Пожалуйста, оставьте ваше имя и контактный номер, '
'и наш менеджер свяжется с вами.'
)
- await ask_next_question(message, state, Form.first_name)
+ await ask_next_question(callback.message, state, Form.first_name)
logger.info(
- f"Пользователь {message.from_user.id} начал процесс."
+ f"Пользователь {callback.from_user.id} начал процесс."
)
except Exception as e:
logger.error(
f"Ошибка при выводе формы для пользователя "
- f"{message.from_user.id}: {e}"
+ f"{callback.from_user.id}: {e}"
)
- await message.answer("Произошла ошибка. Попробуйте снова.")
+ await callback.message.answer("Произошла ошибка. Попробуйте снова.")
async def ask_next_question(
@@ -67,7 +65,7 @@ async def ask_next_question(
"""Переход к следующему вопросу."""
try:
- await state.set_state(next_state)
+ await state.set_state(next_state.state)
await message.answer(QUESTIONS[next_state])
logger.info(
f"Переход к следующему вопросу: {next_state}"
@@ -91,7 +89,6 @@ async def process_first_name(message: Message, state: FSMContext) -> None:
await message.answer(
"Имя должно содержать только буквы. Попробуйте снова."
)
- await ask_next_question(message, state, Form.first_name)
return
await state.update_data(first_name=message.text)
@@ -100,73 +97,11 @@ async def process_first_name(message: Message, state: FSMContext) -> None:
f"Пользователь {message.from_user.id} ввёл имя: {message.text}"
)
- await ask_next_question(message, state, Form.last_name)
-
- except Exception as e:
- logger.error(
- f"Ошибка при обработке имени пользователя "
- f"{message.from_user.id}: {e}"
- )
-
- await message.answer("Произошла ошибка. Попробуйте снова.")
-
-
-@router.message(Form.last_name)
-async def process_last_name(message: Message, state: FSMContext) -> None:
- """Состояние: ввод фамилии."""
-
- try:
- if not is_valid_name(message.text):
- await message.answer(
- "Фамилия должна содержать только буквы. Попробуйте снова."
- )
- await ask_next_question(message, state, Form.last_name)
- return
-
- await state.update_data(last_name=message.text)
-
- logger.info(
- f"Пользователь {message.from_user.id} ввёл фамилию: "
- f"{message.text}"
- )
-
- await ask_next_question(message, state, Form.middle_name)
-
- except Exception as e:
- logger.error(
- f"Ошибка при обработке фамилии пользователя "
- f"{message.from_user.id}: {e}"
- )
-
- await message.answer("Произошла ошибка. Попробуйте снова.")
-
-
-@router.message(Form.middle_name)
-async def process_middle_name(message: Message, state: FSMContext) -> None:
- """Состояние: ввод отчества."""
-
- try:
- if message.text.lower() != "нет" and not is_valid_name(message.text):
- await message.answer(
- "Отчество должно содержать только буквы или быть 'нет'. "
- "Попробуйте снова."
- )
-
- await ask_next_question(message, state, Form.middle_name)
- return
-
- await state.update_data(middle_name=message.text)
-
- logger.info(
- f"Пользователь {message.from_user.id} ввёл отчество: "
- f"{message.text}"
- )
-
await ask_next_question(message, state, Form.phone_number)
except Exception as e:
logger.error(
- f"Ошибка при обработке отчества пользователя "
+ f"Ошибка при обработке имени пользователя "
f"{message.from_user.id}: {e}"
)
@@ -183,8 +118,6 @@ async def process_phone_number(message: Message, state: FSMContext) -> None:
"Номер телефона должен быть в формате +7XXXXXXXXXX "
"или 8XXXXXXXXXX. Попробуйте снова."
)
-
- await ask_next_question(message, state, Form.phone_number)
return
formatted_phone_number = format_phone_number(message.text)
@@ -207,15 +140,11 @@ async def process_phone_number(message: Message, state: FSMContext) -> None:
f'с вами в ближайшее время.\n'
f"Отправленная форма:\n"
f"Имя: {user_data['first_name']}\n"
- f"Фамилия: {user_data['last_name']}\n"
- f"Отчество: {user_data['middle_name']}\n"
f"Номер телефона: {user_data['phone_number']}"
)
keyboard = InlineKeyboardBuilder()
- keyboard.add(InlineKeyboardButton(
- text="Вернуться в главное меню", callback_data="back_to_main_menu"
- ))
+ keyboard.add(back_to_main_menu)
await message.answer(
"Вы можете вернуться в главное меню.",
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index cc87348..70c441c 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -1,14 +1,14 @@
import logging
-from aiogram import F, Router
+from aiogram import Router
from aiogram.filters import CommandStart, Command
from aiogram.types import Message
from models.models import RoleEnum
-from crud.request_to_manager import create_user_id, get_role_by_tg_id
+from crud.request_to_manager import (
+ create_user_id, get_role_by_tg_id, is_user_in_db
+)
from bot.keyborads import (
- main_keyboard, company_information_keyboard,
- inline_products_and_services, company_portfolio_choice,
- support_keyboard
+ main_keyboard
)
router = Router()
@@ -36,7 +36,8 @@ async def cmd_admin(message: Message) -> None:
async def cmd_start(message: Message) -> None:
"""Выводит приветствие пользователя."""
- await create_user_id(message.from_user.id)
+ if not await is_user_in_db(message.from_user.id):
+ await create_user_id(message.from_user.id)
try:
await message.answer(
@@ -58,93 +59,3 @@ async def cmd_start(message: Message) -> None:
await message.answer(
"Произошла ошибка. Пожалуйста, попробуйте позже."
)
-
-
-@router.message(F.text == 'Посмотреть портфолио.')
-async def view_portfolio(message: Message) -> None:
- """Показ портфолио компании."""
-
- try:
- await message.answer(
- 'Вот наше портфолио:',
- reply_markup=company_portfolio_choice
- )
- logger.info(
- f"Пользователь {message.from_user.id} запросил портфолио"
- )
-
- except Exception as e:
- logger.error(
- f"Ошибка при показе портфолио для пользователя "
- f"{message.from_user.id}: {e}"
- )
-
- await message.answer(
- "Произошла ошибка. Пожалуйста, попробуйте позже."
- )
-
-
-@router.message(F.text == 'Получить информацию о компании.')
-async def company_info(message: Message) -> None:
- """Информация о компании."""
-
- try:
- await message.answer(
- 'Информация о компании:',
- reply_markup=company_information_keyboard
- )
-
- logger.info(
- f"Пользователь {message.from_user.id} запросил информацию "
- f"о компании"
- )
-
- except Exception as e:
- logger.error(
- f"Ошибка при запросе информации о компании для пользователя "
- f"{message.from_user.id}: {e}"
- )
-
- await message.answer(
- "Произошла ошибка. Пожалуйста, попробуйте позже."
- )
-
-
-@router.message(F.text == 'Получить техническую поддержку.')
-async def get_support(message: Message) -> None:
- """Выводит виды тех. поддержки."""
-
- await message.answer(
- 'Какой вид технической поддержки '
- 'вам нужен? ',
- reply_markup=support_keyboard
- )
-
-
-@router.message(F.text == 'Узнать о продуктах и услугах.')
-async def products_services(message: Message) -> None:
- """Информация о продуктах и услугах."""
-
- try:
- await message.answer(
- (
- 'Мы предлагаем следуюющие продукты и услуги. '
- 'Какой тз них вас интересеует?'
- ),
- reply_markup=await inline_products_and_services()
- )
-
- logger.info(
- f"Пользователь {message.from_user.id} запросил информацию "
- f"о продуктах и услугах"
- )
-
- except Exception as e:
- logger.error(
- f"Ошибка при запросе информации о продуктах и услугах "
- f"для пользователя {message.from_user.id}: {e}"
- )
-
- await message.answer(
- "Произошла ошибка. Пожалуйста, попробуйте позже."
- )
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index 8feb9bf..4bfcc4b 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -1,53 +1,75 @@
from aiogram.types import (
- ReplyKeyboardMarkup, KeyboardButton,
InlineKeyboardMarkup, InlineKeyboardButton
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from crud.request_to_manager import get_all_prtfolio_projects, get_question_by_title
+from crud.request_to_manager import (
+ get_all_prtfolio_projects, get_question_by_title,
+ get_categories_by_name
+)
from models.models import CheckCompanyPortfolio, ProductCategory
-# кнопку вернуться назад можно вынести отдельно, чтобы не дублировать код
-
-PRODUCTS_AND_SERVICES = [
- 'Разработка сайтов', 'Создание порталов',
- 'Разработка мобильных приложений', 'Консультация по КИОСК365',
- '"НБП ЕЖА"', 'Хостинг',
-] # моделирую результат запроса из бд ( * )
-
-main_keyboard = ReplyKeyboardMarkup(
- keyboard=[
- [KeyboardButton(text='Посмотреть портфолио.')],
- [KeyboardButton(text='Получить информацию о компании.')],
- [KeyboardButton(text='Узнать о продуктах и услугах.')],
- [KeyboardButton(text='Получить техническую поддержку.'),],
- [KeyboardButton(text='Связаться с менеджером.')],
- ],
- resize_keyboard=True,
- one_time_keyboard=False,
- input_field_placeholder='Выберите пункт меню.'
+
+back_to_main_menu = InlineKeyboardButton(
+ text='Вернуться к основным вариантам.',
+ callback_data='back_to_main_menu'
)
-company_information_keyboard = InlineKeyboardMarkup(
+back_to_previous_menu = InlineKeyboardButton(
+ text='Назад к продуктам.',
+ callback_data='back_to_previous_menu'
+)
+
+main_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
- text='Презентация компании.',
- url='https://www.visme.co/ru/powerpoint-online/' # тут ссылка на презентацию, ткунл рандомную
+ text='Посмотреть портфолио.',
+ callback_data='view_portfolio'
)
],
[
InlineKeyboardButton(
- text='Карточка компании.',
- url='https://github.com/Rxyalxrd' # тут будет карточка компании, пока моя)
+ text='Получить информацию о компании.',
+ callback_data='company_info'
)
],
[
InlineKeyboardButton(
- text='Вернуться к основным вариантам.',
- callback_data='back_to_main_menu'
+ text='Узнать о продуктах и услугах.',
+ callback_data='products_services'
)
- ]
+ ],
+ [
+ InlineKeyboardButton(
+ text='Получить техническую поддержку.',
+ callback_data='tech_support'
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text='Связаться с менеджером.',
+ callback_data='contact_manager'
+ )
+ ],
+ ]
+)
+
+company_information_keyboard = InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text='Презентация компании.',
+ url='https://www.visme.co/ru/powerpoint-online/'
+ )
+ ],
+ [
+ InlineKeyboardButton(
+ text='Карточка компании.',
+ url='https://github.com/Rxyalxrd'
+ )
+ ],
+ [back_to_main_menu]
]
)
@@ -65,12 +87,7 @@ async def inline_products_and_services():
callback_data=f'category_{obj.id}'
))
- keyboard.add(
- InlineKeyboardButton(
- text='Вернуться к основным вариантам.',
- callback_data='back_to_main_menu'
- )
- )
+ keyboard.add(back_to_main_menu)
return keyboard.adjust(1).as_markup()
@@ -83,12 +100,7 @@ async def inline_products_and_services():
callback_data='show_projects'
)
],
- [
- InlineKeyboardButton(
- text='Вернуться к основным вариантам.',
- callback_data='back_to_main_menu'
- )
- ]
+ [back_to_main_menu]
]
)
@@ -108,12 +120,7 @@ async def list_of_projects_keyboard():
)
)
- keyboard.add(
- InlineKeyboardButton(
- text='Вернуться к основным вариантам.',
- callback_data='back_to_main_menu'
- )
- )
+ keyboard.add(back_to_main_menu)
return keyboard.adjust(1).as_markup()
@@ -138,18 +145,15 @@ async def list_of_projects_keyboard():
callback_data='callback_request'
)
],
- [
- InlineKeyboardButton(
- text='Вернуться к основным вариантам.',
- callback_data='back_to_main_menu'
- )
- ]
+ [back_to_main_menu]
]
)
-async def faq_or_problems_with_products_inline_keyboard(question_type) -> InlineKeyboardMarkup:
- """Создание инлайн-клавиатуры для частозадаваемых вопросов или проблем с продуктами."""
+async def faq_or_problems_with_products_inline_keyboard(
+ question_type: str
+) -> InlineKeyboardMarkup:
+ """Инлайн-клавиатуры для f.a.q вопросов или проблем с продуктами."""
questions = await get_question_by_title(question_type)
@@ -162,4 +166,28 @@ async def faq_or_problems_with_products_inline_keyboard(question_type) -> Inline
)
)
+ keyboard.add(back_to_main_menu)
+
+ return keyboard.adjust(1).as_markup()
+
+
+async def category_type_inline_keyboard(
+ product_name: str
+) -> InlineKeyboardMarkup:
+ """Инлайн клавиатура для типов в категориях."""
+
+ category_types = await get_categories_by_name(product_name)
+
+ keyboard = InlineKeyboardBuilder()
+
+ for category_type in category_types:
+ keyboard.add(
+ InlineKeyboardButton(
+ text=category_type.name,
+ url=category_type.url
+ )
+ )
+
+ keyboard.add(back_to_previous_menu)
+
return keyboard.adjust(1).as_markup()
diff --git a/app/core/db.py b/app/core/db.py
index 2f9c330..44a1f84 100644
--- a/app/core/db.py
+++ b/app/core/db.py
@@ -5,18 +5,7 @@
)
from contextlib import asynccontextmanager
-from pydantic_settings import BaseSettings
-
-
-class Settings(BaseSettings): # создать файл settings.py, перенести туда класс
- database_url: str
- bot_token: str
-
- class Config:
- env_file = '.env'
-
-
-settings = Settings()
+from .settings import settings
class PreBase:
diff --git a/app/core/settings.py b/app/core/settings.py
new file mode 100644
index 0000000..c286daa
--- /dev/null
+++ b/app/core/settings.py
@@ -0,0 +1,12 @@
+from pydantic_settings import BaseSettings
+
+
+class Settings(BaseSettings):
+ database_url: str
+ bot_token: str
+
+ class Config:
+ env_file = '.env'
+
+
+settings = Settings()
diff --git a/app/crud/request_to_manager.py b/app/crud/request_to_manager.py
index 383efae..c6afe36 100644
--- a/app/crud/request_to_manager.py
+++ b/app/crud/request_to_manager.py
@@ -1,7 +1,7 @@
from core.db import get_async_session
from models.models import (
ContactManager, CheckCompanyPortfolio, User, ProductCategory,
- Info
+ Info, CategoryType
)
from sqlalchemy import select
@@ -13,7 +13,7 @@ async def create_request_to_manager(
async with get_async_session() as session:
data_to_db = ContactManager(**user_data)
-
+ # TODO: нужно поменять значение need_contact_with_manager на True
session.add(data_to_db)
await session.commit()
await session.refresh(data_to_db)
@@ -46,6 +46,19 @@ async def create_user_id(tg_id: int) -> User:
return data_to_db
+async def is_user_in_db(tg_id: int) -> bool:
+ """Проверяем, есть ли пользователь в БД."""
+
+ async with get_async_session() as session:
+
+ stmt = select(User).where(User.tg_id == tg_id)
+
+ result = await session.execute(stmt)
+ user = result.scalar_one_or_none()
+
+ return user is not None
+
+
async def get_role_by_tg_id(tg_id: int) -> User:
"""Получаем роль пользователя по его tg_id."""
@@ -82,13 +95,46 @@ async def get_question_by_title(question_type) -> list[Info]:
return result.scalars().all()
-async def get_question_by_id(question_id: int) -> Info:
- """Получает вопрос по его ID."""
+async def get_question_by_id(question_id: int) -> Info | None:
+ """Получить вопрос по его ID."""
async with get_async_session() as session:
result = await session.execute(
- select(Info).where(Info.id == question_id)
+ select(Info).where(Info.id == int(question_id))
)
return result.scalar_one_or_none()
+
+
+async def get_categories_by_name(
+ product_name: str
+) -> list[CategoryType]:
+ """Получить все типы по категории по его названию."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(CategoryType)
+ .join(
+ ProductCategory, ProductCategory.id == CategoryType.product_id
+ )
+ .where(ProductCategory.title == product_name)
+ )
+
+ return result.scalars().all()
+
+
+async def get_title_by_id(category_id: int) -> str:
+ """Получает название категории по ID из базы данных."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(ProductCategory.title).where(
+ ProductCategory.id == category_id
+ )
+ )
+ category_name = result.scalar()
+
+ return category_name
diff --git a/app/middlewares/__init__.py b/app/middlewares/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/middlewares/middleware.py b/app/middlewares/middleware.py
new file mode 100644
index 0000000..b5d10e8
--- /dev/null
+++ b/app/middlewares/middleware.py
@@ -0,0 +1,21 @@
+from typing import Any, Awaitable, Callable, Dict
+
+from aiogram import BaseMiddleware
+from aiogram.types import TelegramObject
+
+from sqlalchemy.ext.asyncio import async_sessionmaker
+
+
+class DataBaseSession(BaseMiddleware):
+ def init(self, session_pool: async_sessionmaker):
+ self.session_pool = session_pool
+
+ async def __call__(
+ self,
+ handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
+ event: TelegramObject,
+ data: Dict[str, Any],
+ ) -> Any:
+ async with self.session_pool() as session:
+ data["session"] = session
+ return await handler(event, data)
diff --git a/app/models/models.py b/app/models/models.py
index 71c8326..c694ca1 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -30,19 +30,28 @@ class User(Base):
unique=True
)
- # username: Mapped[str] = mapped_column(
- # pgsql_types.VARCHAR(150)
- # )
-
- # full_name: Mapped[str] = mapped_column(
- # pgsql_types.VARCHAR(150)
- # )
+ name: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(32),
+ nullable=True
+ )
phone: Mapped[str] = mapped_column(
pgsql_types.VARCHAR(25),
nullable=True
)
+ need_support: Mapped[bool] = mapped_column(
+ pgsql_types.BOOLEAN,
+ default=False,
+ nullable=False
+ )
+
+ need_contact_with_manager: Mapped[bool] = mapped_column(
+ pgsql_types.BOOLEAN,
+ default=False,
+ nullable=False
+ )
+
role: Mapped[RoleEnum] = mapped_column(
pgsql_types.ENUM(
RoleEnum,
@@ -58,9 +67,15 @@ class User(Base):
nullable=False
)
+ shipping_date: Mapped[datetime] = mapped_column(
+ pgsql_types.TIMESTAMP(timezone=True),
+ server_default=func.now(), # TODO: время заностся в бд при /start
+ nullable=False
+ )
+
class ProductCategory(Base):
- """БД модель о продуктах и услугах."""
+ """БД модель продуктов и услуг."""
title: Mapped[str] = mapped_column(
pgsql_types.VARCHAR(150)
@@ -85,11 +100,12 @@ class CategoryType(Base):
)
url: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(64)
+ pgsql_types.VARCHAR(128)
)
media: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(128)
+ pgsql_types.VARCHAR(128),
+ nullable=True
)
@@ -141,26 +157,15 @@ class Info(Base):
nullable=False
)
- # media: Mapped[str] = mapped_column(
- # pgsql_types.VARCHAR(256),
- # )
-
class ContactManager(Base):
- """Бд модель для заявки к менеджеру."""
+ """Бд модель заявки к менеджеру."""
first_name: Mapped[str] = mapped_column(
pgsql_types.VARCHAR(32),
nullable=False
)
- last_name: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(32),
- nullable=False
- )
- middle_name: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(32),
- nullable=False
- )
+
phone_number: Mapped[str] = mapped_column(
pgsql_types.VARCHAR(25),
nullable=False
diff --git a/app/constants.py b/app/scripts_for_db.py
similarity index 71%
rename from app/constants.py
rename to app/scripts_for_db.py
index 8b55036..6725938 100644
--- a/app/constants.py
+++ b/app/scripts_for_db.py
@@ -19,3 +19,16 @@
('PROBLEMS_WITH_PRODUCTS', 'Что делать, если продукт неисправен?', 'Если продукт неисправен, пожалуйста, свяжитесь с поддержкой, и мы организуем замену или возврат.'),
('PROBLEMS_WITH_PRODUCTS', 'Почему мой продукт не включается?', 'Убедитесь, что устройство заряжено, и проверьте кнопку включения.');
'''
+
+'''
+Скрипт тестовых данных для ветки узнать о продуктах и услугах
+
+INSERT INTO categorytype (name, product_id, url, media)
+VALUES
+ ('Корпоративные сайты', 1, 'https://www.google.com', 'adasdad'),
+ ('Лендинги', 1, 'https://www.wikipedia.org', 'adadasda'),
+ ('Интернет-магазины', 1, 'https://www.amazon.com', 'ghfhfgh'),
+ ('Программы лояльности', 2, 'https://www.airlinesoftware.com/loyalty-solutions', 'adasdad'),
+ ('Порталы для госучереждений', 2, 'https://www.egov.kz', 'adadasda'),
+ ('Личные кабинеты', 2, 'https://my.gov.ru', 'ghfhfgh');
+'''
From eb28da9fe7ad9f0abeedd4760a556172766165c2 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Fri, 4 Oct 2024 19:39:16 +0300
Subject: [PATCH 17/75] all branches done
---
alembic/README | 9 +-
.../182d5908e82a_update_user_model.py | 91 ------------
...b37e1_update_user_contactmanager_models.py | 48 +++++++
app/bot/callbacks.py | 17 +--
app/bot/fsm_context.py | 25 ++--
app/bot/handlers.py | 2 +-
app/bot/keyborads.py | 7 +-
app/crud/base_crud.py | 0
app/crud/projects.py | 62 ++++++++
app/crud/questions.py | 28 ++++
app/crud/request_to_manager.py | 136 ++----------------
app/crud/users.py | 42 ++++++
app/main.py | 8 +-
app/middlewares/middleware.py | 2 +-
app/models/models.py | 46 +++---
app/scripts_for_db.py | 11 ++
16 files changed, 252 insertions(+), 282 deletions(-)
delete mode 100644 alembic/versions/182d5908e82a_update_user_model.py
create mode 100644 alembic/versions/31916cbb37e1_update_user_contactmanager_models.py
delete mode 100644 app/crud/base_crud.py
create mode 100644 app/crud/projects.py
create mode 100644 app/crud/questions.py
create mode 100644 app/crud/users.py
diff --git a/alembic/README b/alembic/README
index 9b8ca67..801b3bd 100644
--- a/alembic/README
+++ b/alembic/README
@@ -1,9 +1,14 @@
+Инициализировать Alembic в проекте(должены использовать асинхронный шаблон)
+```bash
+poetry run alembic init --template async alembic
+```
+
Примените миграции
```bash
-alembic upgrade head
+poetry run alembic upgrade head
```
Создание миграций, если внесены изменения в /models
```bash
-alembic revision --autogenerate -m "Your commit"
+poetry run alembic revision --autogenerate -m "Your commit"
```
\ No newline at end of file
diff --git a/alembic/versions/182d5908e82a_update_user_model.py b/alembic/versions/182d5908e82a_update_user_model.py
deleted file mode 100644
index 836cd07..0000000
--- a/alembic/versions/182d5908e82a_update_user_model.py
+++ /dev/null
@@ -1,91 +0,0 @@
-"""Update user model
-
-Revision ID: 182d5908e82a
-Revises:
-Create Date: 2024-10-04 01:00:56.159813
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = '182d5908e82a'
-down_revision: Union[str, None] = None
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('checkcompanyportfolio',
- sa.Column('project_name', sa.VARCHAR(length=48), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('contactmanager',
- sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
- sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('info',
- sa.Column('question_type', postgresql.ENUM('GENERAL_QUESTIONS', 'PROBLEMS_WITH_PRODUCTS', name='question_enum'), nullable=False),
- sa.Column('question', sa.TEXT(), nullable=False),
- sa.Column('answer', sa.TEXT(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('question')
- )
- op.create_table('informationaboutcompany',
- sa.Column('name', sa.VARCHAR(length=48), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('productcategory',
- sa.Column('title', sa.VARCHAR(length=150), nullable=False),
- sa.Column('response', sa.TEXT(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('user',
- sa.Column('tg_id', sa.BIGINT(), nullable=False),
- sa.Column('name', sa.VARCHAR(length=32), nullable=True),
- sa.Column('phone', sa.VARCHAR(length=25), nullable=True),
- sa.Column('need_support', sa.BOOLEAN(), nullable=False),
- sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
- sa.Column('role', postgresql.ENUM('USER', 'ADMIN', 'MANAGER', name='role_enum'), nullable=False),
- sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
- sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('tg_id')
- )
- op.create_table('categorytype',
- sa.Column('name', sa.VARCHAR(length=150), nullable=False),
- sa.Column('product_id', sa.Integer(), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('media', sa.VARCHAR(length=128), nullable=True),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_index(op.f('ix_categorytype_product_id'), 'categorytype', ['product_id'], unique=False)
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_index(op.f('ix_categorytype_product_id'), table_name='categorytype')
- op.drop_table('categorytype')
- op.drop_table('user')
- op.drop_table('productcategory')
- op.drop_table('informationaboutcompany')
- op.drop_table('info')
- op.drop_table('contactmanager')
- op.drop_table('checkcompanyportfolio')
- # ### end Alembic commands ###
diff --git a/alembic/versions/31916cbb37e1_update_user_contactmanager_models.py b/alembic/versions/31916cbb37e1_update_user_contactmanager_models.py
new file mode 100644
index 0000000..7980d3d
--- /dev/null
+++ b/alembic/versions/31916cbb37e1_update_user_contactmanager_models.py
@@ -0,0 +1,48 @@
+"""Update User, ContactManager models
+
+Revision ID: 31916cbb37e1
+Revises:
+Create Date: 2024-10-04 17:29:06.898325
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision: str = '31916cbb37e1'
+down_revision: Union[str, None] = None
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('contactmanager',
+ sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
+ sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
+ sa.Column('need_support', sa.BOOLEAN(), nullable=False),
+ sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
+ sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.drop_column('user', 'need_support')
+ op.drop_column('user', 'name')
+ op.drop_column('user', 'phone')
+ op.drop_column('user', 'shipping_date')
+ op.drop_column('user', 'need_contact_with_manager')
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('user', sa.Column('need_contact_with_manager', sa.BOOLEAN(), autoincrement=False, nullable=False))
+ op.add_column('user', sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False))
+ op.add_column('user', sa.Column('phone', sa.VARCHAR(length=25), autoincrement=False, nullable=True))
+ op.add_column('user', sa.Column('name', sa.VARCHAR(length=32), autoincrement=False, nullable=True))
+ op.add_column('user', sa.Column('need_support', sa.BOOLEAN(), autoincrement=False, nullable=False))
+ op.drop_table('contactmanager')
+ # ### end Alembic commands ###
diff --git a/app/bot/callbacks.py b/app/bot/callbacks.py
index d6bb538..d0cea79 100644
--- a/app/bot/callbacks.py
+++ b/app/bot/callbacks.py
@@ -11,10 +11,8 @@
company_information_keyboard, company_portfolio_choice,
support_keyboard, back_to_main_menu
)
-from crud.request_to_manager import (
- get_question_by_id, response_text_by_id, get_title_by_id
-)
-
+from crud.questions import get_question_by_id
+from crud.projects import get_title_by_id, response_text_by_id
router = Router()
@@ -53,7 +51,7 @@ async def previous_choice(callback: CallbackQuery) -> None:
# ( * )
-@router.callback_query(F.data.in_(['get_faq', 'get_problems_with_products']))
+@router.callback_query(F.data.in_(('get_faq', 'get_problems_with_products')))
async def get_questions(callback: CallbackQuery) -> None:
"""Инлайн вывод общих вопросов и проблем с продуктами."""
@@ -93,15 +91,6 @@ async def get_faq_answer(callback: CallbackQuery) -> None:
await callback.message.edit_text("Вопрос не найден.")
-@router.callback_query(F.data == 'callback_request')
-async def callback_request(callback: CallbackQuery) -> None:
- """Инлайн вывод запроса на обратный звонок."""
-
- pass
-
- # ( * )
-
-
@router.callback_query(F.data == 'back_to_previous_menu')
async def back_to_products(callback: CallbackQuery) -> None:
"""Возвращает к выбору продуктов."""
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index 4d64e6b..3d13ea6 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -32,14 +32,17 @@ class Form(StatesGroup):
}
-@router.callback_query(F.data == 'contact_manager')
+@router.callback_query(F.data.in_(('contact_manager', 'callback_request')))
async def contact_with_manager(
callback: CallbackQuery, state: FSMContext
) -> None:
- """Выводит форму для связи с менеджером."""
+ """Выводит форму для связи с менеджером или запрос на обратный звонок."""
try:
- await callback.message.answer(
+
+ await state.update_data(request_type=callback.data)
+
+ await callback.message.edit_text(
'Пожалуйста, оставьте ваше имя и контактный номер, '
'и наш менеджер свяжется с вами.'
)
@@ -130,8 +133,9 @@ async def process_phone_number(message: Message, state: FSMContext) -> None:
)
user_data = await state.get_data()
+ request_type = user_data.pop('request_type')
- new_request = await create_request_to_manager(user_data)
+ new_request = await create_request_to_manager(user_data, request_type)
logger.info(f"Запись создана в БД с ID: {new_request.id}")
@@ -140,15 +144,10 @@ async def process_phone_number(message: Message, state: FSMContext) -> None:
f'с вами в ближайшее время.\n'
f"Отправленная форма:\n"
f"Имя: {user_data['first_name']}\n"
- f"Номер телефона: {user_data['phone_number']}"
- )
-
- keyboard = InlineKeyboardBuilder()
- keyboard.add(back_to_main_menu)
-
- await message.answer(
- "Вы можете вернуться в главное меню.",
- reply_markup=keyboard.as_markup()
+ f"Номер телефона: {user_data['phone_number']}",
+ reply_markup=InlineKeyboardBuilder().add(
+ back_to_main_menu)
+ .as_markup()
)
await state.clear()
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index 70c441c..2857d96 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -4,7 +4,7 @@
from aiogram.types import Message
from models.models import RoleEnum
-from crud.request_to_manager import (
+from crud.users import (
create_user_id, get_role_by_tg_id, is_user_in_db
)
from bot.keyborads import (
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index 4bfcc4b..f5b494c 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -2,11 +2,8 @@
InlineKeyboardMarkup, InlineKeyboardButton
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from crud.request_to_manager import (
- get_all_prtfolio_projects, get_question_by_title,
- get_categories_by_name
-)
-
+from crud.questions import get_question_by_title
+from crud.projects import get_all_prtfolio_projects, get_categories_by_name
from models.models import CheckCompanyPortfolio, ProductCategory
diff --git a/app/crud/base_crud.py b/app/crud/base_crud.py
deleted file mode 100644
index e69de29..0000000
diff --git a/app/crud/projects.py b/app/crud/projects.py
new file mode 100644
index 0000000..0b0bb83
--- /dev/null
+++ b/app/crud/projects.py
@@ -0,0 +1,62 @@
+from core.db import get_async_session
+from models.models import (
+ CheckCompanyPortfolio, ProductCategory,
+ CategoryType
+)
+from sqlalchemy import select
+
+
+async def get_all_prtfolio_projects(
+ object_model: CheckCompanyPortfolio | ProductCategory
+) -> list[CheckCompanyPortfolio | ProductCategory]:
+ """Получение всех проектов-портфолио или продуктов и услуг."""
+
+ async with get_async_session() as session:
+ result = await session.execute(select(object_model))
+
+ return result.scalars().all()
+
+
+async def response_text_by_id(id: int) -> str:
+ """Возвращает ответ на выбранную категорию."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(ProductCategory.response).where(ProductCategory.id == id)
+ )
+
+ return result.scalar()
+
+
+async def get_categories_by_name(
+ product_name: str
+) -> list[CategoryType]:
+ """Получить все типы по категории по его названию."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(CategoryType)
+ .join(
+ ProductCategory, ProductCategory.id == CategoryType.product_id
+ )
+ .where(ProductCategory.title == product_name)
+ )
+
+ return result.scalars().all()
+
+
+async def get_title_by_id(category_id: int) -> str:
+ """Получает название категории по ID из базы данных."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(ProductCategory.title).where(
+ ProductCategory.id == category_id
+ )
+ )
+ category_name = result.scalar()
+
+ return category_name
diff --git a/app/crud/questions.py b/app/crud/questions.py
new file mode 100644
index 0000000..949942b
--- /dev/null
+++ b/app/crud/questions.py
@@ -0,0 +1,28 @@
+from core.db import get_async_session
+from models.models import Info
+
+from sqlalchemy import select
+
+
+async def get_question_by_title(question_type) -> list[Info]:
+ """Получаем все вопросы по категории."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(Info).where(Info.question_type == question_type)
+ )
+
+ return result.scalars().all()
+
+
+async def get_question_by_id(question_id: int) -> Info | None:
+ """Получить вопрос по его ID."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(Info).where(Info.id == int(question_id))
+ )
+
+ return result.scalar_one_or_none()
diff --git a/app/crud/request_to_manager.py b/app/crud/request_to_manager.py
index c6afe36..d256bb8 100644
--- a/app/crud/request_to_manager.py
+++ b/app/crud/request_to_manager.py
@@ -1,140 +1,24 @@
+from datetime import datetime
+
from core.db import get_async_session
-from models.models import (
- ContactManager, CheckCompanyPortfolio, User, ProductCategory,
- Info, CategoryType
-)
-from sqlalchemy import select
+from models.models import ContactManager
async def create_request_to_manager(
- user_data: dict
+ user_data: dict, request_type: str
) -> ContactManager:
"""Создание заявки на связь с менеджером."""
async with get_async_session() as session:
- data_to_db = ContactManager(**user_data)
- # TODO: нужно поменять значение need_contact_with_manager на True
- session.add(data_to_db)
- await session.commit()
- await session.refresh(data_to_db)
-
- return data_to_db
-
-
-async def get_all_prtfolio_projects(
- object_model: CheckCompanyPortfolio | ProductCategory
-) -> list[CheckCompanyPortfolio | ProductCategory]:
- """Получение всех проектов-портфолио."""
-
- async with get_async_session() as session:
- result = await session.execute(select(object_model))
-
- return result.scalars().all()
-
-
-async def create_user_id(tg_id: int) -> User:
- """Запись tg_id в таблицу user."""
-
- async with get_async_session() as session:
-
- data_to_db = User(tg_id=tg_id)
+ data_to_db = ContactManager(
+ **user_data,
+ shipping_date=datetime.utcnow(),
+ need_support=(request_type == 'callback_request'),
+ need_contact_with_manager=(request_type == 'contact_manager')
+ )
session.add(data_to_db)
await session.commit()
await session.refresh(data_to_db)
return data_to_db
-
-
-async def is_user_in_db(tg_id: int) -> bool:
- """Проверяем, есть ли пользователь в БД."""
-
- async with get_async_session() as session:
-
- stmt = select(User).where(User.tg_id == tg_id)
-
- result = await session.execute(stmt)
- user = result.scalar_one_or_none()
-
- return user is not None
-
-
-async def get_role_by_tg_id(tg_id: int) -> User:
- """Получаем роль пользователя по его tg_id."""
-
- async with get_async_session() as session:
-
- result = await session.execute(
- select(User.role).where(User.tg_id == tg_id)
- )
-
- return result.scalar()
-
-
-async def response_text_by_id(id: int) -> str:
- """Возвращает ответ на выбранную категорию."""
-
- async with get_async_session() as session:
-
- result = await session.execute(
- select(ProductCategory.response).where(ProductCategory.id == id)
- )
-
- return result.scalar()
-
-
-async def get_question_by_title(question_type) -> list[Info]:
- """Получаем все вопросы по категории."""
-
- async with get_async_session() as session:
-
- result = await session.execute(
- select(Info).where(Info.question_type == question_type)
- )
-
- return result.scalars().all()
-
-
-async def get_question_by_id(question_id: int) -> Info | None:
- """Получить вопрос по его ID."""
-
- async with get_async_session() as session:
-
- result = await session.execute(
- select(Info).where(Info.id == int(question_id))
- )
-
- return result.scalar_one_or_none()
-
-
-async def get_categories_by_name(
- product_name: str
-) -> list[CategoryType]:
- """Получить все типы по категории по его названию."""
-
- async with get_async_session() as session:
-
- result = await session.execute(
- select(CategoryType)
- .join(
- ProductCategory, ProductCategory.id == CategoryType.product_id
- )
- .where(ProductCategory.title == product_name)
- )
-
- return result.scalars().all()
-
-
-async def get_title_by_id(category_id: int) -> str:
- """Получает название категории по ID из базы данных."""
-
- async with get_async_session() as session:
-
- result = await session.execute(
- select(ProductCategory.title).where(
- ProductCategory.id == category_id
- )
- )
- category_name = result.scalar()
-
- return category_name
diff --git a/app/crud/users.py b/app/crud/users.py
new file mode 100644
index 0000000..f3e64e6
--- /dev/null
+++ b/app/crud/users.py
@@ -0,0 +1,42 @@
+from core.db import get_async_session
+from models.models import User
+from sqlalchemy import select
+
+
+async def create_user_id(tg_id: int) -> User:
+ """Запись tg_id в таблицу user."""
+
+ async with get_async_session() as session:
+
+ data_to_db = User(tg_id=tg_id)
+
+ session.add(data_to_db)
+ await session.commit()
+ await session.refresh(data_to_db)
+
+ return data_to_db
+
+
+async def is_user_in_db(tg_id: int) -> bool:
+ """Проверяем, есть ли пользователь в БД."""
+
+ async with get_async_session() as session:
+
+ stmt = select(User).where(User.tg_id == tg_id)
+
+ result = await session.execute(stmt)
+ user = result.scalar_one_or_none()
+
+ return user is not None
+
+
+async def get_role_by_tg_id(tg_id: int) -> User:
+ """Получаем роль пользователя по его tg_id."""
+
+ async with get_async_session() as session:
+
+ result = await session.execute(
+ select(User.role).where(User.tg_id == tg_id)
+ )
+
+ return result.scalar()
diff --git a/app/main.py b/app/main.py
index fc795bd..c3438cb 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,13 +1,14 @@
import logging
import asyncio
+# from app.core.db import AsyncSessionLocal
+# from app.middlewares.middleware import DataBaseSession
from core.bot_setup import bot, dispatcher, check_token
from bot.handlers import router as message_router
from bot.callbacks import router as callback_router
from bot.fsm_context import router as fsm_context_router
-# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -43,6 +44,11 @@ async def main() -> None:
if __name__ == "__main__":
try:
+ # dispatcher.update.middleware(
+ # DataBaseSession(
+ # session_pool=AsyncSessionLocal
+ # )
+ # )
asyncio.run(main())
except KeyboardInterrupt:
diff --git a/app/middlewares/middleware.py b/app/middlewares/middleware.py
index b5d10e8..403edc5 100644
--- a/app/middlewares/middleware.py
+++ b/app/middlewares/middleware.py
@@ -7,7 +7,7 @@
class DataBaseSession(BaseMiddleware):
- def init(self, session_pool: async_sessionmaker):
+ def __init__(self, session_pool: async_sessionmaker):
self.session_pool = session_pool
async def __call__(
diff --git a/app/models/models.py b/app/models/models.py
index c694ca1..e7932a6 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -30,28 +30,6 @@ class User(Base):
unique=True
)
- name: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(32),
- nullable=True
- )
-
- phone: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(25),
- nullable=True
- )
-
- need_support: Mapped[bool] = mapped_column(
- pgsql_types.BOOLEAN,
- default=False,
- nullable=False
- )
-
- need_contact_with_manager: Mapped[bool] = mapped_column(
- pgsql_types.BOOLEAN,
- default=False,
- nullable=False
- )
-
role: Mapped[RoleEnum] = mapped_column(
pgsql_types.ENUM(
RoleEnum,
@@ -67,12 +45,6 @@ class User(Base):
nullable=False
)
- shipping_date: Mapped[datetime] = mapped_column(
- pgsql_types.TIMESTAMP(timezone=True),
- server_default=func.now(), # TODO: время заностся в бд при /start
- nullable=False
- )
-
class ProductCategory(Base):
"""БД модель продуктов и услуг."""
@@ -170,3 +142,21 @@ class ContactManager(Base):
pgsql_types.VARCHAR(25),
nullable=False
)
+
+ need_support: Mapped[bool] = mapped_column(
+ pgsql_types.BOOLEAN,
+ default=False,
+ nullable=False
+ )
+
+ need_contact_with_manager: Mapped[bool] = mapped_column(
+ pgsql_types.BOOLEAN,
+ default=False,
+ nullable=False
+ )
+
+ shipping_date: Mapped[datetime] = mapped_column(
+ pgsql_types.TIMESTAMP(timezone=True),
+ server_default=func.now(),
+ nullable=False
+ )
diff --git a/app/scripts_for_db.py b/app/scripts_for_db.py
index 6725938..4325c44 100644
--- a/app/scripts_for_db.py
+++ b/app/scripts_for_db.py
@@ -32,3 +32,14 @@
('Порталы для госучереждений', 2, 'https://www.egov.kz', 'adadasda'),
('Личные кабинеты', 2, 'https://my.gov.ru', 'ghfhfgh');
'''
+
+'''
+Скрипт тестовыхх данных для ветки посмотреть портфолио
+
+INSERT INTO checkcompanyportfolio (project_name, url) VALUES
+('Project Alpha', 'https://example.com/project-alpha'),
+('Project Beta', 'https://example.com/project-beta'),
+('Project Gamma', 'https://example.com/project-gamma'),
+('Project Delta', 'https://example.com/project-delta'),
+('Project Epsilon', 'https://example.com/project-epsilon');
+'''
From a43ed6ab9bbc626ff063d33b5d449e4490fe103c Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Sun, 6 Oct 2024 15:26:42 +0300
Subject: [PATCH 18/75] add mail servise
---
app/bot/fsm_context.py | 13 ++++++++++++-
app/bot/smtp.py | 34 ++++++++++++++++++++++++++++++++++
docker-compose.production.yml | 31 +++++++++++++++++++++++++++++++
poetry.lock | 17 ++++++++++++++++-
pyproject.toml | 1 +
5 files changed, 94 insertions(+), 2 deletions(-)
create mode 100644 app/bot/smtp.py
create mode 100644 docker-compose.production.yml
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index 3d13ea6..e5062e8 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -1,5 +1,7 @@
import logging
+import os
+import asyncio
from aiogram import F, Router
from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.context import FSMContext
@@ -7,14 +9,17 @@
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.keyborads import back_to_main_menu
-from crud.request_to_manager import create_request_to_manager
+from bot.smtp import send_mail
from bot.validators import (
is_valid_name, is_valid_phone_number, format_phone_number
)
+from crud.request_to_manager import create_request_to_manager
router = Router()
logger = logging.getLogger(__name__)
+CLIENT_EMAIL = os.getenv("EMAIL")
+
class Form(StatesGroup):
"""Форма для связи с менеджером."""
@@ -139,6 +144,12 @@ async def process_phone_number(message: Message, state: FSMContext) -> None:
logger.info(f"Запись создана в БД с ID: {new_request.id}")
+ mail = send_mail('Заявка на обратную связь', CLIENT_EMAIL, user_data)
+ asyncio.gather(asyncio.create_task(mail))
+
+ logger.info("Отправлено сообщение на почту менеджеру для связи "
+ f"с пользователем {message.from_user.id}")
+
await message.answer(
f'Спасибо! Наш менеджер свяжется '
f'с вами в ближайшее время.\n'
diff --git a/app/bot/smtp.py b/app/bot/smtp.py
new file mode 100644
index 0000000..6cf0abe
--- /dev/null
+++ b/app/bot/smtp.py
@@ -0,0 +1,34 @@
+# Отправка уведомления на почту для менеджеров
+
+import os
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+from aiosmtplib import SMTP
+from dotenv import load_dotenv
+
+load_dotenv()
+
+BASE_EMAIL = os.getenv("EMAIL")
+PASSWORD = os.getenv("PASSWORD")
+
+
+async def send_mail(subject, to, user_data):
+ text = (f'Пользователь {user_data["first_name"]} {user_data["last_name"]} '
+ f'заказал звонок по номеру {user_data["phone_number"]}')
+
+ message = MIMEMultipart()
+ message["From"] = BASE_EMAIL
+ message["To"] = to
+ message["Subject"] = subject
+ message.attach(
+ MIMEText(f"{text}", "html", "utf-8"))
+
+ smtp_client = SMTP(hostname="smtp.yandex.ru", port=465, use_tls=True)
+ async with smtp_client:
+ await smtp_client.login(BASE_EMAIL, PASSWORD)
+ await smtp_client.send_message(message)
+
+
+#if __name__ == '__main__':
+# asyncio.run(send_mail('Тесты', EMAIL, 'Как оно?)
'))
diff --git a/docker-compose.production.yml b/docker-compose.production.yml
new file mode 100644
index 0000000..6feaf8d
--- /dev/null
+++ b/docker-compose.production.yml
@@ -0,0 +1,31 @@
+version: '3.3'
+
+volumes:
+ pg_data:
+
+services:
+ db:
+ container_name: scid-db
+ image: postgres:13
+ env_file: .env
+ volumes:
+ - pg_data:/var/lib/postgresql/data
+
+ bot:
+ depends_on:
+ - db
+ container_name: scid-bot
+ build: .
+ #image: greenvibe/scid_bot
+ env_file: .env
+
+ gateway:
+ depends_on:
+ - bot
+ container_name: scid-gateway
+ image: nginx:1.22.1-alpine
+ env_file: .env
+ volumes:
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf
+ ports:
+ - 8000:80
diff --git a/poetry.lock b/poetry.lock
index a680364..73b83e9 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -177,6 +177,21 @@ files = [
[package.dependencies]
frozenlist = ">=1.1.0"
+[[package]]
+name = "aiosmtplib"
+version = "3.0.2"
+description = "asyncio SMTP client"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "aiosmtplib-3.0.2-py3-none-any.whl", hash = "sha256:8783059603a34834c7c90ca51103c3aa129d5922003b5ce98dbaa6d4440f10fc"},
+ {file = "aiosmtplib-3.0.2.tar.gz", hash = "sha256:08fd840f9dbc23258025dca229e8a8f04d2ccf3ecb1319585615bfc7933f7f47"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.0.0)", "sphinx-autodoc-typehints (>=1.24.0)", "sphinx-copybutton (>=0.5.0)"]
+uvloop = ["uvloop (>=0.18)"]
+
[[package]]
name = "aiosqlite"
version = "0.20.0"
@@ -1143,4 +1158,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
-content-hash = "90db1da4a85768319a24a7ef08be47bdc8d66328bc97ac041a05bc38eb1bea6c"
+content-hash = "8ec858bde1d44f558a848ecdcd82c3bf80e750733c333fea5e6848ebf037d263"
diff --git a/pyproject.toml b/pyproject.toml
index c79104c..9994c5b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,6 +15,7 @@ sqlalchemy = "^2.0.35"
pydantic-settings = "^2.5.2"
asyncpg = "^0.29.0"
psycopg2-binary = "^2.9.9"
+aiosmtplib = "^3.0.2"
[tool.poetry.group.dev.dependencies]
aiosqlite = "^0.20.0"
From 465023d5d13d19f0109c1017d9efd33ca09333a4 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Mon, 7 Oct 2024 14:50:09 +0300
Subject: [PATCH 19/75] merge two direction
---
.env.example | 3 +
.../d2365dc6d2ca_merge_2_direction.py | 44 ++
app/admin/filters/filters.py | 19 +
app/admin/handlers/admin_handlers/__init__.py | 16 +
app/admin/handlers/admin_handlers/admin.py | 429 ++++++++++++++++++
.../admin_about_company_handlers.py | 202 +++++++++
.../admin_handlers/admin_category_handlers.py | 357 +++++++++++++++
.../admin_handlers/admin_info_handlers.py | 236 ++++++++++
.../admin_portfolio_handlers.py | 265 +++++++++++
.../admin_handlers/admin_product_handlers.py | 356 +++++++++++++++
app/admin/handlers/user.py | 321 +++++++++++++
app/admin/handlers/validators.py | 9 +
app/admin/keyboards/__init__.py | 0
app/admin/keyboards/keyboards.py | 222 +++++++++
app/bot/callbacks.py | 35 +-
app/bot/fsm_context.py | 10 +-
app/bot/handlers.py | 25 +-
app/bot/keyborads.py | 20 +-
app/const.py | 94 ++++
app/core/base.py | 11 +-
app/core/db.py | 9 +-
app/core/init_db.py | 14 +
app/core/settings.py | 1 +
app/crud/about_crud.py | 42 ++
app/crud/base_crud.py | 51 +++
app/crud/category_product.py | 50 ++
app/crud/feedback_crud.py | 57 +++
app/crud/info_crud.py | 32 ++
app/crud/portfolio_projects_crud.py | 22 +
app/crud/product_crud.py | 29 ++
app/crud/projects.py | 56 +--
app/crud/questions.py | 30 +-
app/crud/request_to_manager.py | 25 +-
app/crud/user_crud.py | 68 +++
app/crud/users.py | 40 +-
app/main.py | 15 +-
app/models/models.py | 48 +-
37 files changed, 3129 insertions(+), 134 deletions(-)
create mode 100644 .env.example
create mode 100644 alembic/versions/d2365dc6d2ca_merge_2_direction.py
create mode 100644 app/admin/filters/filters.py
create mode 100644 app/admin/handlers/admin_handlers/__init__.py
create mode 100644 app/admin/handlers/admin_handlers/admin.py
create mode 100644 app/admin/handlers/admin_handlers/admin_about_company_handlers.py
create mode 100644 app/admin/handlers/admin_handlers/admin_category_handlers.py
create mode 100644 app/admin/handlers/admin_handlers/admin_info_handlers.py
create mode 100644 app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
create mode 100644 app/admin/handlers/admin_handlers/admin_product_handlers.py
create mode 100644 app/admin/handlers/user.py
create mode 100644 app/admin/handlers/validators.py
create mode 100644 app/admin/keyboards/__init__.py
create mode 100644 app/admin/keyboards/keyboards.py
create mode 100644 app/const.py
create mode 100644 app/core/init_db.py
create mode 100644 app/crud/about_crud.py
create mode 100644 app/crud/base_crud.py
create mode 100644 app/crud/category_product.py
create mode 100644 app/crud/feedback_crud.py
create mode 100644 app/crud/info_crud.py
create mode 100644 app/crud/portfolio_projects_crud.py
create mode 100644 app/crud/product_crud.py
create mode 100644 app/crud/user_crud.py
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..1a6a451
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+DATABASE_URL=<'DATABASE URL'>
+TELEGRAM_TOKEN=<'TELEGRAM TOKEN'>
+TELEGRAM_CHAT_IDS=<'TELEGRAM CHAT IDS'>
\ No newline at end of file
diff --git a/alembic/versions/d2365dc6d2ca_merge_2_direction.py b/alembic/versions/d2365dc6d2ca_merge_2_direction.py
new file mode 100644
index 0000000..758d7a4
--- /dev/null
+++ b/alembic/versions/d2365dc6d2ca_merge_2_direction.py
@@ -0,0 +1,44 @@
+"""Merge 2 direction
+
+Revision ID: d2365dc6d2ca
+Revises: 31916cbb37e1
+Create Date: 2024-10-07 12:56:05.967698
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision: str = 'd2365dc6d2ca'
+down_revision: Union[str, None] = '31916cbb37e1'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('feedback',
+ sa.Column('user', sa.Integer(), nullable=False),
+ sa.Column('feedback_text', sa.TEXT(), nullable=False),
+ sa.Column('feedback_date', postgresql.TIMESTAMP(), nullable=False),
+ sa.Column('unread', sa.BOOLEAN(), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['user'], ['user.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.drop_constraint('categorytype_product_id_fkey', 'categorytype', type_='foreignkey')
+ op.create_foreign_key(None, 'categorytype', 'productcategory', ['product_id'], ['id'], ondelete='CASCADE')
+ op.add_column('contactmanager', sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False))
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('contactmanager', 'shipping_date_close')
+ op.drop_constraint(None, 'categorytype', type_='foreignkey')
+ op.create_foreign_key('categorytype_product_id_fkey', 'categorytype', 'productcategory', ['product_id'], ['id'])
+ op.drop_table('feedback')
+ # ### end Alembic commands ###
diff --git a/app/admin/filters/filters.py b/app/admin/filters/filters.py
new file mode 100644
index 0000000..5dad1f5
--- /dev/null
+++ b/app/admin/filters/filters.py
@@ -0,0 +1,19 @@
+from aiogram.filters import Filter
+from aiogram import Bot, types
+# from settings import admin_list
+
+
+class ChatTypeFilter(Filter):
+ def __init__(self, chat_types: list[str]) -> None:
+ self.chat_types = chat_types
+
+ async def __call__(self, message: types.Message) -> bool:
+ return message.chat.type in self.chat_types
+
+
+class IsAdmin(Filter):
+ def __init__(self) -> None:
+ pass
+
+ # async def __call__(self, message: types.Message, bot: Bot) -> bool:
+ # return message.from_user.id in admin_list
diff --git a/app/admin/handlers/admin_handlers/__init__.py b/app/admin/handlers/admin_handlers/__init__.py
new file mode 100644
index 0000000..890363a
--- /dev/null
+++ b/app/admin/handlers/admin_handlers/__init__.py
@@ -0,0 +1,16 @@
+from aiogram import Router
+
+from .admin import admin_main_router
+from .admin_info_handlers import info_router
+from .admin_about_company_handlers import about_router
+from .admin_portfolio_handlers import portfolio_router
+from .admin_product_handlers import product_router
+from .admin_category_handlers import category_router
+
+admin_router = Router()
+admin_router.include_router(admin_main_router)
+admin_router.include_router(info_router)
+admin_router.include_router(about_router)
+admin_router.include_router(portfolio_router)
+admin_router.include_router(product_router)
+admin_router.include_router(category_router)
diff --git a/app/admin/handlers/admin_handlers/admin.py b/app/admin/handlers/admin_handlers/admin.py
new file mode 100644
index 0000000..f136584
--- /dev/null
+++ b/app/admin/handlers/admin_handlers/admin.py
@@ -0,0 +1,429 @@
+from aiogram import Router, F
+from aiogram.types import CallbackQuery, Message
+from aiogram.filters import or_f
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from crud.feedback_crud import feedback_crud
+from crud.user_crud import user_crud
+from filters.filters import ChatTypeFilter, IsAdmin
+from keyboards.keyboards import (
+ get_inline_keyboard,
+ get_inline_paginated_keyboard,
+ get_paginated_keyboard_size,
+)
+from const import (
+ ADMIN_BASE_KEYBOARD,
+ ADMIN_BASE_REPLY_OPTIONS,
+ BASE_BUTTONS,
+ FEEDBACK_PAGINATION,
+ MAIN_MENU_BUTTONS,
+ MAIN_MENU_OPTIONS,
+ ADMIN_PORTFOLIO_KEYBOARD,
+ PORTFOLIO_BUTTONS,
+ PORTFOLIO_MENU_OPTIONS,
+ SUPPORT_OPTIONS,
+ SUPPROT_MENU_BUTTONS,
+ USER_CALLBACK_PAGINATION,
+)
+
+
+admin_main_router = Router()
+admin_main_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+
+
+class UserState(StatesGroup):
+ callback = State()
+ close_case = State()
+
+
+class FeedbackState(StatesGroup):
+ feedback_choice = State()
+ new_feedbacks = State()
+ all_feedbacks = State()
+ mark_as_read = State()
+
+
+class SectionState(StatesGroup):
+ """State для определения раздела, в который вносятся измения."""
+
+ faq = State()
+ troubleshooting = State()
+ portfolio = State()
+ other_projects = State()
+ about = State()
+ product = State()
+ callback_request = State()
+ category = State()
+
+ @classmethod
+ def get_condition(cls, menu_text: str):
+ """Выбрать категорию для раздела."""
+
+ if menu_text == SUPPORT_OPTIONS.get("faq"):
+ return cls.faq
+ elif menu_text == SUPPORT_OPTIONS.get("troubleshooting"):
+ return cls.troubleshooting
+ elif menu_text == SUPPORT_OPTIONS.get("callback_request"):
+ return cls.callback_request
+ elif menu_text == MAIN_MENU_OPTIONS.get("company_bio"):
+ return cls.about
+ elif menu_text == MAIN_MENU_OPTIONS.get("products"):
+ return cls.product
+ elif menu_text == MAIN_MENU_OPTIONS.get("portfolio"):
+ return cls.portfolio
+ elif menu_text == PORTFOLIO_MENU_OPTIONS.get("other_projects"):
+ return cls.other_projects
+ else:
+ return cls.category
+
+
+@admin_main_router.callback_query(F.data.endswith("_"))
+async def update_category(callback: CallbackQuery, state: FSMContext):
+ """Админские кнопки для внесения изменений в разделы."""
+
+ menu = callback.data.rstrip("_")
+
+ if menu == MAIN_MENU_OPTIONS.get("portfolio"):
+ admin_keyboard = ADMIN_PORTFOLIO_KEYBOARD
+ elif (
+ (menu not in MAIN_MENU_BUTTONS)
+ and (menu not in SUPPROT_MENU_BUTTONS)
+ and (menu not in PORTFOLIO_BUTTONS)
+ ):
+ menu = "Назад"
+ admin_keyboard = ADMIN_BASE_KEYBOARD
+ else:
+ admin_keyboard = ADMIN_BASE_KEYBOARD
+
+ await callback.message.edit_text(
+ "Выберете действие:",
+ reply_markup=await get_inline_keyboard(
+ options=admin_keyboard, previous_menu=menu
+ ),
+ )
+
+ await state.set_state(SectionState.get_condition(menu))
+
+
+@admin_main_router.message(
+ F.text == ADMIN_BASE_REPLY_OPTIONS.get("callback_case"),
+)
+async def get_callback_cases_from_text(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+
+ user_list = await user_crud.get_users_with_callback_request(session)
+ users_by_name = [user.name for user in user_list]
+ users_by_id = [user.id for user in user_list]
+
+ await message.delete()
+ await message.answer(
+ "Список пользователей, ожидающих обратного звонка",
+ reply_markup=await get_inline_paginated_keyboard(
+ options=users_by_name,
+ callback=users_by_id,
+ items_per_page=USER_CALLBACK_PAGINATION,
+ size=get_paginated_keyboard_size(USER_CALLBACK_PAGINATION),
+ previous_menu=BASE_BUTTONS.get("main_menu"),
+ previous_menu_text=BASE_BUTTONS.get("main_menu"),
+ ),
+ )
+
+ await state.set_state(UserState.callback)
+
+
+@admin_main_router.callback_query(UserState(), F.data.startswith("page:"))
+async def get_callback_cases(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+
+ user_list = await user_crud.get_users_with_callback_request(session)
+ users_by_name = [user.name for user in user_list]
+ users_by_id = [user.id for user in user_list]
+ current_page = int(callback.data.split(":")[1])
+
+ await callback.message.edit_text(
+ "Список пользователей, ожидающих обратного звонка",
+ reply_markup=await get_inline_paginated_keyboard(
+ options=users_by_name,
+ callback=users_by_id,
+ items_per_page=USER_CALLBACK_PAGINATION,
+ current_page=current_page,
+ size=get_paginated_keyboard_size(USER_CALLBACK_PAGINATION),
+ previous_menu=BASE_BUTTONS.get("main_menu"),
+ previous_menu_text=BASE_BUTTONS.get("main_menu"),
+ ),
+ )
+
+ await state.set_state(UserState.callback)
+
+
+@admin_main_router.callback_query(
+ UserState.callback, F.data != BASE_BUTTONS.get("main_menu")
+)
+async def user_callback_request_data(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Закрыть заявку на обратный звонок."""
+
+ user = await user_crud.get(callback.data, session)
+
+ await callback.message.edit_text(
+ f"Пользователь: {user.name}\n\n Телефон: {user.phone}",
+ reply_markup=await get_inline_keyboard(
+ options=["Закрыть заявку", "Назад к списку"],
+ callback=[callback.data, "page:1"],
+ ),
+ )
+
+ await state.set_state(UserState.close_case)
+
+
+@admin_main_router.callback_query(UserState.close_case, F.data)
+async def close_case(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Закрыть заявку на обратный звонок."""
+
+ user = await user_crud.get(callback.data, session)
+
+ await user_crud.close_case(user, session)
+ await callback.message.edit_text(
+ "Заявка закрыта!",
+ reply_markup=await get_inline_keyboard(
+ ["Назад к списку"],
+ callback=[
+ "page:1",
+ ],
+ ),
+ )
+
+ await state.set_state(UserState.callback)
+
+
+@admin_main_router.message(
+ F.text == ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
+)
+async def feedback_choice(message: Message, state: FSMContext):
+ """Вывести выбор списка отзывов."""
+
+ await message.delete()
+ await message.answer(
+ "Какие отзывы показать?",
+ reply_markup=await get_inline_keyboard(
+ options=[
+ "Только новые отзывы",
+ "Все отзывы",
+ BASE_BUTTONS.get("main_menu"),
+ ],
+ ),
+ )
+
+ await state.set_state(FeedbackState.feedback_choice)
+
+
+@admin_main_router.callback_query(
+ F.data == ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
+)
+async def feedback_choice_callback(callback: CallbackQuery, state: FSMContext):
+ """Вывести выбор списка отзывов."""
+
+ await callback.message.edit_text(
+ "Какие отзывы показать?",
+ reply_markup=await get_inline_keyboard(
+ options=[
+ "Только новые отзывы",
+ "Все отзывы",
+ BASE_BUTTONS.get("main_menu"),
+ ],
+ ),
+ )
+
+ await state.set_state(FeedbackState.feedback_choice)
+
+
+@admin_main_router.callback_query(
+ FeedbackState.feedback_choice,
+ F.data == "Только новые отзывы",
+)
+async def get_unread_feedbacks(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+
+ new_feedbacks = await feedback_crud.get_new_feedbacks(session)
+ feedbacks_list = [
+ f"Отзыв номер {feedback.id} от {feedback.feedback_date}"
+ for feedback in new_feedbacks
+ ]
+ feedbacks_ids = [feedback.id for feedback in new_feedbacks]
+
+ await callback.message.edit_text(
+ "Только новые отзывы",
+ reply_markup=await get_inline_paginated_keyboard(
+ options=feedbacks_list,
+ callback=feedbacks_ids,
+ items_per_page=FEEDBACK_PAGINATION,
+ size=get_paginated_keyboard_size(FEEDBACK_PAGINATION),
+ previous_menu=ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
+ ),
+ )
+
+ await state.set_state(FeedbackState.new_feedbacks)
+
+
+@admin_main_router.callback_query(
+ or_f(
+ FeedbackState.feedback_choice,
+ FeedbackState.new_feedbacks,
+ FeedbackState.mark_as_read,
+ ),
+ F.data.startswith("page:"),
+)
+async def get_paginated_unread_feedbacks(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+
+ new_feedbacks = await feedback_crud.get_new_feedbacks(session)
+ feedbacks_list = [
+ f"Отзыв номер {feedback.id} от {feedback.feedback_date}"
+ for feedback in new_feedbacks
+ ]
+ feedbacks_ids = [feedback.id for feedback in new_feedbacks]
+ current_page = int(callback.data.split(":")[1])
+
+ await callback.message.edit_text(
+ "Только новые отзывы",
+ reply_markup=await get_inline_paginated_keyboard(
+ options=feedbacks_list,
+ callback=feedbacks_ids,
+ current_page=current_page,
+ items_per_page=FEEDBACK_PAGINATION,
+ size=get_paginated_keyboard_size(FEEDBACK_PAGINATION),
+ previous_menu=ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
+ ),
+ )
+
+ await state.set_state(FeedbackState.new_feedbacks)
+
+
+@admin_main_router.callback_query(
+ FeedbackState.feedback_choice,
+ F.data == "Все отзывы",
+)
+async def get_all_feedbacks(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+
+ feedbacks = await feedback_crud.get_multi(session)
+ feedbacks_list = [
+ f"Отзыв номер {feedback.id} от {feedback.feedback_date}"
+ for feedback in feedbacks
+ ]
+ feedbacks_ids = [feedback.id for feedback in feedbacks]
+
+ await callback.message.edit_text(
+ "Все отзывы",
+ reply_markup=await get_inline_paginated_keyboard(
+ options=feedbacks_list,
+ callback=feedbacks_ids,
+ items_per_page=FEEDBACK_PAGINATION,
+ size=get_paginated_keyboard_size(FEEDBACK_PAGINATION),
+ previous_menu=ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
+ ),
+ )
+
+ await state.set_state(FeedbackState.all_feedbacks)
+
+
+@admin_main_router.callback_query(
+ or_f(
+ FeedbackState.feedback_choice,
+ FeedbackState.all_feedbacks,
+ ),
+ F.data.startswith("page:"),
+)
+async def get_all_paginated_feedbacks(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+
+ feedbacks = await feedback_crud.get_multi(session)
+ feedbacks_list = [
+ f"Отзыв номер {feedback.id} от {feedback.feedback_date}"
+ for feedback in feedbacks
+ ]
+ feedbacks_ids = [feedback.id for feedback in feedbacks]
+ current_page = int(callback.data.split(":")[1])
+
+ await callback.message.edit_text(
+ "Все отзывы",
+ reply_markup=await get_inline_paginated_keyboard(
+ options=feedbacks_list,
+ callback=feedbacks_ids,
+ current_page=current_page,
+ items_per_page=FEEDBACK_PAGINATION,
+ size=get_paginated_keyboard_size(FEEDBACK_PAGINATION),
+ previous_menu=ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
+ ),
+ )
+
+ await state.set_state(FeedbackState.all_feedbacks)
+
+
+@admin_main_router.callback_query(
+ or_f(FeedbackState.all_feedbacks, FeedbackState.new_feedbacks),
+ F.data != BASE_BUTTONS.get("main_menu"),
+)
+async def get_feedback(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Получить данные отзыва."""
+
+ feedback = await feedback_crud.get(callback.data, session)
+ feedback_data = (
+ f"Отзыв от пользователя {feedback.author.name}:\n\n"
+ f"{feedback.feedback_text}\n\n Дата: {feedback.feedback_date}"
+ )
+
+ if feedback.unread:
+ await callback.message.edit_text(
+ feedback_data,
+ reply_markup=await get_inline_keyboard(
+ options=["Отметить как прочитанный", "Обратно к отзывам"],
+ callback=[feedback.id, "page:1"],
+ ),
+ )
+
+ await state.set_state(FeedbackState.mark_as_read)
+ else:
+ await callback.message.edit_text(
+ feedback_data,
+ reply_markup=await get_inline_keyboard(
+ options=[
+ "Обратно к отзывам",
+ ],
+ callback=[
+ "page:1",
+ ],
+ ),
+ )
+
+
+@admin_main_router.callback_query(FeedbackState.mark_as_read, F.data)
+async def mark_as_read(callback: CallbackQuery, session: AsyncSession):
+
+ feedback = await feedback_crud.get(callback.data, session)
+
+ await feedback_crud.mark_as_read(feedback, session)
+ await callback.message.edit_text(
+ "Отзыв отмечен как прочитанный!",
+ reply_markup=await get_inline_keyboard(
+ options=[
+ "Обратно к отзывам",
+ ],
+ callback=[
+ "Только новые отзывы",
+ ],
+ ),
+ )
diff --git a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
new file mode 100644
index 0000000..5ab413f
--- /dev/null
+++ b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
@@ -0,0 +1,202 @@
+from aiogram import F, Router
+from aiogram.filters import and_f, or_f
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from aiogram.types import CallbackQuery, Message
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from .admin import SectionState
+from crud.about_crud import company_info_crud
+from filters.filters import ChatTypeFilter, IsAdmin
+from keyboards.keyboards import (
+ get_inline_confirmation_keyboard,
+ get_inline_keyboard,
+)
+# from settings import MAIN_MENU_OPTIONS
+
+MAIN_MENU_OPTIONS = {
+ "company_bio": "Информация о компании",
+ "products": "Продукты и услуги",
+ "support": "Техническая поддержка",
+ "portfolio": "Портфолио",
+ "request_callback": "Связаться с менеджером",
+}
+
+about_router = Router()
+about_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+
+
+class AddAboutInfo(StatesGroup):
+ name = State()
+ url = State()
+
+
+class UpdateAboutInfo(StatesGroup):
+ select = State()
+ name = State()
+ url = State()
+ confirm = State()
+
+
+class DeleteAboutInfo(AddAboutInfo):
+ confirm = State()
+
+
+PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("company_bio")
+
+
+@about_router.callback_query(SectionState.about, F.data == "Добавить")
+async def create_about_info(callback: CallbackQuery, state: FSMContext):
+ await callback.message.answer("Введите название для ссылки:")
+ await state.set_state(AddAboutInfo.name)
+
+
+@about_router.message(AddAboutInfo.name, F.text)
+async def add_info_name(message: Message, state: FSMContext):
+ await state.update_data(name=message.text)
+ await message.answer("Добавьте ссылку:")
+ await state.set_state(AddAboutInfo.url)
+
+
+@about_router.message(AddAboutInfo.url, F.text)
+async def add_about_data(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ await state.update_data(url=message.text)
+ data = await state.get_data()
+ await company_info_crud.create(data, session)
+ await message.answer(
+ "Информация добавлена!",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+ await state.clear()
+
+
+@about_router.callback_query(SectionState.about, F.data == "Удалить")
+async def about_info_to_delete(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ about_data = await company_info_crud.get_multi(session)
+ info_list = [info.name for info in about_data]
+ await callback.message.answer(
+ "Какую информацию вы хотите удалить?",
+ reply_markup=await get_inline_keyboard(
+ info_list, previous_menu=PREVIOUS_MENU
+ ),
+ )
+ await state.set_state(DeleteAboutInfo.name)
+
+
+@about_router.callback_query(DeleteAboutInfo.name, F.data)
+async def confirm_delete_info(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ about_data = await company_info_crud.get_by_about_name(
+ callback.data, session
+ )
+ await callback.message.edit_text(
+ f"Вы уверены, что хотите удалить эту информацию?\n\n {about_data.name}",
+ reply_markup=await get_inline_confirmation_keyboard(
+ option=about_data.name, cancel_option=PREVIOUS_MENU
+ ),
+ )
+ await state.set_state(DeleteAboutInfo.confirm)
+
+
+@about_router.callback_query(DeleteAboutInfo.confirm, F.data != PREVIOUS_MENU)
+async def delete_about_info(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ about_data = await company_info_crud.get_by_about_name(
+ callback.data, session
+ )
+ await company_info_crud.remove(about_data, session)
+ await callback.message.edit_text(
+ "Информация удалена!",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+ await state.clear()
+
+
+@about_router.callback_query(SectionState.about, F.data == "Изменить")
+async def about_info_to_update(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ about_data = await company_info_crud.get_multi(session)
+ info_list = [info.name for info in about_data]
+ await callback.message.edit_text(
+ "Какую информацию вы хотите отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ info_list, previous_menu=PREVIOUS_MENU
+ ),
+ )
+ await state.set_state(UpdateAboutInfo.select)
+
+
+@about_router.callback_query(
+ UpdateAboutInfo.select,
+ and_f(
+ F.data != "Название ссылки",
+ F.data != "Адрес ссылки",
+ F.data != PREVIOUS_MENU,
+ ),
+)
+async def update_info_choise(callback: CallbackQuery, state: FSMContext):
+ await state.update_data(select=callback.data)
+ await callback.message.edit_text(
+ "Что вы хотите отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ ["Название ссылки", "Адрес ссылки"], previous_menu=PREVIOUS_MENU
+ ),
+ )
+
+
+@about_router.callback_query(
+ UpdateAboutInfo.select, F.data == "Название ссылки"
+)
+async def about_name_update(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ about_name = await state.get_data()
+ about_name_text = about_name.get("select")
+ await callback.message.answer(
+ f"Сейчас у ссылки такое название:\n\n {about_name_text}\n\n Введите новое название"
+ )
+ await state.set_state(UpdateAboutInfo.name)
+
+
+@about_router.callback_query(UpdateAboutInfo.select, F.data == "Адрес ссылки")
+async def about_url_update(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ about_name_data = await state.get_data()
+ about_name_text = about_name_data.get("select")
+ about_info = await company_info_crud.get_by_about_name(
+ about_name_text, session
+ )
+ await callback.message.answer(
+ f"Сейчас у ссылки такой адрес:\n\n {about_info.url}\n\n Введите новое название"
+ )
+ await state.set_state(UpdateAboutInfo.url)
+
+
+@about_router.message(or_f(UpdateAboutInfo.name, UpdateAboutInfo.url), F.text)
+async def update_about_info(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ current_state = await state.get_state()
+ old_data = await state.get_data()
+ old_about_data = await company_info_crud.get_by_about_name(
+ old_data.get("select"), session
+ )
+ if current_state == UpdateAboutInfo.name:
+ await state.update_data(name=message.text)
+ elif current_state == UpdateAboutInfo.url:
+ await state.update_data(url=message.text)
+ update_data = await state.get_data()
+ await company_info_crud.update(old_about_data, update_data, session)
+ await message.answer(
+ "Информация обновлена!",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+ await state.clear()
diff --git a/app/admin/handlers/admin_handlers/admin_category_handlers.py b/app/admin/handlers/admin_handlers/admin_category_handlers.py
new file mode 100644
index 0000000..7d65fa9
--- /dev/null
+++ b/app/admin/handlers/admin_handlers/admin_category_handlers.py
@@ -0,0 +1,357 @@
+from aiogram import F, Router
+from aiogram.filters import or_f, and_f
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from aiogram.types import CallbackQuery, Message
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from handlers.admin_handlers.admin import SectionState
+from crud.category_product import category_product_crud
+from crud.product_crud import product_crud
+from filters.filters import ChatTypeFilter, IsAdmin
+from handlers.user import ProductCategory
+from keyboards.keyboards import (
+ get_inline_confirmation_keyboard,
+ get_inline_keyboard,
+)
+# from settings import MAIN_MENU_OPTIONS, admin_list
+
+MAIN_MENU_OPTIONS = {
+ "company_bio": "Информация о компании",
+ "products": "Продукты и услуги",
+ "support": "Техническая поддержка",
+ "portfolio": "Портфолио",
+ "request_callback": "Связаться с менеджером",
+}
+
+category_router = Router()
+category_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+
+
+class AddCategory(StatesGroup):
+ name = State()
+ url = State()
+ media = State()
+ description = State()
+
+
+class UpdateCategory(StatesGroup):
+ name = State()
+ url = State()
+ media = State()
+ description = State()
+ select = State()
+
+
+class DeleteCategory(AddCategory):
+ confirm = State()
+
+
+async def get_category_list(state: FSMContext, session: AsyncSession):
+ """Получить список вариантов для проекта."""
+ fsm_data = await state.get_data()
+ product_id = fsm_data.get("product_id")
+ return [
+ category
+ for category in await category_product_crud.get_multi_for_product(
+ product_id, session
+ )
+ ]
+
+
+async def get_category_by_name(
+ field_name: str, state: FSMContext, session: AsyncSession
+):
+ fsm_data = await state.get_data()
+ product_id = fsm_data.get("product_id")
+ return await category_product_crud.get_category_by_name(
+ product_id=product_id, field_name=field_name, session=session
+ )
+
+
+@category_router.callback_query(
+ or_f(
+ AddCategory(),
+ UpdateCategory(),
+ DeleteCategory(),
+ SectionState.category,
+ ),
+ or_f(F.data == "Назад", F.data == "Отмена"),
+)
+async def get_back_to_category_menu(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Корутина для реализации кнопки 'Назад'."""
+ fsm_data = await state.get_data()
+ product = await product_crud.get(fsm_data.get("product_id"), session)
+ categories = await category_product_crud.get_category_by_product_id(
+ fsm_data.get("product_id"), session
+ )
+ categories_by_name = [category.name for category in categories]
+ urls = [category.url for category in categories]
+ await callback.message.edit_text(
+ f"{product.response}",
+ reply_markup=await get_inline_keyboard(
+ categories_by_name,
+ urls=urls,
+ previous_menu=MAIN_MENU_OPTIONS.get("products"),
+ is_admin=callback.from_user.id in admin_list,
+ admin_update_menu=callback.data,
+ ),
+ )
+ await state.set_state(ProductCategory.product_id)
+
+
+@category_router.callback_query(SectionState.category, F.data == "Добавить")
+async def add_new_category(callback: CallbackQuery, state: FSMContext):
+ """Добавить основные варианты для продукта."""
+ await callback.message.answer(
+ "Введите название для дополнительной информации",
+ reply_markup=await get_inline_keyboard(previous_menu="Назад"),
+ )
+ await state.set_state(AddCategory.name)
+
+
+@category_router.message(AddCategory.name, F.text)
+async def add_product_category_name(
+ message: Message,
+ state: FSMContext,
+):
+ """Выбрать тип данных для основных вариантов."""
+
+ await state.update_data(name=message.text)
+ await message.answer(
+ "Выберете способ передачи информации:",
+ reply_markup=await get_inline_keyboard(
+ ["Ссылка", "Текст", "Картинка"],
+ previous_menu="Назад",
+ ),
+ )
+
+
+@category_router.callback_query(
+ or_f(AddCategory.name, AddCategory.description),
+ or_f(F.data == "Ссылка", F.data == "Текст", F.data == "Картинка"),
+)
+async def add_product_category_data(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Добавить информацию в основной вариант."""
+
+ if callback.data == "Ссылка":
+ info_type = "ссылку"
+ await state.set_state(AddCategory.url)
+ elif callback.data == "Текст":
+ info_type = "текст"
+ fsm_data = await state.get_data()
+ category_name = fsm_data.get("select")
+ product_id = fsm_data.get("product_id")
+ category = await category_product_crud.get_category_by_name(
+ product_id, category_name, session
+ )
+ if category:
+ await state.set_state(UpdateCategory.description)
+ else:
+ await state.set_state(AddCategory.description)
+ elif callback.data == "Картинка":
+ info_type = "Картинку"
+ await state.set_state(AddCategory.media)
+ await callback.message.answer(
+ f"Добавьте {info_type}",
+ reply_markup=await get_inline_keyboard(previous_menu="Назад"),
+ )
+
+
+@category_router.message(
+ or_f(AddCategory.media, UpdateCategory.media), F.photo
+)
+async def add_media_description(message: Message, state: FSMContext):
+ """Добавить описание к картинке."""
+ await state.update_data(media=message.photo[-1].file_id)
+ await message.answer(
+ "Добавить описание к картинке?",
+ reply_markup=await get_inline_confirmation_keyboard(
+ "Текст", cancel_option="Нет"
+ ),
+ )
+ await state.set_state(AddCategory.description)
+
+
+@category_router.message(
+ or_f(AddCategory.description, AddCategory.url),
+ or_f(F.text, F.photo, F.data == "Нет"),
+)
+async def create_product_with_data(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ """Создать вариант для продукта в БД и предложить добавить следующий."""
+ current_state = await state.get_state()
+ if current_state == AddCategory.description:
+ await state.update_data(description=message.text)
+ elif current_state == AddCategory.url:
+ await state.update_data(url=message.text)
+ data = await state.get_data()
+ await category_product_crud.create(data, session)
+ await message.answer(
+ "Информация добавлена!",
+ reply_markup=await get_inline_keyboard(previous_menu="Назад"),
+ )
+
+
+@category_router.callback_query(SectionState.category, F.data == "Удалить")
+async def product_to_delete(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Выбор продукта на удаление."""
+ categories = [
+ category.name for category in await get_category_list(state, session)
+ ]
+ await callback.message.edit_text(
+ "Какой проект вы хотите удалить?",
+ reply_markup=await get_inline_keyboard(
+ options=categories,
+ previous_menu="Назад",
+ ),
+ )
+ await state.set_state(DeleteCategory.name)
+
+
+@category_router.callback_query(DeleteCategory.name, F.data)
+async def confirm_delete(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Подтверждение удаления."""
+ category = await get_category_by_name(callback.data, state, session)
+ await callback.message.edit_text(
+ f"Вы уверены, что хотите удалить этот проект?\n\n {category.name}",
+ reply_markup=await get_inline_confirmation_keyboard(
+ option=category.name, cancel_option="Назад"
+ ),
+ )
+ await state.set_state(DeleteCategory.confirm)
+
+
+@category_router.callback_query(DeleteCategory.confirm, F.data != "Назад")
+async def delete_product(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Удалить продукт из БД."""
+ category = await get_category_by_name(callback.data, state, session)
+ await category_product_crud.remove(category, session)
+ await callback.message.edit_text(
+ "Услуга удалена!",
+ reply_markup=await get_inline_keyboard(previous_menu="Назад"),
+ )
+
+
+@category_router.callback_query(SectionState.category, F.data == "Изменить")
+async def product_to_update(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Выбор продукта для редактирования."""
+ categories = [
+ category.name for category in await get_category_list(state, session)
+ ]
+ await callback.message.edit_text(
+ "Какую услугу вы хотите отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ options=categories, previous_menu="Назад"
+ ),
+ )
+ await state.set_state(UpdateCategory.select)
+
+
+@category_router.callback_query(
+ UpdateCategory.select,
+ and_f(F.data != "Название", F.data != "Содержание"),
+)
+async def update_portfolio_project_choise(
+ callback: CallbackQuery, state: FSMContext
+):
+ """Выбор поля для редактирования."""
+ await state.update_data(select=callback.data)
+ await callback.message.edit_text(
+ "Что вы хотите отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ ["Название", "Содержание"], previous_menu="Назад"
+ ),
+ )
+
+
+@category_router.callback_query(UpdateCategory.select, F.data == "Название")
+async def about_name_update(callback: CallbackQuery, state: FSMContext):
+ """Ввести новое название продукта."""
+ fsm_data = await state.get_data()
+ category_name = fsm_data.get("select")
+ await callback.message.answer(
+ f"Текущее название:\n\n {category_name}\n\n Введите новое название"
+ )
+ await state.set_state(UpdateCategory.name)
+
+
+@category_router.callback_query(UpdateCategory.select, F.data == "Содержание")
+async def about_url_update(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Изменить содержание продукта."""
+ fsm_data = await state.get_data()
+ category_choice = fsm_data.get("select")
+ category = await get_category_by_name(category_choice, state, session)
+ if category.url:
+ await callback.message.answer(
+ f"Текущий адрес ссылки:\n\n {category.url}\n\n Введите новый адрес ссылки:"
+ )
+ await state.set_state(UpdateCategory.url)
+ if category.description and not category.media:
+ await callback.message.answer(
+ f"Текущий текст:\n\n {category.description}\n\n Введите новый текст:"
+ )
+ await state.set_state(UpdateCategory.description)
+ if category.media:
+ await callback.message.answer("Текущая картинка:")
+ await callback.message.answer_photo(
+ photo=category.media, caption=category.description
+ )
+ await callback.message.answer(
+ "Добавьте новую картинку и описание",
+ reply_markup=await get_inline_keyboard(previous_menu="Назад"),
+ )
+ await state.set_state(UpdateCategory.media)
+
+
+@category_router.message(
+ or_f(
+ UpdateCategory.name,
+ UpdateCategory.media,
+ UpdateCategory.url,
+ UpdateCategory.description,
+ ),
+ or_f(
+ F.text,
+ F.photo,
+ ),
+)
+async def update_about_info(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ """Внести изменения продукта в БД."""
+ current_state = await state.get_state()
+ old_data = await state.get_data()
+ print(old_data)
+ old_category_data = await get_category_by_name(
+ old_data.get("select"), state, session
+ )
+ if current_state == UpdateCategory.name:
+ await state.update_data(name=message.text)
+ elif current_state == UpdateCategory.url:
+ await state.update_data(url=message.text)
+ elif current_state == UpdateCategory.media:
+ await state.update_data(media=message.photo)
+ elif current_state == UpdateCategory.description:
+ await state.update_data(description=message.text)
+ update_data = await state.get_data()
+ await category_product_crud.update(old_category_data, update_data, session)
+ await message.answer(
+ "Информация обновлена!",
+ reply_markup=await get_inline_keyboard(previous_menu="Назад"),
+ )
diff --git a/app/admin/handlers/admin_handlers/admin_info_handlers.py b/app/admin/handlers/admin_handlers/admin_info_handlers.py
new file mode 100644
index 0000000..964ae4e
--- /dev/null
+++ b/app/admin/handlers/admin_handlers/admin_info_handlers.py
@@ -0,0 +1,236 @@
+from aiogram import F, Router
+from aiogram.filters import or_f, and_f
+from aiogram.fsm.context import FSMContext
+
+from aiogram.types import CallbackQuery, Message
+
+from crud.info_crud import info_crud
+from filters.filters import ChatTypeFilter, IsAdmin
+from handlers.admin_handlers.admin import SectionState
+from keyboards.keyboards import (
+ get_inline_confirmation_keyboard,
+ get_inline_keyboard,
+)
+# from settings import SUPPORT_OPTIONS
+from sqlalchemy.ext.asyncio import AsyncSession
+from aiogram.fsm.state import State, StatesGroup
+
+
+SUPPORT_OPTIONS = {
+ "faq": "Общие вопросы",
+ "troubleshooting": "Проблемы с продуктами",
+ "callback_request": "Запрос на обратный звонок",
+}
+
+
+info_router = Router()
+info_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+
+
+class AddQuestion(StatesGroup):
+ question = State()
+ answer = State()
+ question_type = State()
+
+
+class UpdateQuestion(StatesGroup):
+ question = State()
+ answer = State()
+ question_type = State()
+ confirm = State()
+
+
+class DeleteQuestion(AddQuestion):
+ confirm = State()
+
+
+PREVIOUS_MENU = SUPPORT_OPTIONS.get("faq")
+
+
+async def set_question_type(state: str):
+ question_type = state.split(":")[-1]
+ return SUPPORT_OPTIONS.get(question_type)
+
+
+async def get_question_list(question_type: str, session: AsyncSession):
+ return [
+ question.question
+ for question in await info_crud.get_all_questions_by_type(
+ question_type, session
+ )
+ ]
+
+
+@info_router.callback_query(
+ or_f(SectionState.faq, SectionState.troubleshooting), F.data == "Добавить"
+)
+async def add_question(callback: CallbackQuery, state: FSMContext):
+ current_state = await state.get_state()
+ await state.set_state(AddQuestion.question_type)
+ await state.update_data(
+ question_type=await set_question_type(current_state)
+ )
+ await callback.message.answer("Введите текст нового вопрос")
+ await state.set_state(AddQuestion.question)
+
+
+@info_router.message(AddQuestion.question, F.text)
+async def add_question_text(message: Message, state: FSMContext):
+ await state.update_data(question=message.text)
+ await message.answer("Введите ответ на этот вопрос")
+ print(await state.get_data())
+ await state.set_state(AddQuestion.answer)
+
+
+@info_router.message(AddQuestion.answer, F.text)
+async def add_question_answer(
+ message: Message,
+ state: FSMContext,
+ session: AsyncSession,
+):
+ await state.update_data(answer=message.text)
+ data = await state.get_data()
+ await info_crud.create(data, session=session)
+ await message.answer(
+ "Вопрос добавлен!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=data.get("question_type")
+ ),
+ )
+ await state.clear()
+
+
+@info_router.callback_query(
+ or_f(SectionState.faq, SectionState.troubleshooting), F.data == "Удалить"
+)
+async def question_to_delete(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ current_state = await state.get_state()
+ await state.set_state(DeleteQuestion.question_type)
+ await state.update_data(
+ question_type=await set_question_type(current_state)
+ )
+ question_type = (await state.get_data()).get("question_type")
+ question_list = await get_question_list(question_type, session)
+ await callback.message.edit_text(
+ "Какой вопрос удалить?",
+ reply_markup=await get_inline_keyboard(
+ question_list, previous_menu=PREVIOUS_MENU
+ ),
+ )
+ await state.set_state(DeleteQuestion.question)
+
+
+@info_router.callback_query(DeleteQuestion.question, F.data)
+async def confirm_delete_question(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ question = await info_crud.get_by_question_text(callback.data, session)
+ await callback.message.edit_text(
+ f"Вы уверены, что хотите удалить этот вопрос?\n\n {question.question}",
+ reply_markup=await get_inline_confirmation_keyboard(
+ option=question.question, cancel_option=PREVIOUS_MENU
+ ),
+ )
+ await state.set_state(DeleteQuestion.confirm)
+
+
+@info_router.callback_query(DeleteQuestion.confirm, F.data != PREVIOUS_MENU)
+async def delete_question(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ await state.clear()
+ question = await info_crud.get_by_question_text(callback.data, session)
+ await info_crud.remove(question, session)
+ await callback.message.edit_text(
+ "Вопрос удален!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=question.question_type
+ ),
+ )
+
+
+@info_router.callback_query(
+ or_f(SectionState.faq, SectionState.troubleshooting),
+ F.data == "Изменить",
+)
+async def update_question(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ current_state = await state.get_state()
+ await state.set_state(UpdateQuestion.question_type)
+ await state.update_data(
+ question_type=await set_question_type(current_state)
+ )
+ question_type = (await state.get_data()).get("question_type")
+ question_list = await get_question_list(question_type, session)
+ await callback.message.edit_text(
+ "Какой вопрос отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ question_list, previous_menu=PREVIOUS_MENU
+ ),
+ )
+ await state.set_state(UpdateQuestion.question)
+
+
+@info_router.callback_query(
+ UpdateQuestion.question, and_f(F.data != "Вопрос", F.data != "Ответ")
+)
+async def update_question_choice(callback: CallbackQuery, state: FSMContext):
+ await state.update_data(question=callback.data)
+ await callback.message.answer(
+ "Что вы хотите отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ ["Вопрос", "Ответ"], previous_menu=PREVIOUS_MENU
+ ),
+ )
+
+
+@info_router.callback_query(UpdateQuestion.question, F.data == "Вопрос")
+async def update_question_text(callback: CallbackQuery, state: FSMContext):
+ question_text = (await state.get_data()).get("question")
+ await callback.message.answer(
+ f"Сейчас вопрос записан вот так:\n\n{question_text}\n\n Введите новый текст"
+ )
+ await state.set_state(UpdateQuestion.confirm)
+
+
+@info_router.callback_query(UpdateQuestion.question, F.data == "Ответ")
+async def update_question_answer(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ question_text = (await state.get_data()).get("question")
+ answer = await info_crud.get_by_question_text(question_text, session)
+ await callback.message.answer(
+ f"Сейчас ответ записан вот так:\n\n{answer.answer}\n\n Введите новый текст"
+ )
+ await state.set_state(UpdateQuestion.answer)
+
+
+@info_router.message(
+ or_f(UpdateQuestion.confirm, UpdateQuestion.answer), F.text
+)
+async def update_question_data(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ current_state = await state.get_state()
+ old_data = await state.get_data()
+ question = await info_crud.get_by_question_text(
+ old_data.get("question"), session
+ )
+
+ if current_state == UpdateQuestion.confirm:
+ await state.update_data(question=message.text)
+ elif current_state == UpdateQuestion.answer:
+ await state.update_data(answer=message.text)
+
+ updated_data = await state.get_data()
+ await info_crud.update(question, updated_data, session=session)
+ await message.answer(
+ "Вопрос обновлен!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=updated_data.get("question_type")
+ ),
+ )
+ await state.clear()
diff --git a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
new file mode 100644
index 0000000..5051a05
--- /dev/null
+++ b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
@@ -0,0 +1,265 @@
+from aiogram import F, Router
+from aiogram.filters import and_f, or_f
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from aiogram.types import CallbackQuery, Message
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from .admin import SectionState
+from crud.about_crud import company_info_crud
+from crud.portfolio_projects_crud import portfolio_crud
+from filters.filters import ChatTypeFilter, IsAdmin
+from keyboards.keyboards import (
+ get_inline_confirmation_keyboard,
+ get_inline_keyboard,
+)
+# from settings import (
+# MAIN_MENU_OPTIONS,
+# ADMIN_PORTFOLIO_OPTIONS,
+# PORTFOLIO_MENU_OPTIONS,
+# )
+
+MAIN_MENU_OPTIONS = {
+ "company_bio": "Информация о компании",
+ "products": "Продукты и услуги",
+ "support": "Техническая поддержка",
+ "portfolio": "Портфолио",
+ "request_callback": "Связаться с менеджером",
+}
+
+PORTFOLIO_MENU_OPTIONS = {
+ "portfolio_button": "Наше портфолио",
+ "other_projects": "Посмотреть другие проекты",
+}
+
+ADMIN_PORTFOLIO_OPTIONS = {
+ "change_url": "Адрес ссылки на портфолио",
+}
+
+portfolio_router = Router()
+portfolio_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+
+
+class UpdatePortfolio(StatesGroup):
+ url = State()
+
+
+class AddProject(StatesGroup):
+ project_name = State()
+ url = State()
+
+
+class UpdateProject(StatesGroup):
+ select = State()
+ project_name = State()
+ url = State()
+ confirm = State()
+
+
+class DeleteProject(AddProject):
+ confirm = State()
+
+
+PREVIOUS_MENU = PORTFOLIO_MENU_OPTIONS.get("other_projects")
+
+
+async def get_portfolio_project_list(session: AsyncSession):
+ """Получить список названий проектов для портфолио."""
+ projects = [
+ project.project_name
+ for project in await portfolio_crud.get_multi(session)
+ ]
+ return projects
+
+
+@portfolio_router.callback_query(
+ SectionState.other_projects, F.data == "Добавить"
+)
+async def add_portfolio_project_name(
+ callback: CallbackQuery, state: FSMContext
+):
+ await callback.message.answer("Введите название проекта:")
+ await state.set_state(AddProject.project_name)
+
+
+@portfolio_router.message(AddProject.project_name, F.text)
+async def add_portfolio_project_url(message: Message, state: FSMContext):
+ await state.update_data(project_name=message.text)
+ await message.answer("Добавьте ссылку:")
+ await state.set_state(AddProject.url)
+
+
+@portfolio_router.message(AddProject.url, F.text)
+async def create_portfolio_project(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ await state.update_data(url=message.text)
+ data = await state.get_data()
+ await portfolio_crud.create(data, session)
+ await message.answer(
+ "Проект добавлен!",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+ await state.clear()
+
+
+@portfolio_router.callback_query(
+ SectionState.other_projects, F.data == "Удалить"
+)
+async def portfolio_project_to_delete(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ await callback.message.answer(
+ "Какой проект Вы хотите удалить?",
+ reply_markup=await get_inline_keyboard(
+ options=await get_portfolio_project_list(session),
+ previous_menu=PREVIOUS_MENU,
+ ),
+ )
+ await state.set_state(DeleteProject.project_name)
+
+
+@portfolio_router.callback_query(DeleteProject.project_name, F.data)
+async def confirm_delete(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ portfolio_project = await portfolio_crud.get_by_project_name(
+ callback.data, session
+ )
+ await callback.message.edit_text(
+ f"Вы уверены, что хотите удалить этот проект?\n\n {portfolio_project.project_name}",
+ reply_markup=await get_inline_confirmation_keyboard(
+ option=portfolio_project.project_name, cancel_option=PREVIOUS_MENU
+ ),
+ )
+ await state.set_state(DeleteProject.confirm)
+
+
+@portfolio_router.callback_query(
+ DeleteProject.confirm, F.data != PREVIOUS_MENU
+)
+async def delete_protfolio_project(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ portfolio_project = await portfolio_crud.get_by_project_name(
+ callback.data, session
+ )
+ await portfolio_crud.remove(portfolio_project, session)
+ await callback.message.edit_text(
+ "Проект удален!",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+ await state.clear()
+
+
+@portfolio_router.callback_query(
+ SectionState.other_projects, F.data == "Изменить"
+)
+async def portfolio_project_to_update(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ await callback.message.edit_text(
+ "Какую информацию вы хотите отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ options=await get_portfolio_project_list(session),
+ previous_menu=PREVIOUS_MENU,
+ ),
+ )
+ await state.set_state(UpdateProject.select)
+
+
+@portfolio_router.callback_query(
+ UpdateProject.select,
+ and_f(F.data != "Название проекта", F.data != "Адрес ссылки", F.data != PREVIOUS_MENU),
+)
+async def update_portfolio_project_choise(
+ callback: CallbackQuery, state: FSMContext
+):
+ await state.update_data(select=callback.data)
+ await callback.message.edit_text(
+ "Что вы хотите отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ ["Название проекта", "Адрес ссылки"], previous_menu=PREVIOUS_MENU
+ ),
+ )
+
+
+@portfolio_router.callback_query(
+ UpdateProject.select, F.data == "Название проекта"
+)
+async def about_name_update(
+ callback: CallbackQuery, state: FSMContext,
+):
+ about_name = await state.get_data()
+ about_name_text = about_name.get("select")
+ await callback.message.answer(
+ f"Сейчас у проекта такое название:\n\n {about_name_text}\n\n Введите новое название"
+ )
+ await state.set_state(UpdateProject.project_name)
+
+
+@portfolio_router.callback_query(
+ UpdateProject.select, F.data == "Адрес ссылки"
+)
+async def about_url_update(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ about_name_data = await state.get_data()
+ about_name_text = about_name_data.get("select")
+ about_info = await portfolio_crud.get_by_project_name(
+ about_name_text, session
+ )
+ await callback.message.answer(
+ f"Сейчас у ссылки такой адрес:\n\n {about_info.url}\n\n Введите новое название"
+ )
+ await state.set_state(UpdateProject.url)
+
+
+@portfolio_router.message(
+ or_f(UpdateProject.project_name, UpdateProject.url), F.text
+)
+async def update_about_info(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ current_state = await state.get_state()
+ old_data = await state.get_data()
+ old_portfolio_data = await portfolio_crud.get_by_project_name(
+ old_data.get("select"), session
+ )
+ if current_state == UpdateProject.project_name:
+ await state.update_data(project_name=message.text)
+ elif current_state == UpdateProject.url:
+ await state.update_data(url=message.text)
+ update_data = await state.get_data()
+ await portfolio_crud.update(old_portfolio_data, update_data, session)
+ await message.answer(
+ "Информация обновлена!",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+ await state.clear()
+
+
+@portfolio_router.callback_query(
+ SectionState.portfolio,
+ F.data == ADMIN_PORTFOLIO_OPTIONS.get("change_url"),
+)
+async def change_portfolio_url(callback: CallbackQuery, state: FSMContext):
+ await callback.message.answer("Введите новый адрес ссылки на портфолио")
+ await state.set_state(UpdatePortfolio.url)
+
+
+@portfolio_router.message(UpdatePortfolio.url, F.text)
+async def update_portfolio_button(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ await state.update_data(url=message.text)
+ updated_data = await state.get_data()
+ portfolio = await company_info_crud.get_portfolio(session)
+ await company_info_crud.update(portfolio, updated_data, session)
+ await message.answer(
+ "Изменения внесены!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=MAIN_MENU_OPTIONS.get("portfolio")
+ ),
+ )
+ await state.clear()
diff --git a/app/admin/handlers/admin_handlers/admin_product_handlers.py b/app/admin/handlers/admin_handlers/admin_product_handlers.py
new file mode 100644
index 0000000..d7c6bf1
--- /dev/null
+++ b/app/admin/handlers/admin_handlers/admin_product_handlers.py
@@ -0,0 +1,356 @@
+from aiogram import F, Router
+from aiogram.filters import or_f, and_f
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from aiogram.types import CallbackQuery, Message
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from .admin import SectionState
+from crud.category_product import category_product_crud
+from crud.product_crud import product_crud
+from filters.filters import ChatTypeFilter, IsAdmin
+from keyboards.keyboards import (
+ get_inline_confirmation_keyboard,
+ get_inline_keyboard,
+)
+# from settings import (
+# MAIN_MENU_OPTIONS,
+# )
+
+MAIN_MENU_OPTIONS = {
+ "company_bio": "Информация о компании",
+ "products": "Продукты и услуги",
+ "support": "Техническая поддержка",
+ "portfolio": "Портфолио",
+ "request_callback": "Связаться с менеджером",
+}
+
+product_router = Router()
+product_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+
+PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("products")
+
+
+class AddProduct(StatesGroup):
+ title = State()
+ response = State()
+
+
+class UpdateProduct(StatesGroup):
+ select = State()
+ title = State()
+ response = State()
+ confirm = State()
+
+
+class DeleteProduct(AddProduct):
+ confirm = State()
+
+
+class AddProductInfo(StatesGroup):
+ name = State()
+ product_id = State()
+ url = State()
+ media = State()
+ description = State()
+ media_description = State()
+
+
+async def get_products_list(session: AsyncSession):
+ """Получить список названий проектов для портфолио."""
+
+ return [project.title for project in await product_crud.get_multi(session)]
+
+
+@product_router.callback_query(SectionState.product, F.data == "Добавить")
+async def add_product(callback: CallbackQuery, state: FSMContext):
+ """Добавить название."""
+
+ await callback.message.answer("Введите название проекта или услуги")
+ await state.set_state(AddProduct.title)
+
+
+@product_router.message(AddProduct.title, F.text)
+async def add_product_description(message: Message, state: FSMContext):
+ """Добавить описание."""
+
+ await state.update_data(title=message.text)
+ await message.answer("Добавьте описание к продукту или услуге")
+ await state.set_state(AddProduct.response)
+
+
+@product_router.message(AddProduct.response, F.text)
+async def creeate_product(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ """Создать продкет в БД."""
+
+ await state.update_data(response=message.text)
+ data = await state.get_data()
+
+ await product_crud.create(data, session)
+ await message.answer(
+ "Продукт создан! Хотите добавить к нему дополнительну информацию?",
+ reply_markup=await get_inline_confirmation_keyboard(
+ option="Да", cancel_option=PREVIOUS_MENU
+ ),
+ )
+
+ await state.set_state(AddProductInfo.product_id)
+
+
+@product_router.callback_query(AddProductInfo.product_id, F.data == "Да")
+async def add_product_categoty(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Добавить основные варианты для продукта."""
+
+ await state.clear()
+
+ last_product = await product_crud.get_last_added_product(session)
+ await state.update_data(product_id=last_product.id)
+
+ await callback.message.answer(
+ "Введите название для дополнительной информации"
+ )
+
+ await state.set_state(AddProductInfo.name)
+
+
+@product_router.message(AddProductInfo.name, F.text)
+async def add_product_category_name(message: Message, state: FSMContext):
+ """Выбрать тип данных для основных вариантов."""
+
+ await state.update_data(name=message.text)
+
+ await message.answer(
+ "Выберете способ передачи информации:",
+ reply_markup=await get_inline_keyboard(
+ ["Ссылка", "Текст", "Картинка"], previous_menu=PREVIOUS_MENU
+ ),
+ )
+
+
+@product_router.callback_query(
+ or_f(AddProductInfo.name, AddProductInfo.description),
+ or_f(F.data == "Ссылка", F.data == "Текст", F.data == "Картинка"),
+)
+async def add_product_category_data(
+ callback: CallbackQuery, state: FSMContext
+):
+ """Добавить информацию в основной вариант."""
+
+ if callback.data == "Ссылка":
+ await state.set_state(AddProductInfo.url)
+ info_type = "ссылку"
+ elif callback.data == "Текст":
+ await state.set_state(AddProductInfo.description)
+ info_type = "текст"
+ elif callback.data == "Картинка":
+ await state.set_state(AddProductInfo.media)
+ info_type = "Картинку"
+ await callback.message.answer(f"Добавьте {info_type}")
+
+
+@product_router.message(AddProductInfo.media, F.photo)
+async def add_media_description(message: Message, state: FSMContext):
+ """Добавить описание к картинке."""
+
+ await state.update_data(media=message.photo[-1].file_id)
+
+ await message.answer(
+ "Добавить описание к картинке?",
+ reply_markup=await get_inline_confirmation_keyboard(
+ "Текст", cancel_option="Нет"
+ ),
+ )
+
+ print(await state.get_data())
+ await state.set_state(AddProductInfo.description)
+
+
+@product_router.message(
+ or_f(AddProductInfo.description, AddProductInfo.url),
+ or_f(F.text, F.photo, F.data == "Нет"),
+)
+async def create_product_with_data(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ """Создать вариант для продукта в БД и предложить добавить следующий."""
+
+ current_state = await state.get_state()
+ if current_state == AddProductInfo.description:
+ await state.update_data(description=message.text)
+ elif current_state == AddProductInfo.url:
+ await state.update_data(url=message.text)
+ data = await state.get_data()
+
+ await category_product_crud.create(data, session)
+ await message.answer(
+ "Информация добавлена! Добавить еще?",
+ reply_markup=await get_inline_confirmation_keyboard(
+ option="Да", cancel_option=PREVIOUS_MENU
+ ),
+ )
+
+ await state.set_state(AddProductInfo.product_id)
+
+
+@product_router.callback_query(SectionState.product, F.data == "Удалить")
+async def product_to_delete(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Выбор продукта на удаление."""
+
+ await callback.message.edit_text(
+ "Какой проект вы хотите удалить?",
+ reply_markup=await get_inline_keyboard(
+ options=await get_products_list(session),
+ previous_menu=PREVIOUS_MENU,
+ ),
+ )
+
+ await state.set_state(DeleteProduct.title)
+
+
+@product_router.callback_query(DeleteProduct.title, F.data)
+async def confirm_delete(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Подтверждение удаления."""
+
+ portfolio_project = await product_crud.get_by_product_name(
+ callback.data, session
+ )
+ await callback.message.edit_text(
+ f"Вы уверены, что хотите удалить "
+ f"этот проект?\n\n {portfolio_project.title}",
+ reply_markup=await get_inline_confirmation_keyboard(
+ option=portfolio_project.title, cancel_option=PREVIOUS_MENU
+ ),
+ )
+
+ await state.set_state(DeleteProduct.confirm)
+
+
+@product_router.callback_query(DeleteProduct.confirm, F.data != PREVIOUS_MENU)
+async def delete_product(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Удалить продукт из БД."""
+
+ portfolio_project = await product_crud.get_by_product_name(
+ callback.data, session
+ )
+
+ await product_crud.remove(portfolio_project, session)
+ await callback.message.edit_text(
+ "Услуга удалена!",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+
+ await state.clear()
+
+
+@product_router.callback_query(SectionState.product, F.data == "Изменить")
+async def product_to_update(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Выбор продукта для редактирования."""
+
+ await callback.message.edit_text(
+ "Какую услугу вы хотите отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ options=await get_products_list(session),
+ previous_menu=PREVIOUS_MENU,
+ ),
+ ),
+
+ await state.set_state(UpdateProduct.select)
+
+
+@product_router.callback_query(
+ UpdateProduct.select,
+ and_f(
+ F.data != "Название проекта",
+ F.data != "Описание",
+ F.data != PREVIOUS_MENU,
+ ),
+)
+async def update_portfolio_project_choise(
+ callback: CallbackQuery, state: FSMContext
+):
+ """Выбор поля для редактирования."""
+
+ await state.update_data(select=callback.data)
+ await callback.message.edit_text(
+ "Что вы хотите отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ ["Название проекта", "Описание"], previous_menu=PREVIOUS_MENU
+ ),
+ )
+
+
+@product_router.callback_query(
+ UpdateProduct.select, F.data == "Название проекта"
+)
+async def about_name_update(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Ввести новое название продукта."""
+
+ product_data = await state.get_data()
+ product_name = product_data.get("select")
+
+ await callback.message.answer(
+ f"Текущее название:\n\n {product_name}\n\n Введите новое название"
+ )
+
+ await state.set_state(UpdateProduct.title)
+
+
+@product_router.callback_query(UpdateProduct.select, F.data == "Описание")
+async def about_url_update(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Ввести новое описание продукта."""
+
+ product_data = await state.get_data()
+ product_title = product_data.get("select")
+ product = await product_crud.get_by_product_name(product_title, session)
+
+ await callback.message.answer(
+ f"Текущее описание:\n\n {product.response}\n\n Введите новое описание"
+ )
+
+ await state.set_state(UpdateProduct.response)
+
+
+@product_router.message(
+ or_f(UpdateProduct.title, UpdateProduct.response), F.text
+)
+async def update_about_info(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ """Внести изменения продукта в БД."""
+
+ current_state = await state.get_state()
+ old_data = await state.get_data()
+ old_product_data = await product_crud.get_by_product_name(
+ old_data.get("select"), session
+ )
+
+ if current_state == UpdateProduct.title:
+ await state.update_data(title=message.text)
+ elif current_state == UpdateProduct.response:
+ await state.update_data(response=message.text)
+
+ update_data = await state.get_data()
+ await product_crud.update(old_product_data, update_data, session)
+
+ await message.answer(
+ "Информация обновлена!",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+
+ await state.clear()
diff --git a/app/admin/handlers/user.py b/app/admin/handlers/user.py
new file mode 100644
index 0000000..fa5e482
--- /dev/null
+++ b/app/admin/handlers/user.py
@@ -0,0 +1,321 @@
+from aiogram import F, Router
+from aiogram.fsm.context import FSMContext
+from aiogram.types import CallbackQuery, Message
+from aiogram.filters import CommandStart, or_f
+from aiogram.fsm.state import State, StatesGroup
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from crud.category_product import category_product_crud
+from admin.filters.filters import ChatTypeFilter
+from const import (
+ ADMIN_BASE_BUTTONS,
+ PORTFOLIO_DEFAULT_DATA,
+ PORTFOLIO_MENU_TEXT,
+ PORTFOLIO_OTHER_PROJECTS_TEXT,
+ PRODUCT_LIST_TEXT,
+ admin_list,
+ BASE_BUTTONS,
+ BASE_KEYBOARD_BUTTONS,
+ MAIN_MENU_OPTIONS,
+ GREETINGS,
+ MAIN_MENU_BUTTONS,
+ COMPANY_ABOUT,
+ PORTFOLIO_BUTTONS,
+ PORTFOLIO_MENU_OPTIONS,
+ SUPPORT_OPTIONS,
+ SUPPORT_MENU_TEXT,
+ SUPPROT_MENU_BUTTONS,
+)
+from admin.keyboards.keyboards import (
+ get_inline_keyboard,
+ get_reply_keyboard,
+ get_delete_message_keyboard,
+)
+
+from crud.info_crud import info_crud
+from crud.about_crud import company_info_crud
+from crud.portfolio_projects_crud import portfolio_crud
+from crud.product_crud import product_crud
+
+
+user_router = Router()
+user_router.message.filter(ChatTypeFilter(["private"]))
+
+
+class QuestionAnswer(StatesGroup):
+ question = State()
+
+
+class ProductCategory(StatesGroup):
+ product_id = State()
+ category = State()
+
+
+@user_router.callback_query(F.data == "delete")
+async def delete_message(callback: CallbackQuery):
+ await callback.message.delete()
+
+
+@user_router.message(CommandStart())
+async def start_cmd(message: Message):
+ """Получить основную экранную клавиатуру."""
+
+ if message.from_user.id in admin_list:
+ await message.answer(
+ GREETINGS,
+ reply_markup=await get_reply_keyboard(
+ ADMIN_BASE_BUTTONS, size=(1, 2)
+ ),
+ )
+ else:
+ await message.answer(
+ GREETINGS,
+ reply_markup=await get_reply_keyboard(
+ BASE_KEYBOARD_BUTTONS, size=(1, 2)
+ ),
+ )
+
+
+@user_router.message(F.text == BASE_BUTTONS.get("main_menu"))
+async def main_menu(message: Message, state: FSMContext):
+ """Получить основное меню бота после команды с экранной клавиатуры."""
+
+ await message.answer(
+ BASE_BUTTONS.get("main_menu"),
+ reply_markup=await get_inline_keyboard(MAIN_MENU_BUTTONS),
+ )
+
+ await message.delete()
+ await state.clear()
+
+
+@user_router.callback_query(F.data == BASE_BUTTONS.get("main_menu"))
+async def main_menu_callback(callback: CallbackQuery, state: FSMContext):
+ """Получить основное меню бота через callback_query."""
+
+ await callback.message.edit_text(
+ BASE_BUTTONS.get("main_menu"),
+ reply_markup=await get_inline_keyboard(MAIN_MENU_BUTTONS),
+ )
+ await state.clear()
+
+
+@user_router.callback_query(F.data == MAIN_MENU_OPTIONS.get("portfolio"))
+async def portfolio_info(callback: CallbackQuery, session: AsyncSession):
+ portlio_url = await company_info_crud.get_portfolio(session)
+ await callback.message.edit_text(
+ PORTFOLIO_MENU_TEXT,
+ reply_markup=await get_inline_keyboard(
+ PORTFOLIO_BUTTONS,
+ urls=[
+ portlio_url.url,
+ ],
+ previous_menu=BASE_BUTTONS.get("main_menu"),
+ is_admin=callback.from_user.id in admin_list,
+ admin_update_menu=callback.data,
+ ),
+ )
+
+
+@user_router.callback_query(F.data == MAIN_MENU_OPTIONS.get("company_bio"))
+async def main_info(
+ callback: CallbackQuery, session: AsyncSession, state: FSMContext
+):
+ """Получить список ссылок на информацию о компании."""
+
+ await state.clear()
+
+ about_company_data = await company_info_crud.get_multi(session)
+ about_company_buttons = [
+ data.name
+ for data in about_company_data
+ if data.name != PORTFOLIO_DEFAULT_DATA.get("name")
+ ]
+
+ company_about_urls = [data.url for data in about_company_data]
+
+ await callback.message.edit_text(
+ COMPANY_ABOUT,
+ reply_markup=await get_inline_keyboard(
+ options=about_company_buttons,
+ previous_menu=BASE_BUTTONS.get("main_menu"),
+ urls=company_about_urls,
+ is_admin=callback.from_user.id in admin_list,
+ admin_update_menu=callback.data,
+ ),
+ )
+
+
+@user_router.callback_query(F.data == MAIN_MENU_OPTIONS.get("support"))
+async def support_menu(callback: CallbackQuery):
+ """Получить меню выбора раздела техподдержки."""
+
+ await callback.message.edit_text(
+ SUPPORT_MENU_TEXT,
+ reply_markup=await get_inline_keyboard(
+ options=SUPPROT_MENU_BUTTONS,
+ previous_menu=BASE_BUTTONS.get("main_menu"),
+ ),
+ )
+
+
+@user_router.callback_query(
+ or_f(
+ F.data == SUPPORT_OPTIONS.get("faq"),
+ F.data == SUPPORT_OPTIONS.get("troubleshooting"),
+ )
+)
+async def info_faq(
+ callback: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
+):
+ """Получить список вопросов раздела техподдержки."""
+
+ await state.clear()
+
+ question_type = callback.data
+ question_list = [
+ question.question
+ for question in await info_crud.get_all_questions_by_type(
+ question_type=question_type, session=session
+ )
+ ]
+
+ await callback.message.edit_text(
+ callback.data,
+ reply_markup=await get_inline_keyboard(
+ options=question_list,
+ previous_menu=MAIN_MENU_OPTIONS.get("support"),
+ is_admin=callback.from_user.id in admin_list,
+ admin_update_menu=callback.data,
+ ),
+ )
+
+ await state.set_state(QuestionAnswer.question)
+
+
+@user_router.callback_query(
+ F.data == PORTFOLIO_MENU_OPTIONS.get("other_projects")
+)
+async def portfolio_other_projects(
+ callback: CallbackQuery, session: AsyncSession, state: FSMContext
+):
+ """Получить список других проектов компании."""
+
+ await state.clear()
+
+ projects = await portfolio_crud.get_multi(session)
+ projects_names = [project.project_name for project in projects]
+ urls = [project.url for project in projects]
+
+ await callback.message.edit_text(
+ PORTFOLIO_OTHER_PROJECTS_TEXT,
+ reply_markup=await get_inline_keyboard(
+ projects_names,
+ previous_menu=MAIN_MENU_OPTIONS.get("portfolio"),
+ urls=urls,
+ is_admin=callback.from_user.id in admin_list,
+ admin_update_menu=callback.data,
+ ),
+ )
+
+
+@user_router.callback_query(F.data == MAIN_MENU_OPTIONS.get("products"))
+async def get_products_list(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Получить список продуктов."""
+
+ products = [
+ product.title for product in await product_crud.get_multi(session)
+ ]
+
+ await state.clear()
+
+ await callback.message.edit_text(
+ PRODUCT_LIST_TEXT,
+ reply_markup=await get_inline_keyboard(
+ products,
+ previous_menu=BASE_BUTTONS.get("main_menu"),
+ is_admin=callback.from_user.id in admin_list,
+ admin_update_menu=callback.data,
+ ),
+ )
+
+ await state.set_state(ProductCategory.category)
+
+
+@user_router.callback_query(ProductCategory.category, F.data)
+async def product_category(
+ callback: CallbackQuery, session: AsyncSession, state: FSMContext
+):
+ """Получить список дополнительных вариантов продукта."""
+
+ product = await product_crud.get_by_product_name(callback.data, session)
+ categories = await category_product_crud.get_category_by_product_id(
+ product.id, session
+ )
+ categories_by_name = [category.name for category in categories]
+ urls = [category.url for category in categories]
+
+ await callback.message.edit_text(
+ f"{product.response}",
+ reply_markup=await get_inline_keyboard(
+ categories_by_name,
+ urls=urls,
+ previous_menu=MAIN_MENU_OPTIONS.get("products"),
+ is_admin=callback.from_user.id in admin_list,
+ admin_update_menu=callback.data,
+ ),
+ )
+
+ await state.set_state(ProductCategory.product_id)
+ await state.update_data(product_id=product.id)
+
+
+@user_router.callback_query(ProductCategory.product_id, F.data)
+async def get_product_info(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Получить данные варианта продукта."""
+
+ state_data = await state.get_data()
+ product_id = state_data.get("product_id")
+ category = await category_product_crud.get_active_field(
+ product_id=product_id, category_name=callback.data, session=session
+ )
+
+ if category.media:
+ await callback.message.answer_photo(
+ photo=category.media,
+ caption=category.description,
+ reply_markup=await get_delete_message_keyboard(),
+ )
+ else:
+ await callback.message.answer(
+ category.description,
+ reply_markup=await get_delete_message_keyboard(),
+ )
+
+
+@user_router.callback_query(QuestionAnswer.question, F.data)
+async def faq_answer(
+ callback: CallbackQuery, session: AsyncSession, state: FSMContext
+):
+ """Получить ответ на вопрос из раздела Техподдержка."""
+
+ question_list = [
+ question.question for question in await info_crud.get_multi(session)
+ ]
+
+ if callback.data not in question_list:
+ return
+
+ question = await info_crud.get_by_question_text(callback.data, session)
+ answer = f"{callback.data}\n\n{question.answer}"
+
+ await callback.message.answer(
+ answer,
+ reply_markup=await get_delete_message_keyboard(),
+ )
diff --git a/app/admin/handlers/validators.py b/app/admin/handlers/validators.py
new file mode 100644
index 0000000..96381b6
--- /dev/null
+++ b/app/admin/handlers/validators.py
@@ -0,0 +1,9 @@
+import re
+
+from const import PHONE_NUMBER_REGEX
+
+
+def phone_number_validator(phone_number: int) -> bool:
+ """Вовзращает True если номер телефона соответствует шаблону."""
+ phone_number = re.sub(r"[-()./ ]", "", phone_number)
+ return re.match(PHONE_NUMBER_REGEX, phone_number) is not None
diff --git a/app/admin/keyboards/__init__.py b/app/admin/keyboards/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/admin/keyboards/keyboards.py b/app/admin/keyboards/keyboards.py
new file mode 100644
index 0000000..7d389b0
--- /dev/null
+++ b/app/admin/keyboards/keyboards.py
@@ -0,0 +1,222 @@
+from aiogram.types import (
+ KeyboardButton,
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+)
+from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
+
+
+def get_paginated_keyboard_size(items_per_page: int):
+ """Вернуть кортеж вида (1, 1, ... 1, 2, 1)"""
+
+ return (1,) * items_per_page + (2, 1)
+
+
+async def get_inline_keyboard(
+ options: list[str] | str | None = None,
+ callback: list[str] | str | None = None,
+ previous_menu: str | None = None,
+ urls: list[str] | None = None,
+ size: tuple[int] = (1,),
+ is_admin: bool | None = False,
+ admin_update_menu: str | None = None,
+) -> InlineKeyboardMarkup:
+ """Создать набор кнопок для меню раздела."""
+
+ keyboard = InlineKeyboardBuilder()
+
+ if not callback:
+ callback = options
+
+ if options:
+ for index, option in enumerate(options):
+ keyboard.add(
+ InlineKeyboardButton(
+ text=option,
+ callback_data=str(callback[index]),
+ url=(
+ urls[index]
+ if urls and index in range(len(urls))
+ else None
+ ),
+ )
+ )
+
+ if previous_menu:
+ keyboard.add(
+ InlineKeyboardButton(
+ text="Назад",
+ callback_data=previous_menu,
+ )
+ )
+
+ if is_admin:
+ keyboard.add(
+ InlineKeyboardButton(
+ text="Редактировать🔧",
+ callback_data=f"{admin_update_menu}_",
+ )
+ )
+
+ return keyboard.adjust(*size).as_markup(resize_keyboard=True)
+
+
+# async def get_inline_paginated_keyboard(
+# options: Optional[Union[list[str], str]] = None,
+# pagination: dict = None,
+# size: tuple[int] = (1,),
+# ) -> InlineKeyboardMarkup:
+# """Создать набор кнопок для меню раздела с поддержкой пагинации."""
+# keyboard = InlineKeyboardBuilder()
+# total_pages = 0
+# current_page = 0
+# if pagination:
+# current_page = pagination.get("current_page", 1)
+# items_per_page = pagination.get("items_per_page", 5)
+# total_items = len(options) if options else 0
+# total_pages = (total_items + items_per_page - 1) // items_per_page
+
+# start_index = (current_page - 1) * items_per_page
+# end_index = min(start_index + items_per_page, total_items)
+
+# current_options = options[start_index:end_index] if options else []
+# else:
+# current_options = options
+
+# for option in current_options:
+# keyboard.add(
+# InlineKeyboardButton(
+# text=option,
+# callback_data=option,
+# ),
+# )
+# if total_pages > 1:
+# if current_page > 1:
+# keyboard.add(
+# InlineKeyboardButton(
+# text="◀️ Предыдущая",
+# callback_data=f"{current_page - 1}",
+# )
+# )
+# if current_page < total_pages:
+# keyboard.add(
+# InlineKeyboardButton(
+# text="Следующая ▶️",
+# callback_data=f"{current_page + 1}",
+# )
+# )
+# keyboard.add(
+# InlineKeyboardButton(
+# text="Главное меню",
+# callback_data=BASE_BUTTONS.get("main_menu"),
+# )
+# )
+
+# return keyboard.adjust(*size).as_markup(resize_keyboard=True)
+
+
+async def get_inline_paginated_keyboard(
+ options: list[str] | str | None = None,
+ callback: list[str] | str | None = None,
+ previous_menu: str | None = None,
+ previous_menu_text: str | None = "Назад",
+ items_per_page: int = 5,
+ size: tuple[int] = (1,),
+ current_page: int = 1,
+) -> InlineKeyboardMarkup:
+ """Создать набор кнопок для меню раздела с поддержкой пагинации."""
+
+ if not callback:
+ callback = options
+
+ keyboard = InlineKeyboardBuilder()
+
+ total_pages = 0
+ total_items = len(options) if options else 0
+ total_pages = (total_items + items_per_page - 1) // items_per_page
+ start_index = (current_page - 1) * items_per_page
+ end_index = min(start_index + items_per_page, total_items)
+ current_options = options[start_index:end_index] if options else []
+
+ for index, option in enumerate(current_options):
+ keyboard.add(
+ InlineKeyboardButton(
+ text=option,
+ callback_data=str(callback[index]),
+ ),
+ )
+
+ navigation_row = []
+ if total_pages > 1:
+ if current_page > 1:
+ navigation_row.append(
+ InlineKeyboardButton(
+ text="◀️ Предыдущая",
+ callback_data=f"page:{current_page - 1}",
+ )
+ )
+
+ if current_page < total_pages:
+ navigation_row.append(
+ InlineKeyboardButton(
+ text="Следующая ▶️",
+ callback_data=f"page:{current_page + 1}",
+ )
+ )
+
+ if navigation_row:
+ keyboard.add(*navigation_row)
+ keyboard.add(
+ InlineKeyboardButton(
+ text=previous_menu_text,
+ callback_data=previous_menu,
+ )
+ )
+
+ return keyboard.adjust(*size).as_markup(resize_keyboard=True)
+
+
+async def get_reply_keyboard(
+ options: list[str] | str | None = None,
+ size: tuple[int] = (1,),
+) -> ReplyKeyboardMarkup:
+ """Создать экранную клавиатуру."""
+
+ keyboard = ReplyKeyboardBuilder()
+
+ if options:
+ if isinstance(options, list):
+ for option in options:
+ keyboard.add(KeyboardButton(text=option, callback_data=option))
+ else:
+ keyboard.add(
+ KeyboardButton(
+ text=options,
+ # callback_data=options,
+ )
+ )
+
+ return keyboard.adjust(*size).as_markup()
+
+
+async def get_delete_message_keyboard() -> InlineKeyboardMarkup:
+ """Создать копку для удаления сообщения."""
+
+ keyboard = InlineKeyboardBuilder()
+ keyboard.add(
+ InlineKeyboardButton(text="Понятно! :)", callback_data="delete")
+ )
+ return keyboard.adjust(1).as_markup(resize_keyboard=True)
+
+
+async def get_inline_confirmation_keyboard(
+ option: str, cancel_option: str
+) -> InlineKeyboardMarkup:
+ """Кнопка для подтверждения действий."""
+
+ keyboard = InlineKeyboardBuilder()
+ keyboard.add(InlineKeyboardButton(text="Да", callback_data=option))
+ keyboard.add(InlineKeyboardButton(text="Нет", callback_data=cancel_option))
+
+ return keyboard.adjust(2).as_markup(resize_keyboard=True)
diff --git a/app/bot/callbacks.py b/app/bot/callbacks.py
index d0cea79..bca13aa 100644
--- a/app/bot/callbacks.py
+++ b/app/bot/callbacks.py
@@ -3,6 +3,7 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
+from sqlalchemy.ext.asyncio import AsyncSession
from bot.keyborads import (
list_of_projects_keyboard, main_keyboard,
@@ -52,7 +53,9 @@ async def previous_choice(callback: CallbackQuery) -> None:
@router.callback_query(F.data.in_(('get_faq', 'get_problems_with_products')))
-async def get_questions(callback: CallbackQuery) -> None:
+async def get_questions(
+ callback: CallbackQuery, session: AsyncSession
+) -> None:
"""Инлайн вывод общих вопросов и проблем с продуктами."""
await callback.answer()
@@ -66,18 +69,22 @@ async def get_questions(callback: CallbackQuery) -> None:
await callback.message.edit_text(
"Выберите вопрос:",
reply_markup=await faq_or_problems_with_products_inline_keyboard(
- question_type
+ question_type, session
)
)
@router.callback_query(F.data.startswith('answer:'))
-async def get_faq_answer(callback: CallbackQuery) -> None:
+async def get_faq_answer(
+ callback: CallbackQuery, session: AsyncSession
+) -> None:
"""Вывод ответа на выбранный вопрос."""
await callback.answer()
- question = await get_question_by_id(callback.data.split(':')[1])
+ question = await get_question_by_id(
+ callback.data.split(':')[1], session
+ )
if question:
await callback.message.edit_text(
@@ -92,7 +99,9 @@ async def get_faq_answer(callback: CallbackQuery) -> None:
@router.callback_query(F.data == 'back_to_previous_menu')
-async def back_to_products(callback: CallbackQuery) -> None:
+async def back_to_products(
+ callback: CallbackQuery, session: AsyncSession
+) -> None:
"""Возвращает к выбору продуктов."""
await callback.answer()
@@ -101,12 +110,14 @@ async def back_to_products(callback: CallbackQuery) -> None:
await callback.message.edit_text(
text='Вы вернулись к списку продуктов и услуг:',
- reply_markup=await inline_products_and_services()
+ reply_markup=await inline_products_and_services(session)
)
@router.callback_query(F.data.startswith('category_'))
-async def get_response_by_title(callback: CallbackQuery) -> None:
+async def get_response_by_title(
+ callback: CallbackQuery, session: AsyncSession
+) -> None:
"""Возвращает заготовленный ответ на выбранную категорию."""
await callback.answer()
@@ -116,9 +127,9 @@ async def get_response_by_title(callback: CallbackQuery) -> None:
category_id = int(callback.data.split('_')[1])
await callback.message.edit_text(
- text=await response_text_by_id(category_id),
+ text=await response_text_by_id(category_id, session),
reply_markup=await category_type_inline_keyboard(
- await get_title_by_id(category_id)
+ await get_title_by_id(category_id, session), session
)
)
@@ -189,14 +200,16 @@ async def get_support(callback: CallbackQuery) -> None:
@router.callback_query(F.data == 'products_services')
-async def products_services(callback: CallbackQuery) -> None:
+async def products_services(
+ callback: CallbackQuery, session: AsyncSession
+) -> None:
"""Информация о продуктах и услугах."""
try:
await callback.message.edit_text(
'Мы предлагаем следующие продукты и услуги. '
'Какой из них вас интересует?',
- reply_markup=await inline_products_and_services()
+ reply_markup=await inline_products_and_services(session)
)
await callback.answer()
logger.info(
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index 3d13ea6..8813a89 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -5,7 +5,7 @@
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
-
+from sqlalchemy.ext.asyncio import AsyncSession
from bot.keyborads import back_to_main_menu
from crud.request_to_manager import create_request_to_manager
from bot.validators import (
@@ -112,7 +112,9 @@ async def process_first_name(message: Message, state: FSMContext) -> None:
@router.message(Form.phone_number)
-async def process_phone_number(message: Message, state: FSMContext) -> None:
+async def process_phone_number(
+ message: Message, state: FSMContext, session: AsyncSession
+) -> None:
"""Состояние: ввод номера телефона."""
try:
@@ -135,7 +137,9 @@ async def process_phone_number(message: Message, state: FSMContext) -> None:
user_data = await state.get_data()
request_type = user_data.pop('request_type')
- new_request = await create_request_to_manager(user_data, request_type)
+ new_request = await create_request_to_manager(
+ user_data, request_type, session
+ )
logger.info(f"Запись создана в БД с ID: {new_request.id}")
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index 2857d96..e3457b1 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -2,6 +2,9 @@
from aiogram import Router
from aiogram.filters import CommandStart, Command
from aiogram.types import Message
+from sqlalchemy.ext.asyncio import AsyncSession
+from admin.keyboards.keyboards import get_inline_keyboard
+from const import MAIN_MENU_BUTTONS
from models.models import RoleEnum
from crud.users import (
@@ -20,24 +23,26 @@
# сейчас в каждой фукнции дергаем
@router.message(Command('admin'))
-async def cmd_admin(message: Message) -> None:
+async def cmd_admin(message: Message, session: AsyncSession) -> None:
"""Вход в админку."""
- role = await get_role_by_tg_id(message.from_user.id)
+ role = await get_role_by_tg_id(message.from_user.id, session)
- response = 'Добро пожаловать в админку' if role in (
- RoleEnum.ADMIN, RoleEnum.MANAGER
- ) else '403: Forbidden'
-
- await message.answer(response)
+ if role in (RoleEnum.ADMIN, RoleEnum.MANAGER):
+ await message.answer(
+ 'Добро пожаловать в админку!',
+ reply_markup=await get_inline_keyboard(MAIN_MENU_BUTTONS)
+ )
+ else:
+ await message.answer('403: Forbidden')
@router.message(CommandStart())
-async def cmd_start(message: Message) -> None:
+async def cmd_start(message: Message, session: AsyncSession) -> None:
"""Выводит приветствие пользователя."""
- if not await is_user_in_db(message.from_user.id):
- await create_user_id(message.from_user.id)
+ if not await is_user_in_db(message.from_user.id, session):
+ await create_user_id(message.from_user.id, session)
try:
await message.answer(
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index f5b494c..b33b09b 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -5,7 +5,7 @@
from crud.questions import get_question_by_title
from crud.projects import get_all_prtfolio_projects, get_categories_by_name
from models.models import CheckCompanyPortfolio, ProductCategory
-
+from sqlalchemy.ext.asyncio import AsyncSession
back_to_main_menu = InlineKeyboardButton(
text='Вернуться к основным вариантам.',
@@ -71,12 +71,12 @@
)
-async def inline_products_and_services():
+async def inline_products_and_services(session: AsyncSession):
"""Инлайн клавиатура для продуктов и услуг."""
keyboard = InlineKeyboardBuilder()
- objects_in_db = await get_all_prtfolio_projects(ProductCategory)
+ objects_in_db = await get_all_prtfolio_projects(ProductCategory, session)
for obj in objects_in_db:
keyboard.add(InlineKeyboardButton(
@@ -102,10 +102,10 @@ async def inline_products_and_services():
)
-async def list_of_projects_keyboard():
+async def list_of_projects_keyboard(session: AsyncSession):
"""Инлайн вывод проектов с данными из БД."""
- projects = await get_all_prtfolio_projects(CheckCompanyPortfolio)
+ projects = await get_all_prtfolio_projects(CheckCompanyPortfolio, session)
keyboard = InlineKeyboardBuilder()
@@ -148,11 +148,12 @@ async def list_of_projects_keyboard():
async def faq_or_problems_with_products_inline_keyboard(
- question_type: str
+ question_type: str,
+ session: AsyncSession
) -> InlineKeyboardMarkup:
"""Инлайн-клавиатуры для f.a.q вопросов или проблем с продуктами."""
- questions = await get_question_by_title(question_type)
+ questions = await get_question_by_title(question_type, session)
keyboard = InlineKeyboardBuilder()
for question in questions:
@@ -169,11 +170,12 @@ async def faq_or_problems_with_products_inline_keyboard(
async def category_type_inline_keyboard(
- product_name: str
+ product_name: str,
+ session: AsyncSession
) -> InlineKeyboardMarkup:
"""Инлайн клавиатура для типов в категориях."""
- category_types = await get_categories_by_name(product_name)
+ category_types = await get_categories_by_name(product_name, session)
keyboard = InlineKeyboardBuilder()
diff --git a/app/const.py b/app/const.py
new file mode 100644
index 0000000..88b3bb2
--- /dev/null
+++ b/app/const.py
@@ -0,0 +1,94 @@
+import os
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+TELEGRAM_CHAT_IDS = os.getenv("TELEGRAM_CHAT_IDS").split(", ")
+admin_list = [int(admin_id) for admin_id in TELEGRAM_CHAT_IDS]
+
+
+def get_buttons(menu: dict[str, str]) -> list[str]:
+ """Возвращает список кнопок для бота."""
+ return [button for button in menu.values()]
+
+
+# Первое сообщение бота после команды /start
+GREETINGS = (
+ "Здравствуйте!\n"
+ "Я ваш виртуальный помощник.\n"
+ "Воспользуйтесь экранной клавиатурой для выбора опции."
+)
+
+# Кнопки админа
+ADMIN_BASE_OPTIONS = {
+ "create": "Добавить",
+ "update": "Изменить",
+ "delete": "Удалить",
+}
+ADMIN_PORTFOLIO_OPTIONS = {
+ "change_url": "Адрес ссылки на портфолио",
+}
+ADMIN_BASE_KEYBOARD = get_buttons(ADMIN_BASE_OPTIONS)
+ADMIN_PORTFOLIO_KEYBOARD = get_buttons(ADMIN_PORTFOLIO_OPTIONS)
+USER_CALLBACK_PAGINATION = 5
+FEEDBACK_PAGINATION = 5
+ADMIN_BASE_REPLY_OPTIONS = {
+ "main_menu": "Главное меню",
+ "callback_case": "Заявки на обратный звонок",
+ "feedback": "Посмотреть отзывы",
+}
+ADMIN_BASE_BUTTONS = get_buttons(ADMIN_BASE_REPLY_OPTIONS)
+# Кнопки экранной клавиатуры
+BASE_BUTTONS = {
+ "main_menu": "Главное меню",
+ "call_manager": "Связаться с менеджером",
+ "rate_us": "Оставить отзыв",
+}
+BASE_KEYBOARD_BUTTONS = get_buttons(BASE_BUTTONS)
+
+# Главное меню - кнопки и текст
+MAIN_MENU_TEXT = "Главное меню"
+MAIN_MENU_OPTIONS = {
+ "company_bio": "Информация о компании",
+ "products": "Продукты и услуги",
+ "support": "Техническая поддержка",
+ "portfolio": "Портфолио",
+ "request_callback": "Связаться с менеджером",
+}
+MAIN_MENU_BUTTONS = get_buttons(MAIN_MENU_OPTIONS)
+
+# Техподдержка - кнопки и текст
+SUPPORT_MENU_TEXT = "Какой вид поддержки Вам нужен?"
+SUPPORT_OPTIONS = {
+ "faq": "Общие вопросы",
+ "troubleshooting": "Проблемы с продуктами",
+ "callback_request": "Запрос на обратный звонок",
+}
+SUPPROT_MENU_BUTTONS = get_buttons(SUPPORT_OPTIONS)
+
+# Информация о компании - кнопки и текст
+COMPANY_ABOUT = "Вот несколько вариантов информации о нашей компании. Что именно вас интересует?"
+
+
+# Портфолио - кнопки и текст
+PORTFOLIO_MENU_TEXT = "Ниже ссылка на наше портфолио и другие наши проекты"
+PORTFOLIO_OTHER_PROJECTS_TEXT = "Еще примеры наших работ:"
+PORTFOLIO_MENU_OPTIONS = {
+ "portfolio_button": "Наше портфолио",
+ "other_projects": "Посмотреть другие проекты",
+}
+PORTFOLIO_URL = "https://scid.ru/cases"
+PORTFOLIO_BUTTONS = get_buttons(PORTFOLIO_MENU_OPTIONS)
+PORTFOLIO_DEFAULT_DATA = {"name": "Портфолио", "url": "https://scid.ru/cases"}
+
+# Продукты
+PRODUCT_LIST_TEXT = (
+ "Мы предлагаем следющие продукты и услуги. Что Вас интересует?"
+)
+PRODUCT_LIST = []
+
+# Константы проекта
+DEFAULT_STR_LEN = 150
+PHONE_NUMBER_LEN = 25
+PHONE_NUMBER_REGEX = r"(\+\d{5,25}$|\d{5,25}$)"
diff --git a/app/core/base.py b/app/core/base.py
index b794672..f42e917 100644
--- a/app/core/base.py
+++ b/app/core/base.py
@@ -1,3 +1,12 @@
"""Импорты класса Base и всех моделей для Alembic."""
from app.core.db import Base # noqa
-from app.models import models # noqa
+from app.models.models import ( # noqa
+ User,
+ ProductCategory,
+ CategoryType,
+ InformationAboutCompany,
+ CheckCompanyPortfolio,
+ Info,
+ ContactManager,
+ Feedback
+)
diff --git a/app/core/db.py b/app/core/db.py
index 44a1f84..88d83ab 100644
--- a/app/core/db.py
+++ b/app/core/db.py
@@ -3,7 +3,6 @@
from sqlalchemy.orm import (
declarative_base, declared_attr, sessionmaker, Mapped, mapped_column
)
-from contextlib import asynccontextmanager
from .settings import settings
@@ -18,11 +17,5 @@ def __tablename__(cls):
Base = declarative_base(cls=PreBase)
-engine = create_async_engine(settings.database_url)
+engine = create_async_engine(settings.database_url, echo=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession)
-
-
-@asynccontextmanager
-async def get_async_session() -> AsyncSession:
- async with AsyncSessionLocal() as async_session:
- yield async_session
diff --git a/app/core/init_db.py b/app/core/init_db.py
new file mode 100644
index 0000000..5f6f59a
--- /dev/null
+++ b/app/core/init_db.py
@@ -0,0 +1,14 @@
+from core.db import AsyncSessionLocal
+from crud.about_crud import company_info_crud
+from const import PORTFOLIO_DEFAULT_DATA
+
+
+async def add_portfolio():
+ """Добавить ссылку на портфолио при запуске бота."""
+ async with AsyncSessionLocal() as async_session:
+ if not await company_info_crud.get_by_about_name(
+ PORTFOLIO_DEFAULT_DATA.get("name"), async_session
+ ):
+ await company_info_crud.create(
+ PORTFOLIO_DEFAULT_DATA, async_session
+ )
diff --git a/app/core/settings.py b/app/core/settings.py
index c286daa..c9dc8aa 100644
--- a/app/core/settings.py
+++ b/app/core/settings.py
@@ -4,6 +4,7 @@
class Settings(BaseSettings):
database_url: str
bot_token: str
+ telegram_chat_ids: str
class Config:
env_file = '.env'
diff --git a/app/crud/about_crud.py b/app/crud/about_crud.py
new file mode 100644
index 0000000..84fae91
--- /dev/null
+++ b/app/crud/about_crud.py
@@ -0,0 +1,42 @@
+from .base_crud import CRUDBase
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from models.models import InformationAboutCompany
+# from core.settings import PORTFOLIO_DEFAULT_DATA
+
+
+PORTFOLIO_DEFAULT_DATA = {"name": "Портфолио", "url": "https://scid.ru/cases"}
+
+
+class AboutCRUD(CRUDBase):
+ async def get_by_about_name(
+ self,
+ about_name: str,
+ session: AsyncSession,
+ ):
+ """Получить объект модели по тексту названия."""
+ db_obj = await session.execute(
+ select(self.model).where(self.model.name == about_name)
+ )
+ return db_obj.scalars().first()
+
+ async def get_portfolio(self, session: AsyncSession):
+ """Получить объект в котором хранится ссылка на портфолио."""
+ portfolio_obj = await session.execute(
+ select(self.model).where(
+ self.model.name == PORTFOLIO_DEFAULT_DATA.get("name")
+ )
+ )
+ return portfolio_obj.scalars().first()
+
+ async def get_multi(self, session: AsyncSession):
+ """Получить список всех объектов модели из БД."""
+ db_objs = await session.execute(
+ select(self.model).where(self.model.id != 1)
+ )
+ return db_objs.scalars().all()
+
+
+company_info_crud = AboutCRUD(InformationAboutCompany)
diff --git a/app/crud/base_crud.py b/app/crud/base_crud.py
new file mode 100644
index 0000000..517e68e
--- /dev/null
+++ b/app/crud/base_crud.py
@@ -0,0 +1,51 @@
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+
+class CRUDBase:
+ def __init__(self, model) -> None:
+ self.model = model
+
+ async def create(
+ self,
+ obj_in: dict,
+ session: AsyncSession,
+ ):
+ db_obj = self.model(**obj_in)
+ session.add(db_obj)
+ await session.commit()
+ await session.refresh(db_obj)
+ return db_obj
+
+ async def update(self, db_obj, obj_in, session: AsyncSession):
+ """Внести изменения в объект модели в БД."""
+ obj_fields = db_obj.__dict__
+ for field in obj_fields:
+ if field in obj_in:
+ setattr(db_obj, field, obj_in[field])
+ session.add(db_obj)
+ await session.commit()
+ await session.refresh(db_obj)
+ return db_obj
+
+ async def get(
+ self,
+ obj_id: int,
+ session: AsyncSession,
+ ):
+ """Получить объект модели по ее id."""
+ db_obj = await session.execute(
+ select(self.model).where(self.model.id == obj_id)
+ )
+ return db_obj.scalars().first()
+
+ async def get_multi(self, session: AsyncSession):
+ """Получить список всех объектов модели из БД."""
+ db_objs = await session.execute(select(self.model))
+ return db_objs.scalars().all()
+
+ async def remove(self, db_obj, session: AsyncSession):
+ """Удалить объект модели из БД."""
+ await session.delete(db_obj)
+ await session.commit()
+ return db_obj
diff --git a/app/crud/category_product.py b/app/crud/category_product.py
new file mode 100644
index 0000000..12565d4
--- /dev/null
+++ b/app/crud/category_product.py
@@ -0,0 +1,50 @@
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from crud.base_crud import CRUDBase
+from models.models import CategoryType
+
+
+class CategoryTypeCRUD(CRUDBase):
+ async def get_category_by_product_id(
+ self, product_id: int, session: AsyncSession
+ ):
+ """Получить список всех вариантов продукта."""
+ product_categories = await session.execute(
+ select(self.model).where(self.model.product_id == product_id)
+ )
+ return product_categories.scalars().all()
+
+ async def get_active_field(
+ self, product_id: int, category_name: str, session: AsyncSession
+ ):
+ """Получить поля варианта конкретного продукта."""
+ active_field = await session.execute(
+ select(self.model).where(
+ self.model.product_id == product_id,
+ self.model.name == category_name,
+ )
+ )
+ return active_field.scalars().first()
+
+ async def get_multi_for_product(
+ self, product_id: int, session: AsyncSession
+ ):
+ categories_for_product = await session.execute(
+ select(self.model).where(self.model.product_id == product_id)
+ )
+ return categories_for_product.scalars().all()
+
+ async def get_category_by_name(
+ self, product_id: int, field_name: str, session: AsyncSession
+ ):
+ field = await session.execute(
+ select(self.model).where(
+ self.model.product_id == product_id,
+ self.model.name == field_name,
+ )
+ )
+ return field.scalars().first()
+
+
+category_product_crud = CategoryTypeCRUD(CategoryType)
diff --git a/app/crud/feedback_crud.py b/app/crud/feedback_crud.py
new file mode 100644
index 0000000..9fe5c7a
--- /dev/null
+++ b/app/crud/feedback_crud.py
@@ -0,0 +1,57 @@
+from sqlalchemy import select, desc
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.orm import joinedload
+
+from .base_crud import CRUDBase
+from models.models import Feedback
+
+
+class FeedbackCRUD(CRUDBase):
+ async def get_new_feedbacks(
+ self,
+ session: AsyncSession,
+ ):
+ """Получить список пользователей ожидающих обратного звонка."""
+ users_to_callback = await session.execute(
+ select(self.model)
+ .where(self.model.unread)
+ .order_by(desc(self.model.feedback_date))
+ )
+ return users_to_callback.scalars().all()
+
+ async def mark_as_read(self, feedback: Feedback, session: AsyncSession):
+ """Открыть заявку на обратный звонок."""
+ setattr(feedback, "unread", False)
+ session.add(feedback)
+ await session.commit()
+ await session.refresh(feedback)
+ return feedback
+
+ async def get_multi(self, session: AsyncSession):
+ """Получить список всех объектов модели из БД."""
+ db_objs = await session.execute(
+ select(self.model).order_by(desc(self.model.feedback_date))
+ )
+ return db_objs.scalars().all()
+
+ async def bulk_create(
+ self,
+ objs_in: list,
+ session: AsyncSession,
+ ):
+ db_objs = [self.model(**obj) for obj in objs_in]
+ session.add_all(db_objs)
+ await session.commit()
+ return db_objs
+
+ async def get(self, feedback_id: int, session: AsyncSession):
+ """Получить объект отзыва вместе с его автором"""
+ feedback_with_user = await session.execute(
+ select(Feedback)
+ .options(joinedload(Feedback.author))
+ .where(Feedback.id == feedback_id)
+ )
+ return feedback_with_user.scalar_one()
+
+
+feedback_crud = FeedbackCRUD(Feedback)
diff --git a/app/crud/info_crud.py b/app/crud/info_crud.py
new file mode 100644
index 0000000..d464f85
--- /dev/null
+++ b/app/crud/info_crud.py
@@ -0,0 +1,32 @@
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from .base_crud import CRUDBase
+from models.models import Info
+
+
+class InfoCRUD(CRUDBase):
+ async def get_by_question_text(
+ self,
+ question_text: str,
+ session: AsyncSession,
+ ):
+ """Получить объект модели вопроса по тексту вопроса."""
+ db_obj = await session.execute(
+ select(self.model).where(self.model.question == question_text)
+ )
+ return db_obj.scalars().first()
+
+ async def get_all_questions_by_type(
+ self,
+ question_type: str,
+ session: AsyncSession,
+ ):
+ """Получить список всех вопросов в категории."""
+ obj_list = await session.execute(
+ select(self.model).where(self.model.question_type == question_type)
+ )
+ return obj_list.scalars().all()
+
+
+info_crud = InfoCRUD(Info)
diff --git a/app/crud/portfolio_projects_crud.py b/app/crud/portfolio_projects_crud.py
new file mode 100644
index 0000000..c475e7f
--- /dev/null
+++ b/app/crud/portfolio_projects_crud.py
@@ -0,0 +1,22 @@
+from .base_crud import CRUDBase
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from models.models import CheckCompanyPortfolio
+
+
+class PortfolioProjectsCRUD(CRUDBase):
+ async def get_by_project_name(
+ self,
+ project_name: str,
+ session: AsyncSession,
+ ):
+ """Получить проект портфолио по тексту названия."""
+ portfolio_project = await session.execute(
+ select(self.model).where(self.model.project_name == project_name)
+ )
+ return portfolio_project.scalars().first()
+
+
+portfolio_crud = PortfolioProjectsCRUD(CheckCompanyPortfolio)
diff --git a/app/crud/product_crud.py b/app/crud/product_crud.py
new file mode 100644
index 0000000..c903e31
--- /dev/null
+++ b/app/crud/product_crud.py
@@ -0,0 +1,29 @@
+from .base_crud import CRUDBase
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from models.models import ProductCategory
+
+
+class ProductCRUD(CRUDBase):
+ async def get_last_added_product(self, session: AsyncSession):
+ """Получить последний созданный продукт."""
+ last_product = await session.execute(
+ select(self.model).order_by(-self.model.id)
+ )
+ return last_product.scalars().first()
+
+ async def get_by_product_name(
+ self,
+ product_name: str,
+ session: AsyncSession,
+ ):
+ """Получить объект модели по тексту названия."""
+ product = await session.execute(
+ select(self.model).where(self.model.title == product_name)
+ )
+ return product.scalars().first()
+
+
+product_crud = ProductCRUD(ProductCategory)
diff --git a/app/crud/projects.py b/app/crud/projects.py
index 0b0bb83..4b7e8f6 100644
--- a/app/crud/projects.py
+++ b/app/crud/projects.py
@@ -1,4 +1,4 @@
-from core.db import get_async_session
+from sqlalchemy.ext.asyncio import AsyncSession
from models.models import (
CheckCompanyPortfolio, ProductCategory,
CategoryType
@@ -7,56 +7,50 @@
async def get_all_prtfolio_projects(
- object_model: CheckCompanyPortfolio | ProductCategory
+ object_model: CheckCompanyPortfolio | ProductCategory,
+ session: AsyncSession
) -> list[CheckCompanyPortfolio | ProductCategory]:
"""Получение всех проектов-портфолио или продуктов и услуг."""
- async with get_async_session() as session:
- result = await session.execute(select(object_model))
+ result = await session.execute(select(object_model))
- return result.scalars().all()
+ return result.scalars().all()
-async def response_text_by_id(id: int) -> str:
+async def response_text_by_id(id: int, session: AsyncSession) -> str:
"""Возвращает ответ на выбранную категорию."""
- async with get_async_session() as session:
+ result = await session.execute(
+ select(ProductCategory.response).where(ProductCategory.id == id)
+ )
- result = await session.execute(
- select(ProductCategory.response).where(ProductCategory.id == id)
- )
-
- return result.scalar()
+ return result.scalar()
async def get_categories_by_name(
- product_name: str
+ product_name: str, session: AsyncSession
) -> list[CategoryType]:
"""Получить все типы по категории по его названию."""
- async with get_async_session() as session:
-
- result = await session.execute(
- select(CategoryType)
- .join(
- ProductCategory, ProductCategory.id == CategoryType.product_id
- )
- .where(ProductCategory.title == product_name)
+ result = await session.execute(
+ select(CategoryType)
+ .join(
+ ProductCategory, ProductCategory.id == CategoryType.product_id
)
+ .where(ProductCategory.title == product_name)
+ )
- return result.scalars().all()
+ return result.scalars().all()
-async def get_title_by_id(category_id: int) -> str:
+async def get_title_by_id(category_id: int, session: AsyncSession) -> str:
"""Получает название категории по ID из базы данных."""
- async with get_async_session() as session:
-
- result = await session.execute(
- select(ProductCategory.title).where(
- ProductCategory.id == category_id
- )
+ result = await session.execute(
+ select(ProductCategory.title).where(
+ ProductCategory.id == category_id
)
- category_name = result.scalar()
+ )
+ category_name = result.scalar()
- return category_name
+ return category_name
diff --git a/app/crud/questions.py b/app/crud/questions.py
index 949942b..d43b7e5 100644
--- a/app/crud/questions.py
+++ b/app/crud/questions.py
@@ -1,28 +1,28 @@
-from core.db import get_async_session
+from sqlalchemy.ext.asyncio import AsyncSession
from models.models import Info
from sqlalchemy import select
-async def get_question_by_title(question_type) -> list[Info]:
+async def get_question_by_title(
+ question_type: str, session: AsyncSession
+) -> list[Info]:
"""Получаем все вопросы по категории."""
- async with get_async_session() as session:
+ result = await session.execute(
+ select(Info).where(Info.question_type == question_type)
+ )
- result = await session.execute(
- select(Info).where(Info.question_type == question_type)
- )
+ return result.scalars().all()
- return result.scalars().all()
-
-async def get_question_by_id(question_id: int) -> Info | None:
+async def get_question_by_id(
+ question_id: int, session: AsyncSession
+) -> Info | None:
"""Получить вопрос по его ID."""
- async with get_async_session() as session:
-
- result = await session.execute(
- select(Info).where(Info.id == int(question_id))
- )
+ result = await session.execute(
+ select(Info).where(Info.id == int(question_id))
+ )
- return result.scalar_one_or_none()
+ return result.scalar_one_or_none()
diff --git a/app/crud/request_to_manager.py b/app/crud/request_to_manager.py
index d256bb8..6f0c382 100644
--- a/app/crud/request_to_manager.py
+++ b/app/crud/request_to_manager.py
@@ -1,24 +1,23 @@
from datetime import datetime
-from core.db import get_async_session
+from sqlalchemy.ext.asyncio import AsyncSession
from models.models import ContactManager
async def create_request_to_manager(
- user_data: dict, request_type: str
+ user_data: dict, request_type: str, session: AsyncSession
) -> ContactManager:
"""Создание заявки на связь с менеджером."""
- async with get_async_session() as session:
- data_to_db = ContactManager(
- **user_data,
- shipping_date=datetime.utcnow(),
- need_support=(request_type == 'callback_request'),
- need_contact_with_manager=(request_type == 'contact_manager')
- )
+ data_to_db = ContactManager(
+ **user_data,
+ shipping_date=datetime.utcnow(),
+ need_support=(request_type == 'callback_request'),
+ need_contact_with_manager=(request_type == 'contact_manager')
+ )
- session.add(data_to_db)
- await session.commit()
- await session.refresh(data_to_db)
+ session.add(data_to_db)
+ await session.commit()
+ await session.refresh(data_to_db)
- return data_to_db
+ return data_to_db
diff --git a/app/crud/user_crud.py b/app/crud/user_crud.py
new file mode 100644
index 0000000..4138ae7
--- /dev/null
+++ b/app/crud/user_crud.py
@@ -0,0 +1,68 @@
+from datetime import datetime
+
+from .base_crud import CRUDBase
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from models.models import User
+
+
+class UserCRUD(CRUDBase):
+ async def get_users_with_callback_request(
+ self,
+ session: AsyncSession,
+ ):
+ """Получить список пользователей ожидающих обратного звонка."""
+
+ users_to_callback = await session.execute(
+ select(self.model)
+ .where(self.model.callback_request)
+ .order_by(self.model.callback_request_date)
+ )
+
+ return users_to_callback.scalars().all()
+
+ async def get_user_by_tg_id(self, tg_id: int, session: AsyncSession):
+ """Получить пользователя по его tg_id."""
+
+ user = await session.execute(
+ select(self.model).where(self.model.telegram_id == tg_id)
+ )
+
+ return user.scalars().first()
+
+ async def case_open(self, user: User, session: AsyncSession):
+ """Открыть заявку на обратный звонок."""
+
+ setattr(user, "callback_request", True)
+ setattr(user, "callback_request_date", datetime.now())
+
+ session.add(user)
+ await session.commit()
+ await session.refresh(user)
+ return user
+
+ async def close_case(self, user: User, session: AsyncSession):
+ """Закрыть заявку на обратный звонок."""
+
+ setattr(user, "callback_request", False)
+ setattr(user, "case_closed_date", datetime.now())
+
+ session.add(user)
+ await session.commit()
+ await session.refresh(user)
+ return user
+
+ async def bulk_create(
+ self,
+ objs_in: list,
+ session: AsyncSession,
+ ):
+ db_objs = [self.model(**obj) for obj in objs_in]
+ session.add_all(db_objs)
+ await session.commit()
+ return db_objs
+
+
+user_crud = UserCRUD(User)
diff --git a/app/crud/users.py b/app/crud/users.py
index f3e64e6..8066ba0 100644
--- a/app/crud/users.py
+++ b/app/crud/users.py
@@ -1,42 +1,36 @@
-from core.db import get_async_session
+from sqlalchemy.ext.asyncio import AsyncSession
from models.models import User
from sqlalchemy import select
-async def create_user_id(tg_id: int) -> User:
+async def create_user_id(tg_id: int, session: AsyncSession) -> User:
"""Запись tg_id в таблицу user."""
- async with get_async_session() as session:
+ data_to_db = User(tg_id=tg_id)
- data_to_db = User(tg_id=tg_id)
+ session.add(data_to_db)
+ await session.commit()
+ await session.refresh(data_to_db)
- session.add(data_to_db)
- await session.commit()
- await session.refresh(data_to_db)
+ return data_to_db
- return data_to_db
-
-async def is_user_in_db(tg_id: int) -> bool:
+async def is_user_in_db(tg_id: int, session: AsyncSession) -> bool:
"""Проверяем, есть ли пользователь в БД."""
- async with get_async_session() as session:
-
- stmt = select(User).where(User.tg_id == tg_id)
+ stmt = select(User).where(User.tg_id == tg_id)
- result = await session.execute(stmt)
- user = result.scalar_one_or_none()
+ result = await session.execute(stmt)
+ user = result.scalar_one_or_none()
- return user is not None
+ return user is not None
-async def get_role_by_tg_id(tg_id: int) -> User:
+async def get_role_by_tg_id(tg_id: int, session: AsyncSession) -> User:
"""Получаем роль пользователя по его tg_id."""
- async with get_async_session() as session:
-
- result = await session.execute(
- select(User.role).where(User.tg_id == tg_id)
- )
+ result = await session.execute(
+ select(User.role).where(User.tg_id == tg_id)
+ )
- return result.scalar()
+ return result.scalar()
diff --git a/app/main.py b/app/main.py
index c3438cb..d8c3ece 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,13 +1,13 @@
import logging
import asyncio
-# from app.core.db import AsyncSessionLocal
-# from app.middlewares.middleware import DataBaseSession
+from core.db import AsyncSessionLocal
+from middlewares.middleware import DataBaseSession
from core.bot_setup import bot, dispatcher, check_token
from bot.handlers import router as message_router
from bot.callbacks import router as callback_router
from bot.fsm_context import router as fsm_context_router
-
+from core.init_db import add_portfolio
logging.basicConfig(
level=logging.INFO,
@@ -36,6 +36,10 @@ async def main() -> None:
try:
logger.info("Запуск бота...")
+ dispatcher.update.middleware(
+ DataBaseSession(session_pool=AsyncSessionLocal)
+ )
+ await add_portfolio()
await dispatcher.start_polling(bot)
except Exception as e:
@@ -44,11 +48,6 @@ async def main() -> None:
if __name__ == "__main__":
try:
- # dispatcher.update.middleware(
- # DataBaseSession(
- # session_pool=AsyncSessionLocal
- # )
- # )
asyncio.run(main())
except KeyboardInterrupt:
diff --git a/app/models/models.py b/app/models/models.py
index e7932a6..1f19d64 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -1,7 +1,7 @@
from datetime import datetime
from sqlalchemy import ForeignKey
-from sqlalchemy.orm import Mapped, mapped_column
+from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
import sqlalchemy.dialects.postgresql as pgsql_types
@@ -45,6 +45,8 @@ class User(Base):
nullable=False
)
+ feedbacks = relationship("Feedback", back_populates="author", cascade="all, delete")
+
class ProductCategory(Base):
"""БД модель продуктов и услуг."""
@@ -57,6 +59,10 @@ class ProductCategory(Base):
pgsql_types.TEXT
)
+ categories = relationship(
+ "CategoryType", cascade="all, delete", back_populates="product_category"
+ )
+
class CategoryType(Base):
"""БД модель типов категорий."""
@@ -67,7 +73,8 @@ class CategoryType(Base):
)
product_id: Mapped[int] = mapped_column(
- ForeignKey('productcategory.id'),
+ ForeignKey('productcategory.id', ondelete='CASCADE'),
+ nullable=False,
index=True
)
@@ -80,6 +87,8 @@ class CategoryType(Base):
nullable=True
)
+ product_category = relationship("ProductCategory", back_populates="categories")
+
class InformationAboutCompany(Base):
"""Бд модель информации о компании."""
@@ -160,3 +169,38 @@ class ContactManager(Base):
server_default=func.now(),
nullable=False
)
+
+ shipping_date_close: Mapped[datetime] = mapped_column(
+ pgsql_types.TIMESTAMP(timezone=True),
+ server_default=func.now(),
+ nullable=False
+ )
+
+ feedbacks = relationship(
+ "Feedback", back_populates="contact_manager", cascade="all, delete"
+ )
+
+
+class Feedback(Base):
+ user: Mapped[int] = mapped_column(
+ ForeignKey("user.id", ondelete="CASCADE"),
+ nullable=False
+ )
+ contact_manager_id: Mapped[int] = mapped_column(
+ ForeignKey("contactmanager.id", ondelete="CASCADE"),
+ nullable=False
+ )
+ feedback_text: Mapped[str] = mapped_column(
+ pgsql_types.TEXT,
+ nullable=False
+ )
+ feedback_date: Mapped[datetime] = mapped_column(
+ pgsql_types.TIMESTAMP,
+ default=datetime.now
+ )
+ unread: Mapped[bool] = mapped_column(
+ pgsql_types.BOOLEAN,
+ default=True
+ )
+ author = relationship("User", back_populates="feedbacks")
+ contact_manager = relationship("ContactManager", back_populates="feedbacks")
From a9a32ebf76c75be0027e4a4b9d398fa9b1aa2f45 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Tue, 8 Oct 2024 17:03:12 +0300
Subject: [PATCH 20/75] add bot_const, update logging
---
app/bot/bot_const.py | 85 +++++++++++++++++
app/bot/callbacks.py | 207 ++++++++++++++++++++++-------------------
app/bot/exceptions.py | 38 +++++---
app/bot/fsm_context.py | 165 +++++++++++---------------------
app/bot/handlers.py | 60 ++++++------
app/helpers.py | 7 ++
6 files changed, 316 insertions(+), 246 deletions(-)
create mode 100644 app/bot/bot_const.py
create mode 100644 app/helpers.py
diff --git a/app/bot/bot_const.py b/app/bot/bot_const.py
new file mode 100644
index 0000000..8b01e46
--- /dev/null
+++ b/app/bot/bot_const.py
@@ -0,0 +1,85 @@
+from aiogram.fsm.state import StatesGroup, State
+
+
+ADMIN_POSITIVE_ANSWER: str = 'Добро пожаловать в админку!'
+
+ADMIN_NEGATIVE_ANSWER: str = 'У вас нет прав администратора!'
+
+START_MESSAGE: str = (
+ 'Здравстуйте! Я ваш виртуальный помощник. '
+ 'Как я могу помочь вам сегодня? '
+)
+
+MESSAGE_FOR_SHOW_PROJECTS: str = (
+ 'Вот некоторые из наших проектов. '
+ 'Выберите, чтобы узнать больше о каждом из них: '
+)
+
+MESSAGE_FOR_PREVIOUS_CHOICE: str = (
+ 'Вы вернулись в оснвное меню. '
+ 'Как я могу помочь вам дальше? '
+)
+
+MESSAGE_FOR_GET_QUESTIONS: str = 'Какой вид технической поддержки вам нужен? '
+
+QUESTION_NOT_FOUND: str = 'Вопрос не найден.'
+
+MESSAGE_FOR_BACK_TO_PRODUCTS: str = 'Вы вернулись к списку продуктов и услуг: '
+
+MESSAGE_FOR_VIEW_PORTFOLIO: str = (
+ 'Вот ссылка на наше портфолио [url]. '
+ 'Хотите узнать больше о конкретны проектах или услугах? '
+)
+
+MESSAGE_FOR_COMPANY_INFO: str = (
+ 'Вот несколько вариантов информации о нашей компании.'
+ 'Что именно вас интересует?'
+)
+
+MESSAGE_FOR_GET_SUPPORT: str = 'Какой вид технической поддержки вам нужен?'
+
+MESSAGE_FOR_PRODUCTS_SERVICES: str = (
+ 'Мы предлагаем следующие продукты и услуги. '
+ 'Какой из них вас интересует?'
+)
+
+
+class Form(StatesGroup):
+ """Форма для связи с менеджером."""
+
+ first_name = State()
+ phone_number = State()
+
+
+QUESTIONS: dict[Form, str] = {
+ Form.first_name: 'Введите ваше имя:',
+ Form.phone_number: (
+ 'Введите ваш номер телефона (в формате +7XXXXXXXXXX '
+ 'или 8XXXXXXXXXX):'
+ )
+}
+
+
+def succses_answer(user_data: dict) -> str:
+ return (
+ f'Спасибо! Наш менеджер свяжется '
+ f'с вами в ближайшее время.\n'
+ f'Отправленная форма:\n'
+ f'Имя: {user_data['first_name']}\n'
+ f'Номер телефона: {user_data['phone_number']}'
+ )
+
+
+INPUT_NUMBER_PHONE: str = (
+ 'Номер телефона должен быть в формате +7XXXXXXXXXX '
+ 'или 8XXXXXXXXXX. Попробуйте снова.'
+)
+
+INPUT_NAME: str = (
+ 'Имя должно содержать только буквы. Попробуйте снова.'
+)
+
+START_INPUT_USER_DATA: str = (
+ 'Пожалуйста, оставьте ваше имя и контактный номер, '
+ 'и наш менеджер свяжется с вами.'
+)
diff --git a/app/bot/callbacks.py b/app/bot/callbacks.py
index bca13aa..45cc084 100644
--- a/app/bot/callbacks.py
+++ b/app/bot/callbacks.py
@@ -5,6 +5,7 @@
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession
+from bot.exceptions import message_exception_handler
from bot.keyborads import (
list_of_projects_keyboard, main_keyboard,
faq_or_problems_with_products_inline_keyboard,
@@ -14,44 +15,57 @@
)
from crud.questions import get_question_by_id
from crud.projects import get_title_by_id, response_text_by_id
+import bot.bot_const as bc
+
router = Router()
logger = logging.getLogger(__name__)
+@message_exception_handler(
+ log_error_text='Ошибка при выводе списка проектов для'
+)
@router.callback_query(F.data == 'show_projects')
-async def show_projects(callback: CallbackQuery):
+async def show_projects(
+ callback: CallbackQuery, session: AsyncSession
+) -> None:
"""Выводит список проектов компании."""
await callback.answer()
- if callback.message:
- await callback.message.edit_text(
- 'Вот некоторые из наших проектов. '
- 'Выберите, чтобы узнать больше о каждом из них: ',
- reply_markup=await list_of_projects_keyboard()
- )
+ await callback.message.edit_text(
+ bc.MESSAGE_FOR_SHOW_PROJECTS,
+ reply_markup=await list_of_projects_keyboard(session)
+ )
- # тут надо дописать блок else, если случилась ошибка
- # писать через (clause guard) ( * )
+ logger.info(
+ f'Пользователь {callback.from_user.id} запросил список проектов'
+ )
+@message_exception_handler(
+ log_error_text='Ошибка при возврате в основное меню для'
+)
@router.callback_query(F.data == 'back_to_main_menu')
async def previous_choice(callback: CallbackQuery) -> None:
"""Возвращает в основное меню."""
await callback.answer()
- if callback.message:
- await callback.message.edit_text(
- 'Вы вернулись в оснвное меню. '
- 'Как я могу помочь вам дальше? ',
- reply_markup=main_keyboard
- )
- # ( * )
+ await callback.message.edit_text(
+ bc.MESSAGE_FOR_PREVIOUS_CHOICE,
+ reply_markup=main_keyboard
+ )
+ logger.info(
+ f'Пользователь {callback.from_user.id} вернулся в основное меню'
+ )
+
+@message_exception_handler(
+ log_error_text='Ошибка при получении вопросов для'
+)
@router.callback_query(F.data.in_(('get_faq', 'get_problems_with_products')))
async def get_questions(
callback: CallbackQuery, session: AsyncSession
@@ -65,15 +79,22 @@ async def get_questions(
else 'PROBLEMS_WITH_PRODUCTS'
)
- if callback.message:
- await callback.message.edit_text(
- "Выберите вопрос:",
- reply_markup=await faq_or_problems_with_products_inline_keyboard(
- question_type, session
- )
+ await callback.message.edit_text(
+ bc.MESSAGE_FOR_GET_QUESTIONS,
+ reply_markup=await faq_or_problems_with_products_inline_keyboard(
+ question_type, session
)
+ )
+
+ logger.info(
+ f'Пользователь {callback.from_user.id} '
+ f'запросил {question_type.lower()}.'
+ )
+@message_exception_handler(
+ log_error_text='Ошибка при получении ответа на вопрос.'
+)
@router.callback_query(F.data.startswith('answer:'))
async def get_faq_answer(
callback: CallbackQuery, session: AsyncSession
@@ -88,16 +109,23 @@ async def get_faq_answer(
if question:
await callback.message.edit_text(
- text=f"{question.answer}",
+ text=f'{question.answer}',
reply_markup=InlineKeyboardBuilder().add(
back_to_main_menu
).as_markup()
)
-
else:
- await callback.message.edit_text("Вопрос не найден.")
+ await callback.message.edit_text(bc.QUESTION_NOT_FOUND)
+
+ logger.info(
+ f'Пользователь {callback.from_user.id} запросил '
+ f'ответ на вопрос {callback.data.split(':')[1]}'
+ )
+@message_exception_handler(
+ log_error_text='Ошибка при возврате к выбору продуктов.'
+)
@router.callback_query(F.data == 'back_to_previous_menu')
async def back_to_products(
callback: CallbackQuery, session: AsyncSession
@@ -106,14 +134,19 @@ async def back_to_products(
await callback.answer()
- if callback.message:
+ await callback.message.edit_text(
+ text=bc.MESSAGE_FOR_BACK_TO_PRODUCTS,
+ reply_markup=await inline_products_and_services(session)
+ )
- await callback.message.edit_text(
- text='Вы вернулись к списку продуктов и услуг:',
- reply_markup=await inline_products_and_services(session)
- )
+ logger.info(
+ f'Пользователь {callback.from_user.id} вернулся к выбору продуктов.'
+ )
+@message_exception_handler(
+ log_error_text='Ошибка при запросе ответа для выбранной категории.'
+)
@router.callback_query(F.data.startswith('category_'))
async def get_response_by_title(
callback: CallbackQuery, session: AsyncSession
@@ -122,71 +155,60 @@ async def get_response_by_title(
await callback.answer()
- if callback.message:
+ category_id = int(callback.data.split('_')[1])
- category_id = int(callback.data.split('_')[1])
-
- await callback.message.edit_text(
- text=await response_text_by_id(category_id, session),
- reply_markup=await category_type_inline_keyboard(
- await get_title_by_id(category_id, session), session
- )
+ await callback.message.edit_text(
+ text=await response_text_by_id(category_id, session),
+ reply_markup=await category_type_inline_keyboard(
+ await get_title_by_id(category_id, session), session
)
+ )
+
+ logger.info(
+ f'Пользователь {callback.from_user.id} выбрал категорию {category_id}.'
+ )
+@message_exception_handler(
+ log_error_text='Ошибка при показе портфолио.'
+)
@router.callback_query(F.data == 'view_portfolio')
async def view_portfolio(callback: CallbackQuery) -> None:
"""Показ портфолио компании."""
- try:
- await callback.answer()
-
- await callback.message.edit_text(
- 'Вот наше портфолио:',
- reply_markup=company_portfolio_choice
- )
-
- logger.info(f"Пользователь {callback.from_user.id} запросил портфолио")
+ await callback.answer()
- except Exception as e:
- logger.error(
- f"Ошибка при показе портфолио для "
- f"пользователя {callback.from_user.id}: {e}"
- )
+ await callback.message.edit_text(
+ bc.MESSAGE_FOR_VIEW_PORTFOLIO,
+ reply_markup=company_portfolio_choice
+ )
- await callback.message.answer(
- "Произошла ошибка. Пожалуйста, попробуйте позже."
- )
+ logger.info(f'Пользователь {callback.from_user.id} запросил портфолио.')
+@message_exception_handler(
+ log_error_text='Ошибка при запросе информации о компании.'
+)
@router.callback_query(F.data == 'company_info')
async def company_info(callback: CallbackQuery) -> None:
"""Информация о компании."""
- try:
- await callback.answer()
-
- await callback.message.edit_text(
- 'Информация о компании:',
- reply_markup=company_information_keyboard
- )
-
- logger.info(
- f"Пользователь {callback.from_user.id} "
- f"запросил информацию о компании"
- )
+ await callback.answer()
- except Exception as e:
- logger.error(
- f"Ошибка при запросе информации о компании "
- f"для пользователя {callback.from_user.id}: {e}"
- )
+ await callback.message.edit_text(
+ bc.MESSAGE_FOR_COMPANY_INFO,
+ reply_markup=company_information_keyboard
+ )
- await callback.message.answer(
- "Произошла ошибка. Пожалуйста, попробуйте позже."
- )
+ logger.info(
+ f'Пользователь {callback.from_user.id} '
+ f'запросил информацию о компании.'
+ )
+@message_exception_handler(
+ log_error_text='Ошибка при запросе техподдержки.'
+)
@router.callback_query(F.data == 'tech_support')
async def get_support(callback: CallbackQuery) -> None:
"""Выводит виды тех. поддержки."""
@@ -194,35 +216,30 @@ async def get_support(callback: CallbackQuery) -> None:
await callback.answer()
await callback.message.edit_text(
- 'Какой вид технической поддержки вам нужен?',
+ bc.MESSAGE_FOR_GET_SUPPORT,
reply_markup=support_keyboard
)
+ logger.info(f'Пользователь {callback.from_user.id} запросил техподдержку.')
+
+@message_exception_handler(
+ log_error_text='Ошибка при запросе информации о продуктах и услугах.'
+)
@router.callback_query(F.data == 'products_services')
async def products_services(
callback: CallbackQuery, session: AsyncSession
) -> None:
"""Информация о продуктах и услугах."""
- try:
- await callback.message.edit_text(
- 'Мы предлагаем следующие продукты и услуги. '
- 'Какой из них вас интересует?',
- reply_markup=await inline_products_and_services(session)
- )
- await callback.answer()
- logger.info(
- f"Пользователь {callback.from_user.id} запросил "
- f"информацию о продуктах и услугах"
- )
+ await callback.answer()
- except Exception as e:
- logger.error(
- f"Ошибка при запросе информации о продуктах и услугах "
- f"для пользователя {callback.from_user.id}: {e}"
- )
+ await callback.message.edit_text(
+ bc.MESSAGE_FOR_PRODUCTS_SERVICES,
+ reply_markup=await inline_products_and_services(session)
+ )
- await callback.message.answer(
- "Произошла ошибка. Пожалуйста, попробуйте позже."
- )
+ logger.info(
+ f'Пользователь {callback.from_user.id} запросил '
+ f'информацию о продуктах и услугах.'
+ )
diff --git a/app/bot/exceptions.py b/app/bot/exceptions.py
index 2f8d846..525d413 100644
--- a/app/bot/exceptions.py
+++ b/app/bot/exceptions.py
@@ -1,18 +1,32 @@
-class RequestExceptionError(Exception):
- """Ошибка запроса."""
- pass
+import logging
+from functools import wraps
+from typing import Callable
+from aiogram.types import Message
-class EmptyResponseError(Exception):
- """В запросе нет необходимых ключей."""
- pass
+logger = logging.getLogger(__name__)
-class ResponceTypeError(TypeError):
- """Неверный тип ответа."""
- pass
+def message_exception_handler(
+ log_error_text: str,
+ message_error_text: str = 'Произошла ошибка. Пожалуйста, попробуйте позже.'
+) -> Callable:
+ """Обработчик ошибок."""
-class UndocumentedStatusError(Exception):
- """Недокументированный статус."""
- pass
+ def decorator(coroutine: Callable):
+ @wraps(coroutine)
+ async def wrapper(*args, **kwargs) -> None:
+ try:
+ await coroutine(*args, **kwargs)
+ except Exception as e:
+ message: Message = args[0] if args else None
+
+ if message:
+ await message.answer(message_error_text)
+
+ logger.error(f"{log_error_text} {e}")
+
+ return wrapper
+
+ return decorator
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index 8813a89..9f10459 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -1,7 +1,7 @@
import logging
from aiogram import F, Router
-from aiogram.fsm.state import StatesGroup, State
+from aiogram.fsm.state import State
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -11,155 +11,102 @@
from bot.validators import (
is_valid_name, is_valid_phone_number, format_phone_number
)
+import bot.bot_const as bc
+from bot.exceptions import message_exception_handler
+
router = Router()
logger = logging.getLogger(__name__)
-class Form(StatesGroup):
- """Форма для связи с менеджером."""
-
- first_name = State()
- phone_number = State()
-
-
-QUESTIONS = {
- Form.first_name: "Введите ваше имя:",
- Form.phone_number: (
- "Введите ваш номер телефона (в формате +7XXXXXXXXXX "
- "или 8XXXXXXXXXX):"
- )
-}
-
-
+@message_exception_handler(
+ log_error_text='Ошибка при выводе формы.'
+)
@router.callback_query(F.data.in_(('contact_manager', 'callback_request')))
async def contact_with_manager(
callback: CallbackQuery, state: FSMContext
) -> None:
"""Выводит форму для связи с менеджером или запрос на обратный звонок."""
- try:
-
- await state.update_data(request_type=callback.data)
+ await state.update_data(request_type=callback.data)
- await callback.message.edit_text(
- 'Пожалуйста, оставьте ваше имя и контактный номер, '
- 'и наш менеджер свяжется с вами.'
- )
+ await callback.message.edit_text(bc.START_INPUT_USER_DATA)
- await ask_next_question(callback.message, state, Form.first_name)
+ await ask_next_question(callback.message, state, bc.Form.first_name)
- logger.info(
- f"Пользователь {callback.from_user.id} начал процесс."
- )
-
- except Exception as e:
- logger.error(
- f"Ошибка при выводе формы для пользователя "
- f"{callback.from_user.id}: {e}"
- )
-
- await callback.message.answer("Произошла ошибка. Попробуйте снова.")
+ logger.info(f'Пользователь {callback.from_user.id} начал процесс.')
+@message_exception_handler(
+ log_error_text='Ошибка при переходе к следующему вопросу.'
+)
async def ask_next_question(
- message: Message, state: FSMContext, next_state: State
+ message: Message, state: FSMContext, next_state: State
) -> None:
"""Переход к следующему вопросу."""
- try:
- await state.set_state(next_state.state)
- await message.answer(QUESTIONS[next_state])
- logger.info(
- f"Переход к следующему вопросу: {next_state}"
- )
+ await state.set_state(next_state.state)
+ await message.answer(bc.QUESTIONS[next_state])
- except Exception as e:
- logger.error(
- f"Ошибка при переходе к следующему вопросу для пользователя "
- f"{message.from_user.id}: {e}"
- )
+ logger.info(f'Переход к следующему вопросу: {next_state}.')
- await message.answer("Произошла ошибка. Попробуйте снова.")
-
-@router.message(Form.first_name)
+@message_exception_handler(
+ log_error_text='Ошибка при обработке имени пользователя.'
+)
+@router.message(bc.Form.first_name)
async def process_first_name(message: Message, state: FSMContext) -> None:
"""Состояние: ввод имени."""
- try:
- if not is_valid_name(message.text):
- await message.answer(
- "Имя должно содержать только буквы. Попробуйте снова."
- )
- return
-
- await state.update_data(first_name=message.text)
+ if not is_valid_name(message.text):
+ await message.answer(bc.INPUT_NAME)
+ return
- logger.info(
- f"Пользователь {message.from_user.id} ввёл имя: {message.text}"
- )
+ await state.update_data(first_name=message.text)
- await ask_next_question(message, state, Form.phone_number)
-
- except Exception as e:
- logger.error(
- f"Ошибка при обработке имени пользователя "
- f"{message.from_user.id}: {e}"
- )
+ logger.info(
+ f'Пользователь {message.from_user.id} ввёл имя: {message.text}.'
+ )
- await message.answer("Произошла ошибка. Попробуйте снова.")
+ await ask_next_question(message, state, bc.Form.phone_number)
-@router.message(Form.phone_number)
+@message_exception_handler(
+ log_error_text='Ошибка при обработке номера телефона пользователя.'
+)
+@router.message(bc.Form.phone_number)
async def process_phone_number(
message: Message, state: FSMContext, session: AsyncSession
) -> None:
"""Состояние: ввод номера телефона."""
- try:
- if not is_valid_phone_number(message.text):
- await message.answer(
- "Номер телефона должен быть в формате +7XXXXXXXXXX "
- "или 8XXXXXXXXXX. Попробуйте снова."
- )
- return
+ if not is_valid_phone_number(message.text):
+ await message.answer(bc.INPUT_NUMBER_PHONE)
+ return
- formatted_phone_number = format_phone_number(message.text)
+ formatted_phone_number = format_phone_number(message.text)
- await state.update_data(phone_number=formatted_phone_number)
+ await state.update_data(phone_number=formatted_phone_number)
- logger.info(
- f"Пользователь {message.from_user.id} ввёл телефон: "
- f"{formatted_phone_number}"
- )
-
- user_data = await state.get_data()
- request_type = user_data.pop('request_type')
-
- new_request = await create_request_to_manager(
- user_data, request_type, session
- )
+ logger.info(
+ f'Пользователь {message.from_user.id} ввёл телефон: '
+ f'{formatted_phone_number}.'
+ )
- logger.info(f"Запись создана в БД с ID: {new_request.id}")
+ user_data = await state.get_data()
+ request_type = user_data.pop('request_type')
- await message.answer(
- f'Спасибо! Наш менеджер свяжется '
- f'с вами в ближайшее время.\n'
- f"Отправленная форма:\n"
- f"Имя: {user_data['first_name']}\n"
- f"Номер телефона: {user_data['phone_number']}",
- reply_markup=InlineKeyboardBuilder().add(
- back_to_main_menu)
- .as_markup()
- )
+ new_request = await create_request_to_manager(
+ user_data, request_type, session
+ )
- await state.clear()
+ logger.info(f'Запись создана в БД с ID: {new_request.id}.')
- except Exception as e:
- logger.error(
- f"Ошибка при обработке номера телефона пользователя "
- f"{message.from_user.id}: {e}"
- )
+ await message.answer(
+ bc.succses_answer(user_data),
+ reply_markup=InlineKeyboardBuilder().add(
+ back_to_main_menu
+ ).as_markup()
+ )
- await message.answer("Произошла ошибка. Попробуйте снова.")
+ await state.clear()
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index e3457b1..eddbebe 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -5,62 +5,62 @@
from sqlalchemy.ext.asyncio import AsyncSession
from admin.keyboards.keyboards import get_inline_keyboard
from const import MAIN_MENU_BUTTONS
-
+from bot.bot_const import (
+ ADMIN_NEGATIVE_ANSWER, ADMIN_POSITIVE_ANSWER, START_MESSAGE
+)
from models.models import RoleEnum
from crud.users import (
create_user_id, get_role_by_tg_id, is_user_in_db
)
-from bot.keyborads import (
- main_keyboard
-)
+from bot.keyborads import main_keyboard
+from bot.exceptions import message_exception_handler
+from helpers import get_user_id
router = Router()
logger = logging.getLogger(__name__)
-# TODO: данные из message нужно достать один раз,
-# сейчас в каждой фукнции дергаем
-
+@message_exception_handler(
+ log_error_text='Ошибка при обработке команды /admin.'
+)
@router.message(Command('admin'))
async def cmd_admin(message: Message, session: AsyncSession) -> None:
"""Вход в админку."""
- role = await get_role_by_tg_id(message.from_user.id, session)
+ user_id = get_user_id(message)
+ role = await get_role_by_tg_id(user_id, session)
if role in (RoleEnum.ADMIN, RoleEnum.MANAGER):
await message.answer(
- 'Добро пожаловать в админку!',
+ ADMIN_POSITIVE_ANSWER,
reply_markup=await get_inline_keyboard(MAIN_MENU_BUTTONS)
)
else:
- await message.answer('403: Forbidden')
+ await message.answer(ADMIN_NEGATIVE_ANSWER)
+
+ logger.info(
+ f'Пользователь {user_id} вызвал команду /admin.'
+ )
+@message_exception_handler(
+ log_error_text='Ошибка при обработке команды /start.'
+)
@router.message(CommandStart())
async def cmd_start(message: Message, session: AsyncSession) -> None:
"""Выводит приветствие пользователя."""
- if not await is_user_in_db(message.from_user.id, session):
- await create_user_id(message.from_user.id, session)
+ user_id = get_user_id(message)
- try:
- await message.answer(
- 'Здравстуйте! Я ваш виртуальный помощник. '
- 'Как я могу помочь вам сегодня?',
- reply_markup=main_keyboard
- )
+ if not await is_user_in_db(user_id, session):
+ await create_user_id(user_id, session)
- logger.info(
- f"Пользователь {message.from_user.id} вызвал команду /start"
- )
+ await message.answer(
+ START_MESSAGE,
+ reply_markup=main_keyboard
+ )
- except Exception as e:
- logger.error(
- f"Ошибка при обработке команды /start для пользователя "
- f"{message.from_user.id}: {e}"
- )
-
- await message.answer(
- "Произошла ошибка. Пожалуйста, попробуйте позже."
- )
+ logger.info(
+ f'Пользователь {user_id} вызвал команду /start.'
+ )
diff --git a/app/helpers.py b/app/helpers.py
new file mode 100644
index 0000000..89c61fd
--- /dev/null
+++ b/app/helpers.py
@@ -0,0 +1,7 @@
+from aiogram.types import Message
+
+
+def get_user_id(message: Message) -> int:
+ """Получает ID пользователя из сообщения."""
+
+ return message.from_user.id
From 5e5625f3590fdf2d4b3d00686c0182fdddf84333 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Wed, 9 Oct 2024 11:38:39 +0300
Subject: [PATCH 21/75] fix admin-zone app structure, include admin/user
routers to dispatcher
---
...b37e1_update_user_contactmanager_models.py | 48 -------
alembic/versions/3fe28ef6d0c4_first_commit.py | 102 +++++++++++++++
.../d2365dc6d2ca_merge_2_direction.py | 44 -------
app/admin/filters/filters.py | 5 +-
app/admin/handlers/admin_handlers/admin.py | 4 +-
.../admin_about_company_handlers.py | 4 +-
.../admin_handlers/admin_category_handlers.py | 9 +-
.../admin_handlers/admin_info_handlers.py | 6 +-
.../admin_portfolio_handlers.py | 6 +-
.../admin_handlers/admin_product_handlers.py | 4 +-
app/core/base.py | 5 +-
app/core/db.py | 6 +-
app/main.py | 4 +
app/models/models.py | 119 ++++++------------
app/set_admin.py | 18 +++
15 files changed, 190 insertions(+), 194 deletions(-)
delete mode 100644 alembic/versions/31916cbb37e1_update_user_contactmanager_models.py
create mode 100644 alembic/versions/3fe28ef6d0c4_first_commit.py
delete mode 100644 alembic/versions/d2365dc6d2ca_merge_2_direction.py
create mode 100644 app/set_admin.py
diff --git a/alembic/versions/31916cbb37e1_update_user_contactmanager_models.py b/alembic/versions/31916cbb37e1_update_user_contactmanager_models.py
deleted file mode 100644
index 7980d3d..0000000
--- a/alembic/versions/31916cbb37e1_update_user_contactmanager_models.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Update User, ContactManager models
-
-Revision ID: 31916cbb37e1
-Revises:
-Create Date: 2024-10-04 17:29:06.898325
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = '31916cbb37e1'
-down_revision: Union[str, None] = None
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('contactmanager',
- sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
- sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
- sa.Column('need_support', sa.BOOLEAN(), nullable=False),
- sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
- sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.drop_column('user', 'need_support')
- op.drop_column('user', 'name')
- op.drop_column('user', 'phone')
- op.drop_column('user', 'shipping_date')
- op.drop_column('user', 'need_contact_with_manager')
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.add_column('user', sa.Column('need_contact_with_manager', sa.BOOLEAN(), autoincrement=False, nullable=False))
- op.add_column('user', sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False))
- op.add_column('user', sa.Column('phone', sa.VARCHAR(length=25), autoincrement=False, nullable=True))
- op.add_column('user', sa.Column('name', sa.VARCHAR(length=32), autoincrement=False, nullable=True))
- op.add_column('user', sa.Column('need_support', sa.BOOLEAN(), autoincrement=False, nullable=False))
- op.drop_table('contactmanager')
- # ### end Alembic commands ###
diff --git a/alembic/versions/3fe28ef6d0c4_first_commit.py b/alembic/versions/3fe28ef6d0c4_first_commit.py
new file mode 100644
index 0000000..df8c8e1
--- /dev/null
+++ b/alembic/versions/3fe28ef6d0c4_first_commit.py
@@ -0,0 +1,102 @@
+"""first commit
+
+Revision ID: 3fe28ef6d0c4
+Revises:
+Create Date: 2024-10-09 10:04:32.815358
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision: str = '3fe28ef6d0c4'
+down_revision: Union[str, None] = None
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('checkcompanyportfolio',
+ sa.Column('project_name', sa.VARCHAR(length=48), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('contactmanager',
+ sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
+ sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
+ sa.Column('need_support', sa.BOOLEAN(), nullable=False),
+ sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
+ sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
+ sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('info',
+ sa.Column('question_type', postgresql.ENUM('GENERAL_QUESTIONS', 'PROBLEMS_WITH_PRODUCTS', name='question_enum'), nullable=False),
+ sa.Column('question', sa.TEXT(), nullable=False),
+ sa.Column('answer', sa.TEXT(), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('question')
+ )
+ op.create_table('informationaboutcompany',
+ sa.Column('name', sa.VARCHAR(length=48), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('productcategory',
+ sa.Column('title', sa.VARCHAR(length=150), nullable=False),
+ sa.Column('response', sa.TEXT(), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('user',
+ sa.Column('tg_id', sa.BIGINT(), nullable=False),
+ sa.Column('role', postgresql.ENUM('USER', 'ADMIN', 'MANAGER', name='role_enum'), nullable=False),
+ sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('tg_id')
+ )
+ op.create_table('categorytype',
+ sa.Column('name', sa.VARCHAR(length=150), nullable=False),
+ sa.Column('product_id', sa.Integer(), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('media', sa.VARCHAR(length=128), nullable=True),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_categorytype_product_id'), 'categorytype', ['product_id'], unique=False)
+ op.create_table('feedback',
+ sa.Column('user', sa.Integer(), nullable=False),
+ sa.Column('contact_manager_id', sa.Integer(), nullable=False),
+ sa.Column('feedback_text', sa.TEXT(), nullable=False),
+ sa.Column('feedback_date', postgresql.TIMESTAMP(), nullable=False),
+ sa.Column('unread', sa.BOOLEAN(), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['contact_manager_id'], ['contactmanager.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['user'], ['user.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('feedback')
+ op.drop_index(op.f('ix_categorytype_product_id'), table_name='categorytype')
+ op.drop_table('categorytype')
+ op.drop_table('user')
+ op.drop_table('productcategory')
+ op.drop_table('informationaboutcompany')
+ op.drop_table('info')
+ op.drop_table('contactmanager')
+ op.drop_table('checkcompanyportfolio')
+ # ### end Alembic commands ###
diff --git a/alembic/versions/d2365dc6d2ca_merge_2_direction.py b/alembic/versions/d2365dc6d2ca_merge_2_direction.py
deleted file mode 100644
index 758d7a4..0000000
--- a/alembic/versions/d2365dc6d2ca_merge_2_direction.py
+++ /dev/null
@@ -1,44 +0,0 @@
-"""Merge 2 direction
-
-Revision ID: d2365dc6d2ca
-Revises: 31916cbb37e1
-Create Date: 2024-10-07 12:56:05.967698
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = 'd2365dc6d2ca'
-down_revision: Union[str, None] = '31916cbb37e1'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('feedback',
- sa.Column('user', sa.Integer(), nullable=False),
- sa.Column('feedback_text', sa.TEXT(), nullable=False),
- sa.Column('feedback_date', postgresql.TIMESTAMP(), nullable=False),
- sa.Column('unread', sa.BOOLEAN(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['user'], ['user.id'], ondelete='CASCADE'),
- sa.PrimaryKeyConstraint('id')
- )
- op.drop_constraint('categorytype_product_id_fkey', 'categorytype', type_='foreignkey')
- op.create_foreign_key(None, 'categorytype', 'productcategory', ['product_id'], ['id'], ondelete='CASCADE')
- op.add_column('contactmanager', sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False))
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_column('contactmanager', 'shipping_date_close')
- op.drop_constraint(None, 'categorytype', type_='foreignkey')
- op.create_foreign_key('categorytype_product_id_fkey', 'categorytype', 'productcategory', ['product_id'], ['id'])
- op.drop_table('feedback')
- # ### end Alembic commands ###
diff --git a/app/admin/filters/filters.py b/app/admin/filters/filters.py
index 5dad1f5..3b99874 100644
--- a/app/admin/filters/filters.py
+++ b/app/admin/filters/filters.py
@@ -1,6 +1,7 @@
from aiogram.filters import Filter
from aiogram import Bot, types
# from settings import admin_list
+from const import admin_list
class ChatTypeFilter(Filter):
@@ -15,5 +16,5 @@ class IsAdmin(Filter):
def __init__(self) -> None:
pass
- # async def __call__(self, message: types.Message, bot: Bot) -> bool:
- # return message.from_user.id in admin_list
+ async def __call__(self, message: types.Message, bot: Bot) -> bool:
+ return message.from_user.id in admin_list
diff --git a/app/admin/handlers/admin_handlers/admin.py b/app/admin/handlers/admin_handlers/admin.py
index f136584..dafa956 100644
--- a/app/admin/handlers/admin_handlers/admin.py
+++ b/app/admin/handlers/admin_handlers/admin.py
@@ -7,8 +7,8 @@
from crud.feedback_crud import feedback_crud
from crud.user_crud import user_crud
-from filters.filters import ChatTypeFilter, IsAdmin
-from keyboards.keyboards import (
+from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.keyboards.keyboards import (
get_inline_keyboard,
get_inline_paginated_keyboard,
get_paginated_keyboard_size,
diff --git a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
index 5ab413f..1b8fd16 100644
--- a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
@@ -7,8 +7,8 @@
from .admin import SectionState
from crud.about_crud import company_info_crud
-from filters.filters import ChatTypeFilter, IsAdmin
-from keyboards.keyboards import (
+from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.keyboards.keyboards import (
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
diff --git a/app/admin/handlers/admin_handlers/admin_category_handlers.py b/app/admin/handlers/admin_handlers/admin_category_handlers.py
index 7d65fa9..985264e 100644
--- a/app/admin/handlers/admin_handlers/admin_category_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_category_handlers.py
@@ -5,16 +5,17 @@
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
-from handlers.admin_handlers.admin import SectionState
+from admin.handlers.admin_handlers.admin import SectionState
from crud.category_product import category_product_crud
from crud.product_crud import product_crud
-from filters.filters import ChatTypeFilter, IsAdmin
-from handlers.user import ProductCategory
-from keyboards.keyboards import (
+from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.handlers.user import ProductCategory
+from admin.keyboards.keyboards import (
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
# from settings import MAIN_MENU_OPTIONS, admin_list
+from const import admin_list
MAIN_MENU_OPTIONS = {
"company_bio": "Информация о компании",
diff --git a/app/admin/handlers/admin_handlers/admin_info_handlers.py b/app/admin/handlers/admin_handlers/admin_info_handlers.py
index 964ae4e..06e2dde 100644
--- a/app/admin/handlers/admin_handlers/admin_info_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_info_handlers.py
@@ -5,9 +5,9 @@
from aiogram.types import CallbackQuery, Message
from crud.info_crud import info_crud
-from filters.filters import ChatTypeFilter, IsAdmin
-from handlers.admin_handlers.admin import SectionState
-from keyboards.keyboards import (
+from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.handlers.admin_handlers.admin import SectionState
+from admin.keyboards.keyboards import (
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
diff --git a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
index 5051a05..b4f1400 100644
--- a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
@@ -5,11 +5,11 @@
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
-from .admin import SectionState
+from admin.handlers.admin_handlers.admin import SectionState
from crud.about_crud import company_info_crud
from crud.portfolio_projects_crud import portfolio_crud
-from filters.filters import ChatTypeFilter, IsAdmin
-from keyboards.keyboards import (
+from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.keyboards.keyboards import (
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
diff --git a/app/admin/handlers/admin_handlers/admin_product_handlers.py b/app/admin/handlers/admin_handlers/admin_product_handlers.py
index d7c6bf1..2657932 100644
--- a/app/admin/handlers/admin_handlers/admin_product_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_product_handlers.py
@@ -8,8 +8,8 @@
from .admin import SectionState
from crud.category_product import category_product_crud
from crud.product_crud import product_crud
-from filters.filters import ChatTypeFilter, IsAdmin
-from keyboards.keyboards import (
+from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.keyboards.keyboards import (
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
diff --git a/app/core/base.py b/app/core/base.py
index f42e917..12750f6 100644
--- a/app/core/base.py
+++ b/app/core/base.py
@@ -1,5 +1,6 @@
"""Импорты класса Base и всех моделей для Alembic."""
-from app.core.db import Base # noqa
+
+from app.core.db import Base # noqa
from app.models.models import ( # noqa
User,
ProductCategory,
@@ -8,5 +9,5 @@
CheckCompanyPortfolio,
Info,
ContactManager,
- Feedback
+ Feedback,
)
diff --git a/app/core/db.py b/app/core/db.py
index 88d83ab..7ae85cd 100644
--- a/app/core/db.py
+++ b/app/core/db.py
@@ -1,7 +1,7 @@
from sqlalchemy import Integer
-from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
+from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import (
- declarative_base, declared_attr, sessionmaker, Mapped, mapped_column
+ declarative_base, declared_attr, Mapped, mapped_column
)
from .settings import settings
@@ -18,4 +18,4 @@ def __tablename__(cls):
Base = declarative_base(cls=PreBase)
engine = create_async_engine(settings.database_url, echo=True)
-AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession)
+AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession)
diff --git a/app/main.py b/app/main.py
index d8c3ece..5e1dba5 100644
--- a/app/main.py
+++ b/app/main.py
@@ -8,6 +8,8 @@
from bot.callbacks import router as callback_router
from bot.fsm_context import router as fsm_context_router
from core.init_db import add_portfolio
+from admin.handlers.admin_handlers import admin_router
+from admin.handlers.user import user_router
logging.basicConfig(
level=logging.INFO,
@@ -33,6 +35,8 @@ async def main() -> None:
dispatcher.include_router(message_router)
dispatcher.include_router(callback_router)
dispatcher.include_router(fsm_context_router)
+ dispatcher.include_router(admin_router)
+ dispatcher.include_router(user_router)
try:
logger.info("Запуск бота...")
diff --git a/app/models/models.py b/app/models/models.py
index 1f19d64..b8d57fa 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -25,95 +25,75 @@ class User(Base):
"""БД модель пользователя."""
tg_id: Mapped[int] = mapped_column(
- pgsql_types.BIGINT,
- nullable=False,
- unique=True
+ pgsql_types.BIGINT, nullable=False, unique=True
)
role: Mapped[RoleEnum] = mapped_column(
- pgsql_types.ENUM(
- RoleEnum,
- name="role_enum",
- create_type=False
- ),
+ pgsql_types.ENUM(RoleEnum, name="role_enum", create_type=False),
default=RoleEnum.USER,
)
join_date: Mapped[datetime] = mapped_column(
pgsql_types.TIMESTAMP(timezone=True),
server_default=func.now(),
- nullable=False
+ nullable=False,
)
- feedbacks = relationship("Feedback", back_populates="author", cascade="all, delete")
+ feedbacks = relationship(
+ "Feedback", back_populates="author", cascade="all, delete"
+ )
class ProductCategory(Base):
"""БД модель продуктов и услуг."""
- title: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(150)
- )
+ title: Mapped[str] = mapped_column(pgsql_types.VARCHAR(150))
- response: Mapped[str] = mapped_column(
- pgsql_types.TEXT
- )
+ response: Mapped[str] = mapped_column(pgsql_types.TEXT)
categories = relationship(
- "CategoryType", cascade="all, delete", back_populates="product_category"
+ "CategoryType",
+ cascade="all, delete",
+ back_populates="product_category",
)
class CategoryType(Base):
"""БД модель типов категорий."""
- name: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(150),
- nullable=False
- )
+ name: Mapped[str] = mapped_column(pgsql_types.VARCHAR(150), nullable=False)
product_id: Mapped[int] = mapped_column(
ForeignKey('productcategory.id', ondelete='CASCADE'),
nullable=False,
- index=True
+ index=True,
)
- url: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(128)
- )
+ url: Mapped[str] = mapped_column(pgsql_types.VARCHAR(128))
- media: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(128),
- nullable=True
- )
+ media: Mapped[str] = mapped_column(pgsql_types.VARCHAR(128), nullable=True)
- product_category = relationship("ProductCategory", back_populates="categories")
+ product_category = relationship(
+ "ProductCategory", back_populates="categories"
+ )
class InformationAboutCompany(Base):
"""Бд модель информации о компании."""
- name: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(48),
- nullable=False
- )
+ name: Mapped[str] = mapped_column(pgsql_types.VARCHAR(48), nullable=False)
- url: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(128)
- )
+ url: Mapped[str] = mapped_column(pgsql_types.VARCHAR(128))
class CheckCompanyPortfolio(Base):
"""Бд модель информации о проектах."""
project_name: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(48),
- nullable=False
+ pgsql_types.VARCHAR(48), nullable=False
)
- url: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(128)
- )
+ url: Mapped[str] = mapped_column(pgsql_types.VARCHAR(128))
class Info(Base):
@@ -121,59 +101,45 @@ class Info(Base):
question_type: Mapped[QuestionEnum] = mapped_column(
pgsql_types.ENUM(
- QuestionEnum,
- name='question_enum',
- create_type=False
+ QuestionEnum, name='question_enum', create_type=False
),
- nullable=False
+ nullable=False,
)
- question: Mapped[str] = mapped_column(
- pgsql_types.TEXT,
- unique=True
- )
+ question: Mapped[str] = mapped_column(pgsql_types.TEXT, unique=True)
- answer: Mapped[str] = mapped_column(
- pgsql_types.TEXT,
- nullable=False
- )
+ answer: Mapped[str] = mapped_column(pgsql_types.TEXT, nullable=False)
class ContactManager(Base):
"""Бд модель заявки к менеджеру."""
first_name: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(32),
- nullable=False
+ pgsql_types.VARCHAR(32), nullable=False
)
phone_number: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(25),
- nullable=False
+ pgsql_types.VARCHAR(25), nullable=False
)
need_support: Mapped[bool] = mapped_column(
- pgsql_types.BOOLEAN,
- default=False,
- nullable=False
+ pgsql_types.BOOLEAN, default=False, nullable=False
)
need_contact_with_manager: Mapped[bool] = mapped_column(
- pgsql_types.BOOLEAN,
- default=False,
- nullable=False
+ pgsql_types.BOOLEAN, default=False, nullable=False
)
shipping_date: Mapped[datetime] = mapped_column(
pgsql_types.TIMESTAMP(timezone=True),
server_default=func.now(),
- nullable=False
+ nullable=False,
)
shipping_date_close: Mapped[datetime] = mapped_column(
pgsql_types.TIMESTAMP(timezone=True),
server_default=func.now(),
- nullable=False
+ nullable=False,
)
feedbacks = relationship(
@@ -183,24 +149,19 @@ class ContactManager(Base):
class Feedback(Base):
user: Mapped[int] = mapped_column(
- ForeignKey("user.id", ondelete="CASCADE"),
- nullable=False
+ ForeignKey("user.id", ondelete="CASCADE"), nullable=False
)
contact_manager_id: Mapped[int] = mapped_column(
- ForeignKey("contactmanager.id", ondelete="CASCADE"),
- nullable=False
+ ForeignKey("contactmanager.id", ondelete="CASCADE"), nullable=False
)
feedback_text: Mapped[str] = mapped_column(
- pgsql_types.TEXT,
- nullable=False
+ pgsql_types.TEXT, nullable=False
)
feedback_date: Mapped[datetime] = mapped_column(
- pgsql_types.TIMESTAMP,
- default=datetime.now
- )
- unread: Mapped[bool] = mapped_column(
- pgsql_types.BOOLEAN,
- default=True
+ pgsql_types.TIMESTAMP, default=datetime.now
)
+ unread: Mapped[bool] = mapped_column(pgsql_types.BOOLEAN, default=True)
author = relationship("User", back_populates="feedbacks")
- contact_manager = relationship("ContactManager", back_populates="feedbacks")
+ contact_manager = relationship(
+ "ContactManager", back_populates="feedbacks"
+ )
diff --git a/app/set_admin.py b/app/set_admin.py
new file mode 100644
index 0000000..2290fbc
--- /dev/null
+++ b/app/set_admin.py
@@ -0,0 +1,18 @@
+import asyncio
+
+from crud.user_crud import user_crud
+from core.db import AsyncSessionLocal
+
+
+async def set_admin():
+ """
+ После первого запуска бота и чистой БД добавить себя в список админов.
+ Не забыть удалить потом этот файл.
+ """
+ async with AsyncSessionLocal() as session:
+ user = await user_crud.get(1, session)
+ await user_crud.update(user, {"role": "ADMIN"}, session)
+
+
+if __name__ == "__main__":
+ asyncio.run(set_admin())
From 8bf1226218bfd0018dfb3c2e8f217c4ff30704f1 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Wed, 9 Oct 2024 15:11:49 +0300
Subject: [PATCH 22/75] some fix
---
app/bot/fsm_context.py | 1 +
app/bot/handlers.py | 6 +++---
app/bot/keyborads.py | 5 ++---
app/core/bot_setup.py | 10 +++-------
app/core/db.py | 1 +
app/core/init_db.py | 1 +
app/core/settings.py | 5 +++++
7 files changed, 16 insertions(+), 13 deletions(-)
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index 9f10459..b58277a 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -6,6 +6,7 @@
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession
+
from bot.keyborads import back_to_main_menu
from crud.request_to_manager import create_request_to_manager
from bot.validators import (
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index eddbebe..5a0dc10 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -3,15 +3,15 @@
from aiogram.filters import CommandStart, Command
from aiogram.types import Message
from sqlalchemy.ext.asyncio import AsyncSession
+
from admin.keyboards.keyboards import get_inline_keyboard
from const import MAIN_MENU_BUTTONS
from bot.bot_const import (
ADMIN_NEGATIVE_ANSWER, ADMIN_POSITIVE_ANSWER, START_MESSAGE
)
from models.models import RoleEnum
-from crud.users import (
- create_user_id, get_role_by_tg_id, is_user_in_db
-)
+from crud.users import create_user_id, get_role_by_tg_id, is_user_in_db
+
from bot.keyborads import main_keyboard
from bot.exceptions import message_exception_handler
from helpers import get_user_id
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index b33b09b..f6832cb 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -1,7 +1,6 @@
-from aiogram.types import (
- InlineKeyboardMarkup, InlineKeyboardButton
-)
+from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
+
from crud.questions import get_question_by_title
from crud.projects import get_all_prtfolio_projects, get_categories_by_name
from models.models import CheckCompanyPortfolio, ProductCategory
diff --git a/app/core/bot_setup.py b/app/core/bot_setup.py
index b692bc5..5af2271 100644
--- a/app/core/bot_setup.py
+++ b/app/core/bot_setup.py
@@ -1,24 +1,20 @@
-import os
import logging
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
-from dotenv import load_dotenv
-load_dotenv()
+from .settings import settings
-# Настройка логирования
logger = logging.getLogger(__name__)
-bot = Bot(token=os.getenv('BOT_TOKEN'))
+bot = Bot(token=settings.bot_token)
dispatcher = Dispatcher(storage=MemoryStorage())
def check_token() -> None:
"""Проверка наличия токена бота."""
- if os.getenv('BOT_TOKEN') is None:
+ if settings.bot_token is None:
logger.error("Токен бота не найден.")
raise ValueError('Отсутствуют необходимые токены.')
-
else:
logger.info("Токен бота успешно загружен.")
diff --git a/app/core/db.py b/app/core/db.py
index 88d83ab..5a03243 100644
--- a/app/core/db.py
+++ b/app/core/db.py
@@ -8,6 +8,7 @@
class PreBase:
+ """Общий класс для всех таблиц."""
@declared_attr
def __tablename__(cls):
diff --git a/app/core/init_db.py b/app/core/init_db.py
index 5f6f59a..e1dc466 100644
--- a/app/core/init_db.py
+++ b/app/core/init_db.py
@@ -5,6 +5,7 @@
async def add_portfolio():
"""Добавить ссылку на портфолио при запуске бота."""
+
async with AsyncSessionLocal() as async_session:
if not await company_info_crud.get_by_about_name(
PORTFOLIO_DEFAULT_DATA.get("name"), async_session
diff --git a/app/core/settings.py b/app/core/settings.py
index c9dc8aa..d2a1b2d 100644
--- a/app/core/settings.py
+++ b/app/core/settings.py
@@ -2,6 +2,11 @@
class Settings(BaseSettings):
+ """
+ Настройка проекта.
+
+ Взятие данных из .env и их валидация.
+ """
database_url: str
bot_token: str
telegram_chat_ids: str
From 49b6360a78e203efc1897867e46c56471bf6c91a Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Thu, 10 Oct 2024 00:26:18 +0300
Subject: [PATCH 23/75] add mail servise and docker compose
---
.env.example | 12 ++-
DockerFile | 6 +-
alembic/README | 13 ++-
alembic/versions/726f3c66f8d4_test_commit.py | 102 ++++++++++++++++++
app/bot/fsm_context.py | 18 +++-
app/bot/smtp.py | 8 +-
app/core/settings.py | 5 +
app/models/models.py | 14 +--
...mpose.production.yml => docker-compose.yml | 2 +-
nginx.conf | 15 +++
poetry.lock | 83 +-------------
pyproject.toml | 1 -
12 files changed, 170 insertions(+), 109 deletions(-)
create mode 100644 alembic/versions/726f3c66f8d4_test_commit.py
rename docker-compose.production.yml => docker-compose.yml (96%)
create mode 100644 nginx.conf
diff --git a/.env.example b/.env.example
index 3231df3..25f7bd9 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,14 @@
-BOT_TOKEN=<'BOT TOKEN'>
DATABASE_URL=<'DATABASE URL'>
TELEGRAM_TOKEN=<'TELEGRAM TOKEN'>
TELEGRAM_CHAT_IDS=<'TELEGRAM CHAT IDS'>
+
+BOT_TOKEN=<'BOT TOKEN'>
+TELEGRAM_CHAT_IDS=<'TELEGRAM CHAT IDS'>
+
+DATABASE_URL=<'DATABASE URL'>
+POSTGRES_USER=<'DB OWNER'>
+POSTGRES_PASSWORD=<'PASSWORD'>
+POSTGRES_DB=<'DB NAME'>
+
EMAIL=<'MANAGER EMAIL'>
-PASSWORD=<'PWD FOR EMAIL'>
\ No newline at end of file
+EMAIL_PASSWORD=<'PASSWORD'>
diff --git a/DockerFile b/DockerFile
index 498fa3f..d3dbca6 100644
--- a/DockerFile
+++ b/DockerFile
@@ -1,4 +1,5 @@
FROM python:3.12.0-slim
+WORKDIR /app
# Установка необходимых системных зависимостей (если нужно)
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -12,9 +13,6 @@ RUN curl -sSL https://install.python-poetry.org | python3 -
# Установка пути для Poetry
ENV PATH="/root/.local/bin:$PATH"
-# Установка рабочего каталога
-WORKDIR /app
-
# Копирование только pyproject.toml и poetry.lock (если есть) для кэширования зависимостей
COPY pyproject.toml poetry.lock* ./
@@ -25,4 +23,4 @@ RUN poetry install --no-root
COPY . .
# Команда для запуска приложения
-CMD ["poetry", "run", "python", "main.py"]
+CMD ["poetry", "run", "python", "app/main.py"]
diff --git a/alembic/README b/alembic/README
index 801b3bd..d7ad639 100644
--- a/alembic/README
+++ b/alembic/README
@@ -3,12 +3,17 @@
poetry run alembic init --template async alembic
```
-Примените миграции
+При первом запуске указать нулевое состояние базы
```bash
-poetry run alembic upgrade head
+poetry run alembic stamp head
```
-Создание миграций, если внесены изменения в /models
+Создать миграции, если внесены изменения в /models
```bash
poetry run alembic revision --autogenerate -m "Your commit"
-```
\ No newline at end of file
+```
+
+Применить миграции
+```bash
+poetry run alembic upgrade head
+```
diff --git a/alembic/versions/726f3c66f8d4_test_commit.py b/alembic/versions/726f3c66f8d4_test_commit.py
new file mode 100644
index 0000000..f72b1ed
--- /dev/null
+++ b/alembic/versions/726f3c66f8d4_test_commit.py
@@ -0,0 +1,102 @@
+"""Test commit
+
+Revision ID: 726f3c66f8d4
+Revises: d2365dc6d2ca
+Create Date: 2024-10-08 19:27:27.701551
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision: str = '726f3c66f8d4'
+down_revision: Union[str, None] = 'd2365dc6d2ca'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('checkcompanyportfolio',
+ sa.Column('project_name', sa.VARCHAR(length=48), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('contactmanager',
+ sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
+ sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
+ sa.Column('need_support', sa.BOOLEAN(), nullable=False),
+ sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
+ sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('info',
+ sa.Column('question_type', postgresql.ENUM('GENERAL_QUESTIONS', 'PROBLEMS_WITH_PRODUCTS', name='question_enum'), nullable=False),
+ sa.Column('question', sa.TEXT(), nullable=False),
+ sa.Column('answer', sa.TEXT(), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('question')
+ )
+ op.create_table('informationaboutcompany',
+ sa.Column('name', sa.VARCHAR(length=48), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('productcategory',
+ sa.Column('title', sa.VARCHAR(length=150), nullable=False),
+ sa.Column('response', sa.TEXT(), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('user',
+ sa.Column('tg_id', sa.BIGINT(), nullable=False),
+ sa.Column('role', postgresql.ENUM('USER', 'ADMIN', 'MANAGER', name='role_enum'), nullable=False),
+ sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('tg_id')
+ )
+ op.create_table('categorytype',
+ sa.Column('name', sa.VARCHAR(length=150), nullable=False),
+ sa.Column('product_id', sa.Integer(), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('media', sa.VARCHAR(length=128), nullable=True),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index(op.f('ix_categorytype_product_id'), 'categorytype', ['product_id'], unique=False)
+ op.create_table('feedback',
+ sa.Column('user', sa.Integer(), nullable=False),
+ sa.Column('contact_manager_id', sa.Integer(), nullable=False),
+ sa.Column('feedback_text', sa.TEXT(), nullable=False),
+ sa.Column('feedback_date', postgresql.TIMESTAMP(), nullable=False),
+ sa.Column('unread', sa.BOOLEAN(), nullable=False),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['contact_manager_id'], ['contactmanager.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['user'], ['user.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('feedback')
+ op.drop_index(op.f('ix_categorytype_product_id'), table_name='categorytype')
+ op.drop_table('categorytype')
+ op.drop_table('user')
+ op.drop_table('productcategory')
+ op.drop_table('informationaboutcompany')
+ op.drop_table('info')
+ op.drop_table('contactmanager')
+ op.drop_table('checkcompanyportfolio')
+ # ### end Alembic commands ###
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_context.py
index 9f10459..e3b682e 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_context.py
@@ -1,18 +1,24 @@
import logging
+import os
+import asyncio
from aiogram import F, Router
from aiogram.fsm.state import State
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession
+
+import bot.bot_const as bc
+from bot.exceptions import message_exception_handler
from bot.keyborads import back_to_main_menu
-from crud.request_to_manager import create_request_to_manager
+from bot.smtp import send_mail
from bot.validators import (
is_valid_name, is_valid_phone_number, format_phone_number
)
-import bot.bot_const as bc
-from bot.exceptions import message_exception_handler
+from crud.request_to_manager import create_request_to_manager
+
+CLIENT_EMAIL = os.getenv("EMAIL")
router = Router()
@@ -102,6 +108,12 @@ async def process_phone_number(
logger.info(f'Запись создана в БД с ID: {new_request.id}.')
+ mail = send_mail('Заявка на обратную связь', CLIENT_EMAIL, user_data)
+ asyncio.gather(asyncio.create_task(mail))
+
+ logger.info("Отправлено сообщение на почту менеджеру для связи "
+ f"с пользователем {message.from_user.id}")
+
await message.answer(
bc.succses_answer(user_data),
reply_markup=InlineKeyboardBuilder().add(
diff --git a/app/bot/smtp.py b/app/bot/smtp.py
index 6cf0abe..050a76f 100644
--- a/app/bot/smtp.py
+++ b/app/bot/smtp.py
@@ -10,11 +10,11 @@
load_dotenv()
BASE_EMAIL = os.getenv("EMAIL")
-PASSWORD = os.getenv("PASSWORD")
+PASSWORD = os.getenv("EMAIL_PASSWORD")
async def send_mail(subject, to, user_data):
- text = (f'Пользователь {user_data["first_name"]} {user_data["last_name"]} '
+ text = (f'Пользователь {user_data["first_name"]} '
f'заказал звонок по номеру {user_data["phone_number"]}')
message = MIMEMultipart()
@@ -28,7 +28,3 @@ async def send_mail(subject, to, user_data):
async with smtp_client:
await smtp_client.login(BASE_EMAIL, PASSWORD)
await smtp_client.send_message(message)
-
-
-#if __name__ == '__main__':
-# asyncio.run(send_mail('Тесты', EMAIL, 'Как оно?)
'))
diff --git a/app/core/settings.py b/app/core/settings.py
index c9dc8aa..ca35b9b 100644
--- a/app/core/settings.py
+++ b/app/core/settings.py
@@ -5,6 +5,11 @@ class Settings(BaseSettings):
database_url: str
bot_token: str
telegram_chat_ids: str
+ email: str
+ email_password: str
+ postgres_user: str
+ postgres_password: str
+ postgres_db: str
class Config:
env_file = '.env'
diff --git a/app/models/models.py b/app/models/models.py
index 1f19d64..caac594 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -45,7 +45,8 @@ class User(Base):
nullable=False
)
- feedbacks = relationship("Feedback", back_populates="author", cascade="all, delete")
+ feedbacks = relationship("Feedback", back_populates="author",
+ cascade="all, delete")
class ProductCategory(Base):
@@ -59,9 +60,8 @@ class ProductCategory(Base):
pgsql_types.TEXT
)
- categories = relationship(
- "CategoryType", cascade="all, delete", back_populates="product_category"
- )
+ categories = relationship("CategoryType", cascade="all, delete",
+ back_populates="product_category")
class CategoryType(Base):
@@ -87,7 +87,8 @@ class CategoryType(Base):
nullable=True
)
- product_category = relationship("ProductCategory", back_populates="categories")
+ product_category = relationship("ProductCategory",
+ back_populates="categories")
class InformationAboutCompany(Base):
@@ -203,4 +204,5 @@ class Feedback(Base):
default=True
)
author = relationship("User", back_populates="feedbacks")
- contact_manager = relationship("ContactManager", back_populates="feedbacks")
+ contact_manager = relationship("ContactManager",
+ back_populates="feedbacks")
diff --git a/docker-compose.production.yml b/docker-compose.yml
similarity index 96%
rename from docker-compose.production.yml
rename to docker-compose.yml
index 6feaf8d..2cacf23 100644
--- a/docker-compose.production.yml
+++ b/docker-compose.yml
@@ -28,4 +28,4 @@ services:
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- - 8000:80
+ - 80:80
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..e53066e
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,15 @@
+server {
+ listen 80;
+ client_max_body_size 10M;
+ server_tokens off;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_redirect off;
+ proxy_buffering off;
+ proxy_pass http://127.0.0.1:8000;
+ }
+}
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 73b83e9..d3d2e07 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -713,87 +713,6 @@ files = [
{file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"},
]
-[[package]]
-name = "psycopg2-binary"
-version = "2.9.9"
-description = "psycopg2 - Python-PostgreSQL Database Adapter"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"},
- {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
- {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
- {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"},
- {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"},
- {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"},
- {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
-]
-
[[package]]
name = "pydantic"
version = "2.9.2"
@@ -1158,4 +1077,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
-content-hash = "8ec858bde1d44f558a848ecdcd82c3bf80e750733c333fea5e6848ebf037d263"
+content-hash = "6894b39621ead657410e8a9e3ee706ec3f958f130d7857bc3f3ddce1fd831a8a"
diff --git a/pyproject.toml b/pyproject.toml
index 9994c5b..b402792 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,7 +14,6 @@ alembic = "^1.13.2"
sqlalchemy = "^2.0.35"
pydantic-settings = "^2.5.2"
asyncpg = "^0.29.0"
-psycopg2-binary = "^2.9.9"
aiosmtplib = "^3.0.2"
[tool.poetry.group.dev.dependencies]
From 595fcc5a1afd2985d01b0bb5c54427aec74784d0 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Thu, 10 Oct 2024 00:29:55 +0300
Subject: [PATCH 24/75] add nginx
---
docker-compose.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 2cacf23..541cd0e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,7 +16,6 @@ services:
- db
container_name: scid-bot
build: .
- #image: greenvibe/scid_bot
env_file: .env
gateway:
From f73cb404f159e72521f869f89cd3e674192b3ab6 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Thu, 10 Oct 2024 18:54:40 +0300
Subject: [PATCH 25/75] update env file
---
.env.example | 2 ++
app/core/settings.py | 2 ++
2 files changed, 4 insertions(+)
diff --git a/.env.example b/.env.example
index 25f7bd9..eb91225 100644
--- a/.env.example
+++ b/.env.example
@@ -9,6 +9,8 @@ DATABASE_URL=<'DATABASE URL'>
POSTGRES_USER=<'DB OWNER'>
POSTGRES_PASSWORD=<'PASSWORD'>
POSTGRES_DB=<'DB NAME'>
+DB_HOST=db
+DB_PORT=5432
EMAIL=<'MANAGER EMAIL'>
EMAIL_PASSWORD=<'PASSWORD'>
diff --git a/app/core/settings.py b/app/core/settings.py
index ca35b9b..b367caa 100644
--- a/app/core/settings.py
+++ b/app/core/settings.py
@@ -10,6 +10,8 @@ class Settings(BaseSettings):
postgres_user: str
postgres_password: str
postgres_db: str
+ db_host: str
+ db_port: str
class Config:
env_file = '.env'
From 78ca420c5c64eb6294965b9bca7b65046dafd66e Mon Sep 17 00:00:00 2001
From: ikhit
Date: Thu, 10 Oct 2024 19:22:06 +0300
Subject: [PATCH 26/75] start refactor
---
.env.example | 5 -
...4_change_all_model_name_fields_to_name.py} | 12 +-
alembic/versions/726f3c66f8d4_test_commit.py | 102 ------------
app/admin/admin_orm/delete_manager.py | 78 +++++++++
.../admin_handlers/admin_info_handlers.py | 11 +-
app/admin/handlers/user.py | 2 +-
app/admin/keyboards/keyboards.py | 110 +++++++------
app/const.py | 4 +-
app/crud/base_crud.py | 7 +
app/crud/info_crud.py | 2 +-
app/crud/portfolio_projects_crud.py | 2 +-
app/crud/product_crud.py | 2 +-
app/crud/projects.py | 9 +-
app/models/models.py | 6 +-
test.py | 150 ++++++++++++++++++
15 files changed, 313 insertions(+), 189 deletions(-)
rename alembic/versions/{3fe28ef6d0c4_first_commit.py => 42e8d0d2bdd4_change_all_model_name_fields_to_name.py} (94%)
delete mode 100644 alembic/versions/726f3c66f8d4_test_commit.py
create mode 100644 app/admin/admin_orm/delete_manager.py
create mode 100644 test.py
diff --git a/.env.example b/.env.example
index 25f7bd9..7887658 100644
--- a/.env.example
+++ b/.env.example
@@ -1,11 +1,6 @@
-DATABASE_URL=<'DATABASE URL'>
-TELEGRAM_TOKEN=<'TELEGRAM TOKEN'>
-TELEGRAM_CHAT_IDS=<'TELEGRAM CHAT IDS'>
-
BOT_TOKEN=<'BOT TOKEN'>
TELEGRAM_CHAT_IDS=<'TELEGRAM CHAT IDS'>
-DATABASE_URL=<'DATABASE URL'>
POSTGRES_USER=<'DB OWNER'>
POSTGRES_PASSWORD=<'PASSWORD'>
POSTGRES_DB=<'DB NAME'>
diff --git a/alembic/versions/3fe28ef6d0c4_first_commit.py b/alembic/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py
similarity index 94%
rename from alembic/versions/3fe28ef6d0c4_first_commit.py
rename to alembic/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py
index df8c8e1..2918ed3 100644
--- a/alembic/versions/3fe28ef6d0c4_first_commit.py
+++ b/alembic/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py
@@ -1,8 +1,8 @@
-"""first commit
+"""change all model name fields to name
-Revision ID: 3fe28ef6d0c4
+Revision ID: 42e8d0d2bdd4
Revises:
-Create Date: 2024-10-09 10:04:32.815358
+Create Date: 2024-10-10 10:21:14.189895
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
-revision: str = '3fe28ef6d0c4'
+revision: str = '42e8d0d2bdd4'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -21,7 +21,7 @@
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('checkcompanyportfolio',
- sa.Column('project_name', sa.VARCHAR(length=48), nullable=False),
+ sa.Column('name', sa.VARCHAR(length=48), nullable=False),
sa.Column('url', sa.VARCHAR(length=128), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
@@ -51,7 +51,7 @@ def upgrade() -> None:
sa.PrimaryKeyConstraint('id')
)
op.create_table('productcategory',
- sa.Column('title', sa.VARCHAR(length=150), nullable=False),
+ sa.Column('name', sa.VARCHAR(length=150), nullable=False),
sa.Column('response', sa.TEXT(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
diff --git a/alembic/versions/726f3c66f8d4_test_commit.py b/alembic/versions/726f3c66f8d4_test_commit.py
deleted file mode 100644
index f72b1ed..0000000
--- a/alembic/versions/726f3c66f8d4_test_commit.py
+++ /dev/null
@@ -1,102 +0,0 @@
-"""Test commit
-
-Revision ID: 726f3c66f8d4
-Revises: d2365dc6d2ca
-Create Date: 2024-10-08 19:27:27.701551
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = '726f3c66f8d4'
-down_revision: Union[str, None] = 'd2365dc6d2ca'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('checkcompanyportfolio',
- sa.Column('project_name', sa.VARCHAR(length=48), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('contactmanager',
- sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
- sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
- sa.Column('need_support', sa.BOOLEAN(), nullable=False),
- sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
- sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
- sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('info',
- sa.Column('question_type', postgresql.ENUM('GENERAL_QUESTIONS', 'PROBLEMS_WITH_PRODUCTS', name='question_enum'), nullable=False),
- sa.Column('question', sa.TEXT(), nullable=False),
- sa.Column('answer', sa.TEXT(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('question')
- )
- op.create_table('informationaboutcompany',
- sa.Column('name', sa.VARCHAR(length=48), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('productcategory',
- sa.Column('title', sa.VARCHAR(length=150), nullable=False),
- sa.Column('response', sa.TEXT(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('user',
- sa.Column('tg_id', sa.BIGINT(), nullable=False),
- sa.Column('role', postgresql.ENUM('USER', 'ADMIN', 'MANAGER', name='role_enum'), nullable=False),
- sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('tg_id')
- )
- op.create_table('categorytype',
- sa.Column('name', sa.VARCHAR(length=150), nullable=False),
- sa.Column('product_id', sa.Integer(), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('media', sa.VARCHAR(length=128), nullable=True),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ondelete='CASCADE'),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_index(op.f('ix_categorytype_product_id'), 'categorytype', ['product_id'], unique=False)
- op.create_table('feedback',
- sa.Column('user', sa.Integer(), nullable=False),
- sa.Column('contact_manager_id', sa.Integer(), nullable=False),
- sa.Column('feedback_text', sa.TEXT(), nullable=False),
- sa.Column('feedback_date', postgresql.TIMESTAMP(), nullable=False),
- sa.Column('unread', sa.BOOLEAN(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['contact_manager_id'], ['contactmanager.id'], ondelete='CASCADE'),
- sa.ForeignKeyConstraint(['user'], ['user.id'], ondelete='CASCADE'),
- sa.PrimaryKeyConstraint('id')
- )
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('feedback')
- op.drop_index(op.f('ix_categorytype_product_id'), table_name='categorytype')
- op.drop_table('categorytype')
- op.drop_table('user')
- op.drop_table('productcategory')
- op.drop_table('informationaboutcompany')
- op.drop_table('info')
- op.drop_table('contactmanager')
- op.drop_table('checkcompanyportfolio')
- # ### end Alembic commands ###
diff --git a/app/admin/admin_orm/delete_manager.py b/app/admin/admin_orm/delete_manager.py
new file mode 100644
index 0000000..25ccd23
--- /dev/null
+++ b/app/admin/admin_orm/delete_manager.py
@@ -0,0 +1,78 @@
+from app.admin.keyboards.keyboards import (
+ get_inline_confirmation_keyboard,
+ get_inline_keyboard,
+ InlineKeyboardManager
+)
+from app.crud.base_crud import CRUDBase
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from aiogram.types import CallbackQuery
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from models.models import Info
+
+
+class DeleteStates(StatesGroup):
+ """Класс состояний для удаления."""
+
+ select = State()
+ confirm = State()
+
+
+class DeleteManager:
+ """Менеджер для удаления объекта из БД."""
+
+ def __init__(
+ self, model_curd: CRUDBase, keyboard: InlineKeyboardManager
+ ) -> None:
+ self.model_crud = model_curd
+ self.keyboard = keyboard
+
+ async def get_all_model_names(self, session: AsyncSession) -> list[str]:
+ """Получить список названий объектов из таблицы БД."""
+ models = await self.model_crud.get_multi(session)
+ return [model.name for model in models]
+
+ async def select_obj_to_delete(
+ self, callback: CallbackQuery, state: FSMContext, session: AsyncSession
+ ) -> None:
+ obj_list_by_name = await self.get_all_model_names(session)
+ await callback.message.edit_text(
+ "Какой объект удалить?",
+ reply_markup=self.keyboard.add_buttons(
+ obj_list_by_name
+ ).create_keyboard(),
+ )
+ await state.set_state(DeleteStates.select)
+
+ async def confirm_delete(
+ self, callback: CallbackQuery, state: FSMContext, session: AsyncSession
+ ) -> None:
+ self.obj_to_delete = await self.model_crud.get_by_string(
+ callback.data, session
+ )
+ obj_data = (
+ self.obj_to_delete.question
+ if isinstance(self.obj_to_delete, Info)
+ else self.obj_to_delete.name
+ )
+ await callback.message.edit_text(
+ f"Вы уверены, что хотите удалить этот вопрос?\n\n {obj_data}",
+ reply_markup=await get_inline_confirmation_keyboard(
+ cancel_option=self.previous_menu
+ ),
+ )
+ await state.set_state(DeleteStates.confirm)
+
+ async def delete_obj(
+ self, callback: CallbackQuery, state: FSMContext, session: AsyncSession
+ ) -> None:
+ """Удалить объект из БД."""
+ await self.model_crud.remove(self.obj_to_delete, session)
+ await callback.message.edit_text(
+ "Вопрос удален!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.previous_menu
+ ),
+ )
+ await state.clear()
diff --git a/app/admin/handlers/admin_handlers/admin_info_handlers.py b/app/admin/handlers/admin_handlers/admin_info_handlers.py
index 06e2dde..373c0df 100644
--- a/app/admin/handlers/admin_handlers/admin_info_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_info_handlers.py
@@ -11,6 +11,7 @@
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
+
# from settings import SUPPORT_OPTIONS
from sqlalchemy.ext.asyncio import AsyncSession
from aiogram.fsm.state import State, StatesGroup
@@ -126,7 +127,7 @@ async def question_to_delete(
async def confirm_delete_question(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- question = await info_crud.get_by_question_text(callback.data, session)
+ question = await info_crud.get_by_string(callback.data, session)
await callback.message.edit_text(
f"Вы уверены, что хотите удалить этот вопрос?\n\n {question.question}",
reply_markup=await get_inline_confirmation_keyboard(
@@ -141,7 +142,7 @@ async def delete_question(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
await state.clear()
- question = await info_crud.get_by_question_text(callback.data, session)
+ question = await info_crud.get_by_string(callback.data, session)
await info_crud.remove(question, session)
await callback.message.edit_text(
"Вопрос удален!",
@@ -201,7 +202,7 @@ async def update_question_answer(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
question_text = (await state.get_data()).get("question")
- answer = await info_crud.get_by_question_text(question_text, session)
+ answer = await info_crud.get_by_string(question_text, session)
await callback.message.answer(
f"Сейчас ответ записан вот так:\n\n{answer.answer}\n\n Введите новый текст"
)
@@ -216,9 +217,7 @@ async def update_question_data(
):
current_state = await state.get_state()
old_data = await state.get_data()
- question = await info_crud.get_by_question_text(
- old_data.get("question"), session
- )
+ question = await info_crud.get_by_string(old_data.get("question"), session)
if current_state == UpdateQuestion.confirm:
await state.update_data(question=message.text)
diff --git a/app/admin/handlers/user.py b/app/admin/handlers/user.py
index fa5e482..c0b94b0 100644
--- a/app/admin/handlers/user.py
+++ b/app/admin/handlers/user.py
@@ -312,7 +312,7 @@ async def faq_answer(
if callback.data not in question_list:
return
- question = await info_crud.get_by_question_text(callback.data, session)
+ question = await info_crud.get_by_string(callback.data, session)
answer = f"{callback.data}\n\n{question.answer}"
await callback.message.answer(
diff --git a/app/admin/keyboards/keyboards.py b/app/admin/keyboards/keyboards.py
index 7d389b0..53e04da 100644
--- a/app/admin/keyboards/keyboards.py
+++ b/app/admin/keyboards/keyboards.py
@@ -62,60 +62,6 @@ async def get_inline_keyboard(
return keyboard.adjust(*size).as_markup(resize_keyboard=True)
-# async def get_inline_paginated_keyboard(
-# options: Optional[Union[list[str], str]] = None,
-# pagination: dict = None,
-# size: tuple[int] = (1,),
-# ) -> InlineKeyboardMarkup:
-# """Создать набор кнопок для меню раздела с поддержкой пагинации."""
-# keyboard = InlineKeyboardBuilder()
-# total_pages = 0
-# current_page = 0
-# if pagination:
-# current_page = pagination.get("current_page", 1)
-# items_per_page = pagination.get("items_per_page", 5)
-# total_items = len(options) if options else 0
-# total_pages = (total_items + items_per_page - 1) // items_per_page
-
-# start_index = (current_page - 1) * items_per_page
-# end_index = min(start_index + items_per_page, total_items)
-
-# current_options = options[start_index:end_index] if options else []
-# else:
-# current_options = options
-
-# for option in current_options:
-# keyboard.add(
-# InlineKeyboardButton(
-# text=option,
-# callback_data=option,
-# ),
-# )
-# if total_pages > 1:
-# if current_page > 1:
-# keyboard.add(
-# InlineKeyboardButton(
-# text="◀️ Предыдущая",
-# callback_data=f"{current_page - 1}",
-# )
-# )
-# if current_page < total_pages:
-# keyboard.add(
-# InlineKeyboardButton(
-# text="Следующая ▶️",
-# callback_data=f"{current_page + 1}",
-# )
-# )
-# keyboard.add(
-# InlineKeyboardButton(
-# text="Главное меню",
-# callback_data=BASE_BUTTONS.get("main_menu"),
-# )
-# )
-
-# return keyboard.adjust(*size).as_markup(resize_keyboard=True)
-
-
async def get_inline_paginated_keyboard(
options: list[str] | str | None = None,
callback: list[str] | str | None = None,
@@ -192,8 +138,7 @@ async def get_reply_keyboard(
else:
keyboard.add(
KeyboardButton(
- text=options,
- # callback_data=options,
+ text=options
)
)
@@ -211,7 +156,8 @@ async def get_delete_message_keyboard() -> InlineKeyboardMarkup:
async def get_inline_confirmation_keyboard(
- option: str, cancel_option: str
+ cancel_option: str,
+ option: str = "Да",
) -> InlineKeyboardMarkup:
"""Кнопка для подтверждения действий."""
@@ -220,3 +166,53 @@ async def get_inline_confirmation_keyboard(
keyboard.add(InlineKeyboardButton(text="Нет", callback_data=cancel_option))
return keyboard.adjust(2).as_markup(resize_keyboard=True)
+
+
+class InlineKeyboardManager:
+ def __init__(self, options=None, callback=None, urls=None, size=(1,)):
+ self.options = options if options is not None else []
+ self.callback = callback if callback is not None else self.options
+ self.urls = urls if urls is not None else []
+ self.size = size
+ self.keyboard = InlineKeyboardBuilder()
+
+ def add_buttons(self):
+ """Добавить основные кнопки в клавиатуру."""
+ for index, option in enumerate(self.options):
+ self.keyboard.add(
+ InlineKeyboardButton(
+ text=option,
+ callback_data=str(self.callback[index]),
+ url=(
+ self.urls[index]
+ if self.urls and index < len(self.urls)
+ else None
+ ),
+ )
+ )
+
+ def add_previous_menu_button(self, previous_menu):
+ """Добавить кнопку 'Назад'."""
+ self.keyboard.add(
+ InlineKeyboardButton(
+ text="Назад",
+ callback_data=previous_menu,
+ )
+ )
+
+ def add_admin_button(self, admin_update_menu):
+ """Добавить кнопку 'Редактировать' для администраторов."""
+ self.keyboard.add(
+ InlineKeyboardButton(
+ text="Редактировать🔧",
+ callback_data=f"{admin_update_menu}_",
+ )
+ )
+
+ def create_keyboard(self) -> InlineKeyboardMarkup:
+ """Создать клавиатуру и вернуть ее."""
+ self.add_buttons()
+ return self.keyboard.adjust(*self.size).as_markup(resize_keyboard=True)
+
+ def add_extra_buttons(self, optons: str | list[str], callback: str | list[str]):
+ ...
diff --git a/app/const.py b/app/const.py
index 88b3bb2..ab9aacf 100644
--- a/app/const.py
+++ b/app/const.py
@@ -31,8 +31,6 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
}
ADMIN_BASE_KEYBOARD = get_buttons(ADMIN_BASE_OPTIONS)
ADMIN_PORTFOLIO_KEYBOARD = get_buttons(ADMIN_PORTFOLIO_OPTIONS)
-USER_CALLBACK_PAGINATION = 5
-FEEDBACK_PAGINATION = 5
ADMIN_BASE_REPLY_OPTIONS = {
"main_menu": "Главное меню",
"callback_case": "Заявки на обратный звонок",
@@ -92,3 +90,5 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
DEFAULT_STR_LEN = 150
PHONE_NUMBER_LEN = 25
PHONE_NUMBER_REGEX = r"(\+\d{5,25}$|\d{5,25}$)"
+USER_CALLBACK_PAGINATION = 5
+FEEDBACK_PAGINATION = 5
diff --git a/app/crud/base_crud.py b/app/crud/base_crud.py
index 517e68e..4f3dd70 100644
--- a/app/crud/base_crud.py
+++ b/app/crud/base_crud.py
@@ -49,3 +49,10 @@ async def remove(self, db_obj, session: AsyncSession):
await session.delete(db_obj)
await session.commit()
return db_obj
+
+ async def get_by_string(self, string: str, session: AsyncSession):
+ """Получить объект модели из БД по его названию."""
+ db_obj = await session.execute(
+ select(self.model).where(self.model.name == string)
+ )
+ return db_obj.scalars().first()
diff --git a/app/crud/info_crud.py b/app/crud/info_crud.py
index d464f85..1f781bb 100644
--- a/app/crud/info_crud.py
+++ b/app/crud/info_crud.py
@@ -6,7 +6,7 @@
class InfoCRUD(CRUDBase):
- async def get_by_question_text(
+ async def get_by_string(
self,
question_text: str,
session: AsyncSession,
diff --git a/app/crud/portfolio_projects_crud.py b/app/crud/portfolio_projects_crud.py
index c475e7f..3fb9935 100644
--- a/app/crud/portfolio_projects_crud.py
+++ b/app/crud/portfolio_projects_crud.py
@@ -14,7 +14,7 @@ async def get_by_project_name(
):
"""Получить проект портфолио по тексту названия."""
portfolio_project = await session.execute(
- select(self.model).where(self.model.project_name == project_name)
+ select(self.model).where(self.model.name == project_name)
)
return portfolio_project.scalars().first()
diff --git a/app/crud/product_crud.py b/app/crud/product_crud.py
index c903e31..362ec2d 100644
--- a/app/crud/product_crud.py
+++ b/app/crud/product_crud.py
@@ -21,7 +21,7 @@ async def get_by_product_name(
):
"""Получить объект модели по тексту названия."""
product = await session.execute(
- select(self.model).where(self.model.title == product_name)
+ select(self.model).where(self.model.name == product_name)
)
return product.scalars().first()
diff --git a/app/crud/projects.py b/app/crud/projects.py
index 4b7e8f6..48e30c8 100644
--- a/app/crud/projects.py
+++ b/app/crud/projects.py
@@ -1,3 +1,4 @@
+from typing import Union
from sqlalchemy.ext.asyncio import AsyncSession
from models.models import (
CheckCompanyPortfolio, ProductCategory,
@@ -7,9 +8,9 @@
async def get_all_prtfolio_projects(
- object_model: CheckCompanyPortfolio | ProductCategory,
+ object_model: Union[CheckCompanyPortfolio, ProductCategory],
session: AsyncSession
-) -> list[CheckCompanyPortfolio | ProductCategory]:
+) -> list[Union[CheckCompanyPortfolio, ProductCategory]]:
"""Получение всех проектов-портфолио или продуктов и услуг."""
result = await session.execute(select(object_model))
@@ -37,7 +38,7 @@ async def get_categories_by_name(
.join(
ProductCategory, ProductCategory.id == CategoryType.product_id
)
- .where(ProductCategory.title == product_name)
+ .where(ProductCategory.name == product_name)
)
return result.scalars().all()
@@ -47,7 +48,7 @@ async def get_title_by_id(category_id: int, session: AsyncSession) -> str:
"""Получает название категории по ID из базы данных."""
result = await session.execute(
- select(ProductCategory.title).where(
+ select(ProductCategory.name).where(
ProductCategory.id == category_id
)
)
diff --git a/app/models/models.py b/app/models/models.py
index b8d57fa..87ad912 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -7,7 +7,7 @@
from enum import Enum
-from core.db import Base
+from app.core.base import Base
class RoleEnum(str, Enum):
@@ -47,7 +47,7 @@ class User(Base):
class ProductCategory(Base):
"""БД модель продуктов и услуг."""
- title: Mapped[str] = mapped_column(pgsql_types.VARCHAR(150))
+ name: Mapped[str] = mapped_column(pgsql_types.VARCHAR(150))
response: Mapped[str] = mapped_column(pgsql_types.TEXT)
@@ -89,7 +89,7 @@ class InformationAboutCompany(Base):
class CheckCompanyPortfolio(Base):
"""Бд модель информации о проектах."""
- project_name: Mapped[str] = mapped_column(
+ name: Mapped[str] = mapped_column(
pgsql_types.VARCHAR(48), nullable=False
)
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..8aaab2d
--- /dev/null
+++ b/test.py
@@ -0,0 +1,150 @@
+class QuestionManager:
+ def __init__(self, session: AsyncSession):
+ self.session = session
+
+ async def ask_question_to_delete(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ current_state = await state.get_state()
+ await state.set_state(DeleteQuestion.question_type)
+ await state.update_data(
+ question_type=await set_question_type(current_state)
+ )
+ question_type = (await state.get_data()).get("question_type")
+ question_list = await get_question_list(question_type, self.session)
+ await callback.message.edit_text(
+ "Какой вопрос удалить?",
+ reply_markup=await get_inline_keyboard(
+ question_list, previous_menu=PREVIOUS_MENU
+ ),
+ )
+ await state.set_state(DeleteQuestion.question)
+
+ async def confirm_delete_question(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ question = await info_crud.get_by_question_text(
+ callback.data, self.session
+ )
+ await callback.message.edit_text(
+ f"Вы уверены, что хотите удалить этот вопрос?\n\n {question.question}",
+ reply_markup=await get_inline_confirmation_keyboard(
+ option=question.question, cancel_option=PREVIOUS_MENU
+ ),
+ )
+ await state.set_state(DeleteQuestion.confirm)
+
+ async def delete_question(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ await state.clear()
+ question = await info_crud.get_by_question_text(
+ callback.data, self.session
+ )
+ await info_crud.remove(question, self.session)
+ await callback.message.edit_text(
+ "Вопрос удален!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=question.question_type
+ ),
+ )
+
+
+# Использование в роутере
+@info_router.callback_query(
+ or_f(SectionState.faq, SectionState.troubleshooting), F.data == "Удалить"
+)
+async def handle_delete_question(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ manager = QuestionManager(session)
+ await manager.ask_question_to_delete(callback, state)
+
+
+@info_router.callback_query(DeleteQuestion.question, F.data)
+async def handle_confirm_delete_question(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ manager = QuestionManager(session)
+ await manager.confirm_delete_question(callback, state)
+
+
+@info_router.callback_query(DeleteQuestion.confirm, F.data != PREVIOUS_MENU)
+async def handle_delete_question(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ manager = QuestionManager(session)
+ await manager.delete_question(callback, state)
+
+
+from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+
+class InlineKeyboardManager:
+ def __init__(self, options=None, callback=None, urls=None, size=(1,)):
+ self.options = options if options is not None else []
+ self.callback = callback if callback is not None else self.options
+ self.urls = urls if urls is not None else []
+ self.size = size
+ self.keyboard = InlineKeyboardBuilder()
+
+ def add_buttons(self):
+ """Добавить основные кнопки в клавиатуру."""
+ for index, option in enumerate(self.options):
+ self.keyboard.add(
+ InlineKeyboardButton(
+ text=option,
+ callback_data=str(self.callback[index]),
+ url=(
+ self.urls[index]
+ if self.urls and index < len(self.urls)
+ else None
+ ),
+ )
+ )
+
+ def add_previous_menu_button(self, previous_menu):
+ """Добавить кнопку 'Назад'."""
+ self.keyboard.add(
+ InlineKeyboardButton(
+ text="Назад",
+ callback_data=previous_menu,
+ )
+ )
+
+ def add_admin_button(self, admin_update_menu):
+ """Добавить кнопку 'Редактировать' для администраторов."""
+ self.keyboard.add(
+ InlineKeyboardButton(
+ text="Редактировать🔧",
+ callback_data=f"{admin_update_menu}_",
+ )
+ )
+
+ def create_keyboard(self) -> InlineKeyboardMarkup:
+ """Создать клавиатуру и вернуть ее."""
+ self.add_buttons()
+ return self.keyboard.adjust(*self.size).as_markup(resize_keyboard=True)
+
+
+# Пример использования
+async def get_inline_keyboard(options, callback=None, urls=None, size=(1,)):
+ manager = InlineKeyboardManager(options, callback, urls, size)
+ return manager.create_keyboard()
+
+
+# Использование с добавлением дополнительных кнопок
+async def get_custom_keyboard(
+ options, previous_menu=None, is_admin=False, admin_update_menu=None
+):
+ manager = InlineKeyboardManager(options)
+ manager.add_buttons()
+
+ if previous_menu:
+ manager.add_previous_menu_button(previous_menu)
+
+ if is_admin and admin_update_menu:
+ manager.add_admin_button(admin_update_menu)
+
+ return manager.create_keyboard()
From 06be0d9613ecc85b0ba63795e7e97287cdf350ff Mon Sep 17 00:00:00 2001
From: ikhit
Date: Thu, 10 Oct 2024 23:37:05 +0300
Subject: [PATCH 27/75] switch branch
---
app/admin/admin_orm/__init__.py | 0
app/admin/admin_orm/delete_manager.py | 2 +-
2 files changed, 1 insertion(+), 1 deletion(-)
create mode 100644 app/admin/admin_orm/__init__.py
diff --git a/app/admin/admin_orm/__init__.py b/app/admin/admin_orm/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/admin/admin_orm/delete_manager.py b/app/admin/admin_orm/delete_manager.py
index 25ccd23..538fb65 100644
--- a/app/admin/admin_orm/delete_manager.py
+++ b/app/admin/admin_orm/delete_manager.py
@@ -1,7 +1,7 @@
from app.admin.keyboards.keyboards import (
get_inline_confirmation_keyboard,
get_inline_keyboard,
- InlineKeyboardManager
+ InlineKeyboardManager,
)
from app.crud.base_crud import CRUDBase
from aiogram.fsm.context import FSMContext
From 67a7775bc7daaa6e30b5b85207554525bac93f85 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Fri, 11 Oct 2024 09:38:01 +0300
Subject: [PATCH 28/75] add createmanager for admin
---
app/admin/admin_orm/create_manager.py | 175 ++++++++++++++++++++++++++
app/admin/admin_orm/delete_manager.py | 62 ++++++---
app/admin/admin_orm/update_manager.py | 0
app/admin/keyboards/keyboards.py | 70 +++++++----
app/const.py | 6 +
5 files changed, 276 insertions(+), 37 deletions(-)
create mode 100644 app/admin/admin_orm/create_manager.py
create mode 100644 app/admin/admin_orm/update_manager.py
diff --git a/app/admin/admin_orm/create_manager.py b/app/admin/admin_orm/create_manager.py
new file mode 100644
index 0000000..ea2c765
--- /dev/null
+++ b/app/admin/admin_orm/create_manager.py
@@ -0,0 +1,175 @@
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from aiogram.types import CallbackQuery, Message
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.admin.keyboards.keyboards import InlineKeyboardManager
+from app.const import ADMIN_CONTENT_BUTTONS
+from app.crud.base_crud import CRUDBase
+
+
+class CreateState(StatesGroup):
+ """Класс состояний для создания объекта в БД."""
+
+ select = State()
+ name = State()
+ url = State()
+ text = State()
+ media = State()
+
+
+class CreateManager:
+ """
+ Менеджер для управления процессом создания объектов в базе данных.
+
+ Этот класс обеспечивает обработку различных этапов ввода данных от пользователя
+ для создания новых объектов. Он взаимодействует с базой данных через CRUD-операции
+ и управляет состоянием пользователя в процессе ввода.
+
+ Attributes:
+ model_crud (CRUDBase): Объект для выполнения операций CRUD с моделью.
+ keyboard (InlineKeyboardManager): Менеджер для создания интерактивных клавиатур.
+
+ Methods:
+ select_data_type(callback: CallbackQuery, state: FSMContext):
+ Запрашивает у пользователя тип данных для создания.
+
+ prompt_for_input(message: Message, prompt: str, next_state: State):
+ Отправляет сообщение с просьбой ввести данные и устанавливает следующее состояние.
+
+ add_obj_name(callback: CallbackQuery, state: FSMContext):
+ Запрашивает название объекта у пользователя.
+
+ add_obj_url(message: Message, state: FSMContext):
+ Запрашивает URL объекта у пользователя.
+
+ add_obj_text(message: Message, state: FSMContext):
+ Запрашивает текст объекта у пользователя.
+
+ add_obj_media(message: Message, state: FSMContext):
+ Запрашивает медиафайл и текст к нему у пользователя.
+
+ add_obj_to_db(message: Message, state: FSMContext, session: AsyncSession):
+ Добавляет объект в базу данных и сбрасывает машинное состояние.
+ """
+
+ def __init__(
+ self, model_crud: CRUDBase, keyboard: InlineKeyboardManager
+ ) -> None:
+ self.model_crud = model_crud
+ self.keyboard = keyboard
+
+ async def select_data_type(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ """Выбрать тип данных для модели в БД."""
+ await callback.message.edit_text(
+ "Выбирите способ передачи информации:",
+ reply_markup=await self.keyboard.add_extra_buttons(
+ ADMIN_CONTENT_BUTTONS
+ ),
+ )
+ await state.set_state(CreateState.select)
+
+ async def add_obj_name(
+ self,
+ callback: CallbackQuery,
+ state: FSMContext,
+ ):
+ """
+ Добавить название объекта и перейти в
+ следующее машинное состояние.
+ """
+ await callback.message.answer(
+ "Введите название:", reply_markup=self.keyboard
+ )
+ await state.set_state(CreateState.name)
+
+ async def prompt_for_input(
+ self,
+ message: Message,
+ message_text: str,
+ state: FSMContext,
+ next_state: State,
+ ):
+ """
+ Добавить название объекта в state_data и перейти
+ к заполнению следующего поля.
+ """
+ await state.update_data(name=message.text)
+ await message.answer(
+ message_text,
+ reply_markup=self.keyboard,
+ )
+ await state.set_state(next_state)
+
+ async def add_obj_url(self, message: Message, state: FSMContext):
+ """
+ Добавить ссылку к объекту и перейти в
+ следующее машинное состояние.
+ """
+ message_text = (
+ "Ссылка обязательно должна начинаться с 'https://'\n\n "
+ "Введите адрес ссылки:"
+ )
+ await self.prompt_for_input(
+ message,
+ message_text,
+ state,
+ next_state=CreateState.url,
+ )
+
+ async def add_obj_text(self, message: Message, state: FSMContext):
+ """
+ Добавить текст к объекту и перейти в
+ следующее машинное состояние.
+ """
+ message_text = "Введите текст:"
+ await self.prompt_for_input(
+ message,
+ message_text,
+ state,
+ next_state=CreateState.text,
+ )
+
+ async def add_obj_media(self, message: Message, state: FSMContext):
+ """
+ Добавить картинку к объекту и перейти в
+ следующее машинное состояние.
+ """
+ message_text = "Добавьте картинку и текст к ней:"
+ await self.prompt_for_input(
+ message,
+ message_text,
+ state,
+ next_state=CreateState.media,
+ )
+
+ async def add_obj_to_db(
+ self, message: Message, state: FSMContext, session: AsyncSession
+ ):
+ """Добавить объект в БД и сбросить машинное состояние."""
+ try:
+ current_state = await state.get_state()
+ if current_state == CreateState.url.state:
+ await state.update_data(url=message.text)
+ elif current_state == CreateState.text.state:
+ await state.update_data(text=message.text)
+ elif current_state == CreateState.media.state:
+ await state.update_data(
+ media=message.photo[-1].file_id,
+ text=message.caption,
+ )
+
+ data = await state.get_data()
+ await self.model_crud.create(data, session)
+
+ await message.answer(
+ "Данные добавлены!",
+ reply_markup=await InlineKeyboardManager.get_back_button(
+ self.keyboard.previous_menu
+ ),
+ )
+ await state.clear()
+ except Exception as e:
+ await message.answer(f"Произошла ошибка: {e}")
diff --git a/app/admin/admin_orm/delete_manager.py b/app/admin/admin_orm/delete_manager.py
index 538fb65..7062d30 100644
--- a/app/admin/admin_orm/delete_manager.py
+++ b/app/admin/admin_orm/delete_manager.py
@@ -1,5 +1,4 @@
from app.admin.keyboards.keyboards import (
- get_inline_confirmation_keyboard,
get_inline_keyboard,
InlineKeyboardManager,
)
@@ -12,7 +11,7 @@
from models.models import Info
-class DeleteStates(StatesGroup):
+class DeleteState(StatesGroup):
"""Класс состояний для удаления."""
select = State()
@@ -20,12 +19,34 @@ class DeleteStates(StatesGroup):
class DeleteManager:
- """Менеджер для удаления объекта из БД."""
+ """
+ Менеджер для удаления объектов из базы данных.
+
+ Этот класс предоставляет методы для удаления объектов из
+ базы данных, используя заданную модель CRUD.
+
+ Attributes:
+ model_crud (CRUDBase): Модель, предоставляющая методы для
+ работы с объектами в БД.
+
+ keyboard (InlineKeyboardManager): Менеджер клавиатуры для
+ взаимодействия с пользователем.
+
+ Methods:
+ delete_object(object_id: int) -> bool:
+ Удаляет объект с заданным идентификатором из базы данных.
+ Возвращает True, если удаление прошло успешно, иначе False.
+
+ confirm_deletion(object_id: int) -> None:
+ Запрашивает подтверждение у пользователя перед удалением объекта.
+ """
def __init__(
- self, model_curd: CRUDBase, keyboard: InlineKeyboardManager
+ self,
+ model_crud: CRUDBase,
+ keyboard: InlineKeyboardManager,
) -> None:
- self.model_crud = model_curd
+ self.model_crud = model_crud
self.keyboard = keyboard
async def get_all_model_names(self, session: AsyncSession) -> list[str]:
@@ -34,19 +55,25 @@ async def get_all_model_names(self, session: AsyncSession) -> list[str]:
return [model.name for model in models]
async def select_obj_to_delete(
- self, callback: CallbackQuery, state: FSMContext, session: AsyncSession
+ self,
+ callback: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
) -> None:
obj_list_by_name = await self.get_all_model_names(session)
await callback.message.edit_text(
"Какой объект удалить?",
- reply_markup=self.keyboard.add_buttons(
+ reply_markup=await self.keyboard.add_extra_buttons(
obj_list_by_name
- ).create_keyboard(),
+ ),
)
- await state.set_state(DeleteStates.select)
+ await state.set_state(DeleteState.select)
async def confirm_delete(
- self, callback: CallbackQuery, state: FSMContext, session: AsyncSession
+ self,
+ callback: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
) -> None:
self.obj_to_delete = await self.model_crud.get_by_string(
callback.data, session
@@ -58,19 +85,22 @@ async def confirm_delete(
)
await callback.message.edit_text(
f"Вы уверены, что хотите удалить этот вопрос?\n\n {obj_data}",
- reply_markup=await get_inline_confirmation_keyboard(
- cancel_option=self.previous_menu
+ reply_markup=await InlineKeyboardManager.get_inline_confirmation(
+ cancel_option=self.keyboard.previous_menu
),
- )
- await state.set_state(DeleteStates.confirm)
+ ),
+ await state.set_state(DeleteState.confirm)
async def delete_obj(
- self, callback: CallbackQuery, state: FSMContext, session: AsyncSession
+ self,
+ callback: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
) -> None:
"""Удалить объект из БД."""
await self.model_crud.remove(self.obj_to_delete, session)
await callback.message.edit_text(
- "Вопрос удален!",
+ "Данные удалены!",
reply_markup=await get_inline_keyboard(
previous_menu=self.previous_menu
),
diff --git a/app/admin/admin_orm/update_manager.py b/app/admin/admin_orm/update_manager.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/admin/keyboards/keyboards.py b/app/admin/keyboards/keyboards.py
index 53e04da..dfc5ded 100644
--- a/app/admin/keyboards/keyboards.py
+++ b/app/admin/keyboards/keyboards.py
@@ -136,11 +136,7 @@ async def get_reply_keyboard(
for option in options:
keyboard.add(KeyboardButton(text=option, callback_data=option))
else:
- keyboard.add(
- KeyboardButton(
- text=options
- )
- )
+ keyboard.add(KeyboardButton(text=options))
return keyboard.adjust(*size).as_markup()
@@ -155,17 +151,17 @@ async def get_delete_message_keyboard() -> InlineKeyboardMarkup:
return keyboard.adjust(1).as_markup(resize_keyboard=True)
-async def get_inline_confirmation_keyboard(
- cancel_option: str,
- option: str = "Да",
-) -> InlineKeyboardMarkup:
- """Кнопка для подтверждения действий."""
+# async def get_inline_confirmation_keyboard(
+# cancel_option: str,
+# option: str = "Да",
+# ) -> InlineKeyboardMarkup:
+# """Кнопка для подтверждения действий."""
- keyboard = InlineKeyboardBuilder()
- keyboard.add(InlineKeyboardButton(text="Да", callback_data=option))
- keyboard.add(InlineKeyboardButton(text="Нет", callback_data=cancel_option))
+# keyboard = InlineKeyboardBuilder()
+# keyboard.add(InlineKeyboardButton(text="Да", callback_data=option))
+# keyboard.add(InlineKeyboardButton(text="Нет", callback_data=cancel_option))
- return keyboard.adjust(2).as_markup(resize_keyboard=True)
+# return keyboard.adjust(2).as_markup(resize_keyboard=True)
class InlineKeyboardManager:
@@ -176,7 +172,7 @@ def __init__(self, options=None, callback=None, urls=None, size=(1,)):
self.size = size
self.keyboard = InlineKeyboardBuilder()
- def add_buttons(self):
+ async def add_buttons(self):
"""Добавить основные кнопки в клавиатуру."""
for index, option in enumerate(self.options):
self.keyboard.add(
@@ -191,8 +187,9 @@ def add_buttons(self):
)
)
- def add_previous_menu_button(self, previous_menu):
+ async def add_previous_menu_button(self, previous_menu):
"""Добавить кнопку 'Назад'."""
+ self.previous_menu = previous_menu
self.keyboard.add(
InlineKeyboardButton(
text="Назад",
@@ -200,7 +197,7 @@ def add_previous_menu_button(self, previous_menu):
)
)
- def add_admin_button(self, admin_update_menu):
+ async def add_admin_button(self, admin_update_menu):
"""Добавить кнопку 'Редактировать' для администраторов."""
self.keyboard.add(
InlineKeyboardButton(
@@ -209,10 +206,41 @@ def add_admin_button(self, admin_update_menu):
)
)
- def create_keyboard(self) -> InlineKeyboardMarkup:
+ async def create_keyboard(self) -> InlineKeyboardMarkup:
"""Создать клавиатуру и вернуть ее."""
self.add_buttons()
return self.keyboard.adjust(*self.size).as_markup(resize_keyboard=True)
-
- def add_extra_buttons(self, optons: str | list[str], callback: str | list[str]):
- ...
+
+ async def add_extra_buttons(
+ self, options: str | list[str], callback: str | list[str]
+ ):
+ for index, option in enumerate(options):
+ self.keyboard.add(
+ InlineKeyboardButton(
+ text=option,
+ callback_data=str(callback[index]),
+ )
+ )
+
+ @staticmethod
+ async def get_inline_confirmation(
+ cancel_option: str,
+ option: str = "Да",
+ ) -> InlineKeyboardMarkup:
+ """Кнопка для подтверждения действий."""
+
+ keyboard = InlineKeyboardBuilder()
+ keyboard.add(InlineKeyboardButton(text="Да", callback_data=option))
+ keyboard.add(
+ InlineKeyboardButton(text="Нет", callback_data=cancel_option)
+ )
+
+ return keyboard.adjust(2).as_markup(resize_keyboard=True)
+
+ @staticmethod
+ async def get_back_button(previous_menu: str) -> InlineKeyboardMarkup:
+ keyboard = InlineKeyboardBuilder()
+ keyboard.add(
+ InlineKeyboardButton(text="Назад", callback_data=previous_menu)
+ )
+ return keyboard.adjust(1).as_markup(resize_keyboard=True)
diff --git a/app/const.py b/app/const.py
index ab9aacf..a486685 100644
--- a/app/const.py
+++ b/app/const.py
@@ -37,6 +37,12 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
"feedback": "Посмотреть отзывы",
}
ADMIN_BASE_BUTTONS = get_buttons(ADMIN_BASE_REPLY_OPTIONS)
+ADMIN_CONTENT_OPTIONS = {
+ "url": "Ссылка",
+ "text": "Текст",
+ "media": "Картинка",
+}
+ADMIN_CONTENT_BUTTONS = get_buttons(ADMIN_CONTENT_OPTIONS)
# Кнопки экранной клавиатуры
BASE_BUTTONS = {
"main_menu": "Главное меню",
From 480a4243e6d491c17c89837ac2f7e82dfadcd8c7 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Fri, 11 Oct 2024 10:18:52 +0300
Subject: [PATCH 29/75] add base manager and base update/create states, start
update manager
---
app/admin/admin_orm/create_manager.py | 23 ++++-----
app/admin/admin_orm/delete_manager.py | 67 +++++++++++++--------------
app/admin/admin_orm/manager_base.py | 39 ++++++++++++++++
app/admin/admin_orm/update_manager.py | 37 +++++++++++++++
4 files changed, 115 insertions(+), 51 deletions(-)
create mode 100644 app/admin/admin_orm/manager_base.py
diff --git a/app/admin/admin_orm/create_manager.py b/app/admin/admin_orm/create_manager.py
index ea2c765..6bbfc08 100644
--- a/app/admin/admin_orm/create_manager.py
+++ b/app/admin/admin_orm/create_manager.py
@@ -1,24 +1,23 @@
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import State, StatesGroup
+from aiogram.fsm.state import State
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
+from app.admin.admin_orm.manager_base import (
+ BaseAdminManager,
+ CreateUpdateState,
+)
from app.admin.keyboards.keyboards import InlineKeyboardManager
from app.const import ADMIN_CONTENT_BUTTONS
-from app.crud.base_crud import CRUDBase
-class CreateState(StatesGroup):
+class CreateState(CreateUpdateState):
"""Класс состояний для создания объекта в БД."""
- select = State()
- name = State()
- url = State()
- text = State()
- media = State()
+ pass
-class CreateManager:
+class CreateManager(BaseAdminManager):
"""
Менеджер для управления процессом создания объектов в базе данных.
@@ -53,12 +52,6 @@ class CreateManager:
Добавляет объект в базу данных и сбрасывает машинное состояние.
"""
- def __init__(
- self, model_crud: CRUDBase, keyboard: InlineKeyboardManager
- ) -> None:
- self.model_crud = model_crud
- self.keyboard = keyboard
-
async def select_data_type(
self, callback: CallbackQuery, state: FSMContext
):
diff --git a/app/admin/admin_orm/delete_manager.py b/app/admin/admin_orm/delete_manager.py
index 7062d30..5719056 100644
--- a/app/admin/admin_orm/delete_manager.py
+++ b/app/admin/admin_orm/delete_manager.py
@@ -1,13 +1,13 @@
-from app.admin.keyboards.keyboards import (
- get_inline_keyboard,
- InlineKeyboardManager,
-)
-from app.crud.base_crud import CRUDBase
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession
+from app.admin.admin_orm.manager_base import BaseAdminManager
+from app.admin.keyboards.keyboards import (
+ get_inline_keyboard,
+ InlineKeyboardManager,
+)
from models.models import Info
@@ -18,36 +18,28 @@ class DeleteState(StatesGroup):
confirm = State()
-class DeleteManager:
+class DeleteManager(BaseAdminManager):
"""
Менеджер для удаления объектов из базы данных.
- Этот класс предоставляет методы для удаления объектов из
- базы данных, используя заданную модель CRUD.
+ Этот класс предоставляет функциональность для получения списка объектов,
+ выбора объекта для удаления, подтверждения удаления и выполнения операции удаления.
+ Он взаимодействует с базой данных через CRUD-операции и управляет состоянием
+ пользователя в процессе удаления.
- Attributes:
- model_crud (CRUDBase): Модель, предоставляющая методы для
- работы с объектами в БД.
+ Methods:
+ get_all_model_names(session: AsyncSession) -> list[str]:
+ Получает список названий объектов из таблицы БД.
- keyboard (InlineKeyboardManager): Менеджер клавиатуры для
- взаимодействия с пользователем.
+ select_obj_to_delete(callback: CallbackQuery, state: FSMContext, session: AsyncSession) -> None:
+ Запрашивает у пользователя, какой объект он хочет удалить, и отображает список объектов.
- Methods:
- delete_object(object_id: int) -> bool:
- Удаляет объект с заданным идентификатором из базы данных.
- Возвращает True, если удаление прошло успешно, иначе False.
-
- confirm_deletion(object_id: int) -> None:
- Запрашивает подтверждение у пользователя перед удалением объекта.
- """
+ confirm_delete(callback: CallbackQuery, state: FSMContext, session: AsyncSession) -> None:
+ Подтверждает выбор объекта для удаления и запрашивает подтверждение от пользователя.
- def __init__(
- self,
- model_crud: CRUDBase,
- keyboard: InlineKeyboardManager,
- ) -> None:
- self.model_crud = model_crud
- self.keyboard = keyboard
+ delete_obj(callback: CallbackQuery, state: FSMContext, session: AsyncSession) -> None:
+ Удаляет выбранный объект из базы данных и сбрасывает состояние.
+ """
async def get_all_model_names(self, session: AsyncSession) -> list[str]:
"""Получить список названий объектов из таблицы БД."""
@@ -98,11 +90,14 @@ async def delete_obj(
session: AsyncSession,
) -> None:
"""Удалить объект из БД."""
- await self.model_crud.remove(self.obj_to_delete, session)
- await callback.message.edit_text(
- "Данные удалены!",
- reply_markup=await get_inline_keyboard(
- previous_menu=self.previous_menu
- ),
- )
- await state.clear()
+ try:
+ await self.model_crud.remove(self.obj_to_delete, session)
+ await callback.message.edit_text(
+ "Данные удалены!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.previous_menu
+ ),
+ )
+ await state.clear()
+ except Exception as e:
+ await callback.message.answer(f"Произошла ошибка: {e}")
diff --git a/app/admin/admin_orm/manager_base.py b/app/admin/admin_orm/manager_base.py
new file mode 100644
index 0000000..2057bb3
--- /dev/null
+++ b/app/admin/admin_orm/manager_base.py
@@ -0,0 +1,39 @@
+from abc import ABC
+
+from aiogram.fsm.state import State, StatesGroup
+
+from app.admin.keyboards.keyboards import InlineKeyboardManager
+from app.crud.base_crud import CRUDBase
+
+
+class CreateUpdateState(StatesGroup):
+ """
+ Базовый класс для машинных состояний при
+ добавлении или обновлении данных в БД.
+ """
+
+ select = State()
+ name = State()
+ url = State()
+ text = State()
+ media = State()
+
+
+class BaseAdminManager(ABC):
+ """
+ Базовый абстрактный класс для менеджеров администратора.
+
+ Этот класс определяет общие атрибуты и методы для менеджеров,
+ которые будут использоваться для взаимодействия с CRUD-операциями
+ и клавиатурами администратора.
+
+ Attributes:
+ model_crud (CRUDBase): Объект для выполнения операций CRUD с моделью.
+ keyboard (InlineKeyboardManager): Менеджер для создания интерактивных клавиатур.
+ """
+
+ def __init__(
+ self, model_crud: CRUDBase, keyboard: InlineKeyboardManager
+ ) -> None:
+ self.model_crud = model_crud
+ self.keyboard = keyboard
diff --git a/app/admin/admin_orm/update_manager.py b/app/admin/admin_orm/update_manager.py
index e69de29..5b90714 100644
--- a/app/admin/admin_orm/update_manager.py
+++ b/app/admin/admin_orm/update_manager.py
@@ -0,0 +1,37 @@
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from aiogram.types import CallbackQuery, Message
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.admin.admin_orm.manager_base import BaseAdminManager, CreateUpdateState
+from app.admin.keyboards.keyboards import InlineKeyboardManager
+from app.const import ADMIN_CONTENT_BUTTONS
+from app.crud.base_crud import CRUDBase
+
+
+class UpdateState(CreateUpdateState):
+ """Класс состояний для редактирования данных в БД."""
+
+ pass
+
+
+class UpdateManager(BaseAdminManager):
+
+ async def get_all_model_names(self, session: AsyncSession) -> list[str]:
+ """Получить список названий объектов из таблицы БД."""
+ models = await self.model_crud.get_multi(session)
+ return [model.name for model in models]
+
+ async def select_data_to_update(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ """Выбрать тип данных для модели в БД."""
+ await callback.message.edit_text(
+ "Выбирите данные для обновления:",
+ reply_markup=await self.keyboard.add_extra_buttons(
+ ["Название", "Содержание"]
+ ),
+ )
+ await state.set_state(UpdateState.select)
+
+
From f56dd75aae5e4030957fd9083caa297d0bbc470c Mon Sep 17 00:00:00 2001
From: ikhit
Date: Fri, 11 Oct 2024 15:51:40 +0300
Subject: [PATCH 30/75] add admin update manager
---
.../{manager_base.py => base_manager.py} | 0
app/admin/admin_orm/create_manager.py | 6 +-
app/admin/admin_orm/delete_manager.py | 17 +--
app/admin/admin_orm/update_manager.py | 144 ++++++++++++++++--
app/admin/keyboards/keyboards.py | 24 ++-
app/const.py | 5 +
6 files changed, 165 insertions(+), 31 deletions(-)
rename app/admin/admin_orm/{manager_base.py => base_manager.py} (100%)
diff --git a/app/admin/admin_orm/manager_base.py b/app/admin/admin_orm/base_manager.py
similarity index 100%
rename from app/admin/admin_orm/manager_base.py
rename to app/admin/admin_orm/base_manager.py
diff --git a/app/admin/admin_orm/create_manager.py b/app/admin/admin_orm/create_manager.py
index 6bbfc08..028bdd3 100644
--- a/app/admin/admin_orm/create_manager.py
+++ b/app/admin/admin_orm/create_manager.py
@@ -3,7 +3,7 @@
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
-from app.admin.admin_orm.manager_base import (
+from app.admin.admin_orm.base_manager import (
BaseAdminManager,
CreateUpdateState,
)
@@ -58,7 +58,7 @@ async def select_data_type(
"""Выбрать тип данных для модели в БД."""
await callback.message.edit_text(
"Выбирите способ передачи информации:",
- reply_markup=await self.keyboard.add_extra_buttons(
+ reply_markup=self.keyboard.add_extra_buttons(
ADMIN_CONTENT_BUTTONS
),
)
@@ -159,7 +159,7 @@ async def add_obj_to_db(
await message.answer(
"Данные добавлены!",
- reply_markup=await InlineKeyboardManager.get_back_button(
+ reply_markup=InlineKeyboardManager.get_back_button(
self.keyboard.previous_menu
),
)
diff --git a/app/admin/admin_orm/delete_manager.py b/app/admin/admin_orm/delete_manager.py
index 5719056..98142f8 100644
--- a/app/admin/admin_orm/delete_manager.py
+++ b/app/admin/admin_orm/delete_manager.py
@@ -3,11 +3,8 @@
from aiogram.types import CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession
-from app.admin.admin_orm.manager_base import BaseAdminManager
-from app.admin.keyboards.keyboards import (
- get_inline_keyboard,
- InlineKeyboardManager,
-)
+from app.admin.admin_orm.base_manager import BaseAdminManager
+from app.admin.keyboards.keyboards import InlineKeyboardManager
from models.models import Info
@@ -55,9 +52,7 @@ async def select_obj_to_delete(
obj_list_by_name = await self.get_all_model_names(session)
await callback.message.edit_text(
"Какой объект удалить?",
- reply_markup=await self.keyboard.add_extra_buttons(
- obj_list_by_name
- ),
+ reply_markup=self.keyboard.add_extra_buttons(obj_list_by_name),
)
await state.set_state(DeleteState.select)
@@ -77,7 +72,7 @@ async def confirm_delete(
)
await callback.message.edit_text(
f"Вы уверены, что хотите удалить этот вопрос?\n\n {obj_data}",
- reply_markup=await InlineKeyboardManager.get_inline_confirmation(
+ reply_markup=InlineKeyboardManager.get_inline_confirmation(
cancel_option=self.keyboard.previous_menu
),
),
@@ -94,8 +89,8 @@ async def delete_obj(
await self.model_crud.remove(self.obj_to_delete, session)
await callback.message.edit_text(
"Данные удалены!",
- reply_markup=await get_inline_keyboard(
- previous_menu=self.previous_menu
+ reply_markup=InlineKeyboardManager.get_back_button(
+ self.keyboard.previous_menu
),
)
await state.clear()
diff --git a/app/admin/admin_orm/update_manager.py b/app/admin/admin_orm/update_manager.py
index 5b90714..7e0a5a7 100644
--- a/app/admin/admin_orm/update_manager.py
+++ b/app/admin/admin_orm/update_manager.py
@@ -1,12 +1,13 @@
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
-from app.admin.admin_orm.manager_base import BaseAdminManager, CreateUpdateState
+from app.admin.admin_orm.base_manager import (
+ BaseAdminManager,
+ CreateUpdateState,
+)
from app.admin.keyboards.keyboards import InlineKeyboardManager
-from app.const import ADMIN_CONTENT_BUTTONS
-from app.crud.base_crud import CRUDBase
+from app.const import ADMIN_UPDATE_BUTTONS
class UpdateState(CreateUpdateState):
@@ -16,22 +17,147 @@ class UpdateState(CreateUpdateState):
class UpdateManager(BaseAdminManager):
+ """
+ Менеджер для редактирования объектов в базе данных.
+
+ Этот класс предоставляет функциональность для получения списка объектов,
+ выбора объекта для редактирования, выбора данных для обновления и
+ внесения изменений в объекты. Он управляет состоянием пользователя
+ в процессе редактирования и взаимодействует с базой данных
+ через CRUD-операции.
+
+ Methods:
+ get_all_model_names(session: AsyncSession) -> list[str]:
+ Получает список названий объектов из таблицы БД.
+
+ select_obj_to_update(callback: CallbackQuery, state: FSMContext, session: AsyncSession) -> None:
+ Запрашивает у пользователя, какой объект он хочет отредактировать, и отображает список объектов.
+
+ select_data_to_update(callback: CallbackQuery, session: AsyncSession) -> None:
+ Позволяет выбрать поле для редактирования объекта в БД.
+
+ change_obj_name(callback: CallbackQuery, state: FSMContext) -> None:
+ Запрашивает новое название для объекта и обновляет состояние.
+
+ change_obj_content(callback: CallbackQuery, state: FSMContext) -> None:
+ Позволяет пользователю изменить содержание объекта, включая текст, URL и медиафайлы.
+
+ update_obj_in_db(message: Message, state: FSMContext, session: AsyncSession) -> None:
+ Вносит изменения в объект в БД и сбрасывает состояние.
+ """
async def get_all_model_names(self, session: AsyncSession) -> list[str]:
"""Получить список названий объектов из таблицы БД."""
models = await self.model_crud.get_multi(session)
return [model.name for model in models]
+ async def select_obj_to_update(
+ self,
+ callback: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
+ ) -> None:
+ obj_list_by_name = await self.get_all_model_names(session)
+ await callback.message.edit_text(
+ "Какой объект отредактировать?",
+ reply_markup=self.keyboard.add_extra_buttons(obj_list_by_name),
+ )
+ await state.set_state(UpdateState.select)
+
async def select_data_to_update(
- self, callback: CallbackQuery, state: FSMContext
+ self,
+ callback: CallbackQuery,
+ session: AsyncSession,
):
- """Выбрать тип данных для модели в БД."""
+ """Выбрать поле редактирования для модели в БД."""
+ self.obj_to_update = await self.model_crud.get_by_string(
+ callback.data, session
+ )
+ keyboard = InlineKeyboardManager(ADMIN_UPDATE_BUTTONS)
+ await keyboard.add_previous_menu_button(self.keyboard.previous_menu)
await callback.message.edit_text(
"Выбирите данные для обновления:",
- reply_markup=await self.keyboard.add_extra_buttons(
- ["Название", "Содержание"]
+ reply_markup=keyboard.create_keyboard(),
+ )
+
+ async def change_obj_name(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ """Внести изменение в название объекта."""
+ message_text = (
+ f"Текущее название: \n\n {self.obj_to_update.name} \n\n"
+ "Введите новое:"
+ )
+ await callback.message.edit_text(
+ message_text,
+ reply_markup=InlineKeyboardManager.get_back_button(
+ self.keyboard.previous_menu
),
)
- await state.set_state(UpdateState.select)
+ await state.set_state(UpdateState.name)
+
+ async def change_obj_content(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ """Внести изменение в содержание объекта."""
+ if not self.obj_to_update.media:
+ if self.obj_to_update.url:
+ message_text = (
+ f"Текущий адрес ссылки: \n\n {self.obj_to_update.url} \n\n"
+ "Введите новый:"
+ )
+ await state.set_state(UpdateState.url)
+ elif self.obj_to_update.text and not self.obj_to_update.media:
+ message_text = (
+ f"Текущий текст: \n\n {self.obj_to_update.text} \n\n"
+ "Введите новый:"
+ )
+ await state.set_state(UpdateState.text)
+ await callback.message.edit_text(
+ message_text,
+ reply_markup=InlineKeyboardManager.get_back_button(
+ self.keyboard.previous_menu
+ ),
+ )
+ else:
+ await callback.message.answer("Текущая картинка:")
+ await callback.message.answer_photo(
+ photo=self.obj_to_update.media,
+ caption=self.obj_to_update.text,
+ )
+ await callback.message.answer(
+ "Добавьте новую картинку и описание",
+ reply_markup=InlineKeyboardManager.get_back_button(
+ self.keyboard.previous_menu,
+ ),
+ )
+ await state.set_state(UpdateState.media)
+
+ async def update_obj_in_db(
+ self, message: Message, state: FSMContext, session: AsyncSession
+ ):
+ """Внести изменения объекта в БД."""
+ try:
+ current_state = await state.get_state()
+ if current_state == UpdateState.url.state:
+ await state.update_data(url=message.text)
+ elif current_state == UpdateState.text.state:
+ await state.update_data(text=message.text)
+ elif current_state == UpdateState.media.state:
+ await state.update_data(
+ media=message.photo[-1].file_id,
+ text=message.caption,
+ )
+ data = await state.get_data()
+ await self.model_crud.update(self.obj_to_update, data, session)
+ await message.answer(
+ "Данные обновлены!",
+ reply_markup=InlineKeyboardManager.get_back_button(
+ self.keyboard.previous_menu
+ ),
+ )
+ await state.clear()
+ except Exception as e:
+ await message.answer(f"Произошла ошибка: {e}")
diff --git a/app/admin/keyboards/keyboards.py b/app/admin/keyboards/keyboards.py
index dfc5ded..d76d514 100644
--- a/app/admin/keyboards/keyboards.py
+++ b/app/admin/keyboards/keyboards.py
@@ -165,14 +165,20 @@ async def get_delete_message_keyboard() -> InlineKeyboardMarkup:
class InlineKeyboardManager:
- def __init__(self, options=None, callback=None, urls=None, size=(1,)):
+ def __init__(
+ self,
+ options=None,
+ callback=None,
+ urls=None,
+ size=(1,),
+ ):
self.options = options if options is not None else []
self.callback = callback if callback is not None else self.options
self.urls = urls if urls is not None else []
self.size = size
self.keyboard = InlineKeyboardBuilder()
- async def add_buttons(self):
+ def add_buttons(self):
"""Добавить основные кнопки в клавиатуру."""
for index, option in enumerate(self.options):
self.keyboard.add(
@@ -187,17 +193,19 @@ async def add_buttons(self):
)
)
- async def add_previous_menu_button(self, previous_menu):
+ def add_previous_menu_button(
+ self, previous_menu: str, menu_text: str = "Назад"
+ ):
"""Добавить кнопку 'Назад'."""
self.previous_menu = previous_menu
self.keyboard.add(
InlineKeyboardButton(
- text="Назад",
+ text=menu_text,
callback_data=previous_menu,
)
)
- async def add_admin_button(self, admin_update_menu):
+ def add_admin_button(self, admin_update_menu):
"""Добавить кнопку 'Редактировать' для администраторов."""
self.keyboard.add(
InlineKeyboardButton(
@@ -206,7 +214,7 @@ async def add_admin_button(self, admin_update_menu):
)
)
- async def create_keyboard(self) -> InlineKeyboardMarkup:
+ def create_keyboard(self) -> InlineKeyboardMarkup:
"""Создать клавиатуру и вернуть ее."""
self.add_buttons()
return self.keyboard.adjust(*self.size).as_markup(resize_keyboard=True)
@@ -223,7 +231,7 @@ async def add_extra_buttons(
)
@staticmethod
- async def get_inline_confirmation(
+ def get_inline_confirmation(
cancel_option: str,
option: str = "Да",
) -> InlineKeyboardMarkup:
@@ -238,7 +246,7 @@ async def get_inline_confirmation(
return keyboard.adjust(2).as_markup(resize_keyboard=True)
@staticmethod
- async def get_back_button(previous_menu: str) -> InlineKeyboardMarkup:
+ def get_back_button(previous_menu: str) -> InlineKeyboardMarkup:
keyboard = InlineKeyboardBuilder()
keyboard.add(
InlineKeyboardButton(text="Назад", callback_data=previous_menu)
diff --git a/app/const.py b/app/const.py
index a486685..8a434ec 100644
--- a/app/const.py
+++ b/app/const.py
@@ -37,6 +37,11 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
"feedback": "Посмотреть отзывы",
}
ADMIN_BASE_BUTTONS = get_buttons(ADMIN_BASE_REPLY_OPTIONS)
+ADMIN_UPDATE_OPTIONS = {
+ "name": "Название",
+ "content": "Содержание",
+}
+ADMIN_UPDATE_BUTTONS = get_buttons(ADMIN_UPDATE_OPTIONS)
ADMIN_CONTENT_OPTIONS = {
"url": "Ссылка",
"text": "Текст",
From 05d489ad10ede0cb47b3215608573b1fcd95a67d Mon Sep 17 00:00:00 2001
From: ikhit
Date: Sat, 12 Oct 2024 06:49:36 +0300
Subject: [PATCH 31/75] yet another commit
---
app/admin/admin_orm/create_manager.py | 11 ++++++-----
app/admin/admin_orm/delete_manager.py | 3 ++-
app/admin/keyboards/keyboards.py | 13 -------------
3 files changed, 8 insertions(+), 19 deletions(-)
diff --git a/app/admin/admin_orm/create_manager.py b/app/admin/admin_orm/create_manager.py
index 028bdd3..7aee8d4 100644
--- a/app/admin/admin_orm/create_manager.py
+++ b/app/admin/admin_orm/create_manager.py
@@ -56,11 +56,12 @@ async def select_data_type(
self, callback: CallbackQuery, state: FSMContext
):
"""Выбрать тип данных для модели в БД."""
+ self.keyboard.add_extra_buttons(
+ ADMIN_CONTENT_BUTTONS
+ )
await callback.message.edit_text(
"Выбирите способ передачи информации:",
- reply_markup=self.keyboard.add_extra_buttons(
- ADMIN_CONTENT_BUTTONS
- ),
+ reply_markup=self.keyboard.create_keyboard(),
)
await state.set_state(CreateState.select)
@@ -74,7 +75,7 @@ async def add_obj_name(
следующее машинное состояние.
"""
await callback.message.answer(
- "Введите название:", reply_markup=self.keyboard
+ "Введите название:", reply_markup=self.keyboard.create_keyboard()
)
await state.set_state(CreateState.name)
@@ -92,7 +93,7 @@ async def prompt_for_input(
await state.update_data(name=message.text)
await message.answer(
message_text,
- reply_markup=self.keyboard,
+ reply_markup=self.keyboard.create_keyboard(),
)
await state.set_state(next_state)
diff --git a/app/admin/admin_orm/delete_manager.py b/app/admin/admin_orm/delete_manager.py
index 98142f8..74fc8b1 100644
--- a/app/admin/admin_orm/delete_manager.py
+++ b/app/admin/admin_orm/delete_manager.py
@@ -50,9 +50,10 @@ async def select_obj_to_delete(
session: AsyncSession,
) -> None:
obj_list_by_name = await self.get_all_model_names(session)
+ keyboard = self.keyboard.add_extra_buttons(obj_list_by_name)
await callback.message.edit_text(
"Какой объект удалить?",
- reply_markup=self.keyboard.add_extra_buttons(obj_list_by_name),
+ reply_markup=keyboard.create_keyboard(),
)
await state.set_state(DeleteState.select)
diff --git a/app/admin/keyboards/keyboards.py b/app/admin/keyboards/keyboards.py
index d76d514..c2f8c4e 100644
--- a/app/admin/keyboards/keyboards.py
+++ b/app/admin/keyboards/keyboards.py
@@ -151,19 +151,6 @@ async def get_delete_message_keyboard() -> InlineKeyboardMarkup:
return keyboard.adjust(1).as_markup(resize_keyboard=True)
-# async def get_inline_confirmation_keyboard(
-# cancel_option: str,
-# option: str = "Да",
-# ) -> InlineKeyboardMarkup:
-# """Кнопка для подтверждения действий."""
-
-# keyboard = InlineKeyboardBuilder()
-# keyboard.add(InlineKeyboardButton(text="Да", callback_data=option))
-# keyboard.add(InlineKeyboardButton(text="Нет", callback_data=cancel_option))
-
-# return keyboard.adjust(2).as_markup(resize_keyboard=True)
-
-
class InlineKeyboardManager:
def __init__(
self,
From 68c34e62b75addaa08d8d82bf8dccd4bc77b25e7 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Sat, 12 Oct 2024 16:08:03 +0300
Subject: [PATCH 32/75] add docker compose
---
DockerFile | 5 +
alembic/versions/3fe28ef6d0c4_first_commit.py | 102 ------------------
alembic/versions/726f3c66f8d4_test_commit.py | 102 ------------------
alembic.ini => app/alembic.ini | 0
{alembic => app/alembic}/README | 0
{alembic => app/alembic}/env.py | 6 +-
{alembic => app/alembic}/script.py.mako | 0
app/core/base.py | 4 +-
app/models/models.py | 3 +-
app/scripts_for_db.py | 9 ++
docker-compose.yml | 1 +
11 files changed, 20 insertions(+), 212 deletions(-)
delete mode 100644 alembic/versions/3fe28ef6d0c4_first_commit.py
delete mode 100644 alembic/versions/726f3c66f8d4_test_commit.py
rename alembic.ini => app/alembic.ini (100%)
rename {alembic => app/alembic}/README (100%)
rename {alembic => app/alembic}/env.py (98%)
rename {alembic => app/alembic}/script.py.mako (100%)
diff --git a/DockerFile b/DockerFile
index d3dbca6..fa43760 100644
--- a/DockerFile
+++ b/DockerFile
@@ -22,5 +22,10 @@ RUN poetry install --no-root
# Копирование остальных файлов, включая alembic
COPY . .
+# Создание миграций
+#RUN poetry run alembic stamp head
+#RUN poetry run alembic revision --autogenerate -m "compose commit"
+#RUN poetry run alembic upgrade head
+
# Команда для запуска приложения
CMD ["poetry", "run", "python", "app/main.py"]
diff --git a/alembic/versions/3fe28ef6d0c4_first_commit.py b/alembic/versions/3fe28ef6d0c4_first_commit.py
deleted file mode 100644
index df8c8e1..0000000
--- a/alembic/versions/3fe28ef6d0c4_first_commit.py
+++ /dev/null
@@ -1,102 +0,0 @@
-"""first commit
-
-Revision ID: 3fe28ef6d0c4
-Revises:
-Create Date: 2024-10-09 10:04:32.815358
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = '3fe28ef6d0c4'
-down_revision: Union[str, None] = None
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('checkcompanyportfolio',
- sa.Column('project_name', sa.VARCHAR(length=48), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('contactmanager',
- sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
- sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
- sa.Column('need_support', sa.BOOLEAN(), nullable=False),
- sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
- sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
- sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('info',
- sa.Column('question_type', postgresql.ENUM('GENERAL_QUESTIONS', 'PROBLEMS_WITH_PRODUCTS', name='question_enum'), nullable=False),
- sa.Column('question', sa.TEXT(), nullable=False),
- sa.Column('answer', sa.TEXT(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('question')
- )
- op.create_table('informationaboutcompany',
- sa.Column('name', sa.VARCHAR(length=48), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('productcategory',
- sa.Column('title', sa.VARCHAR(length=150), nullable=False),
- sa.Column('response', sa.TEXT(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('user',
- sa.Column('tg_id', sa.BIGINT(), nullable=False),
- sa.Column('role', postgresql.ENUM('USER', 'ADMIN', 'MANAGER', name='role_enum'), nullable=False),
- sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('tg_id')
- )
- op.create_table('categorytype',
- sa.Column('name', sa.VARCHAR(length=150), nullable=False),
- sa.Column('product_id', sa.Integer(), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('media', sa.VARCHAR(length=128), nullable=True),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ondelete='CASCADE'),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_index(op.f('ix_categorytype_product_id'), 'categorytype', ['product_id'], unique=False)
- op.create_table('feedback',
- sa.Column('user', sa.Integer(), nullable=False),
- sa.Column('contact_manager_id', sa.Integer(), nullable=False),
- sa.Column('feedback_text', sa.TEXT(), nullable=False),
- sa.Column('feedback_date', postgresql.TIMESTAMP(), nullable=False),
- sa.Column('unread', sa.BOOLEAN(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['contact_manager_id'], ['contactmanager.id'], ondelete='CASCADE'),
- sa.ForeignKeyConstraint(['user'], ['user.id'], ondelete='CASCADE'),
- sa.PrimaryKeyConstraint('id')
- )
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('feedback')
- op.drop_index(op.f('ix_categorytype_product_id'), table_name='categorytype')
- op.drop_table('categorytype')
- op.drop_table('user')
- op.drop_table('productcategory')
- op.drop_table('informationaboutcompany')
- op.drop_table('info')
- op.drop_table('contactmanager')
- op.drop_table('checkcompanyportfolio')
- # ### end Alembic commands ###
diff --git a/alembic/versions/726f3c66f8d4_test_commit.py b/alembic/versions/726f3c66f8d4_test_commit.py
deleted file mode 100644
index f72b1ed..0000000
--- a/alembic/versions/726f3c66f8d4_test_commit.py
+++ /dev/null
@@ -1,102 +0,0 @@
-"""Test commit
-
-Revision ID: 726f3c66f8d4
-Revises: d2365dc6d2ca
-Create Date: 2024-10-08 19:27:27.701551
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = '726f3c66f8d4'
-down_revision: Union[str, None] = 'd2365dc6d2ca'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('checkcompanyportfolio',
- sa.Column('project_name', sa.VARCHAR(length=48), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('contactmanager',
- sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
- sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
- sa.Column('need_support', sa.BOOLEAN(), nullable=False),
- sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
- sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
- sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('info',
- sa.Column('question_type', postgresql.ENUM('GENERAL_QUESTIONS', 'PROBLEMS_WITH_PRODUCTS', name='question_enum'), nullable=False),
- sa.Column('question', sa.TEXT(), nullable=False),
- sa.Column('answer', sa.TEXT(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('question')
- )
- op.create_table('informationaboutcompany',
- sa.Column('name', sa.VARCHAR(length=48), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('productcategory',
- sa.Column('title', sa.VARCHAR(length=150), nullable=False),
- sa.Column('response', sa.TEXT(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_table('user',
- sa.Column('tg_id', sa.BIGINT(), nullable=False),
- sa.Column('role', postgresql.ENUM('USER', 'ADMIN', 'MANAGER', name='role_enum'), nullable=False),
- sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('tg_id')
- )
- op.create_table('categorytype',
- sa.Column('name', sa.VARCHAR(length=150), nullable=False),
- sa.Column('product_id', sa.Integer(), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
- sa.Column('media', sa.VARCHAR(length=128), nullable=True),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ondelete='CASCADE'),
- sa.PrimaryKeyConstraint('id')
- )
- op.create_index(op.f('ix_categorytype_product_id'), 'categorytype', ['product_id'], unique=False)
- op.create_table('feedback',
- sa.Column('user', sa.Integer(), nullable=False),
- sa.Column('contact_manager_id', sa.Integer(), nullable=False),
- sa.Column('feedback_text', sa.TEXT(), nullable=False),
- sa.Column('feedback_date', postgresql.TIMESTAMP(), nullable=False),
- sa.Column('unread', sa.BOOLEAN(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['contact_manager_id'], ['contactmanager.id'], ondelete='CASCADE'),
- sa.ForeignKeyConstraint(['user'], ['user.id'], ondelete='CASCADE'),
- sa.PrimaryKeyConstraint('id')
- )
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('feedback')
- op.drop_index(op.f('ix_categorytype_product_id'), table_name='categorytype')
- op.drop_table('categorytype')
- op.drop_table('user')
- op.drop_table('productcategory')
- op.drop_table('informationaboutcompany')
- op.drop_table('info')
- op.drop_table('contactmanager')
- op.drop_table('checkcompanyportfolio')
- # ### end Alembic commands ###
diff --git a/alembic.ini b/app/alembic.ini
similarity index 100%
rename from alembic.ini
rename to app/alembic.ini
diff --git a/alembic/README b/app/alembic/README
similarity index 100%
rename from alembic/README
rename to app/alembic/README
diff --git a/alembic/env.py b/app/alembic/env.py
similarity index 98%
rename from alembic/env.py
rename to app/alembic/env.py
index c557683..35b65ef 100644
--- a/alembic/env.py
+++ b/app/alembic/env.py
@@ -2,15 +2,13 @@
import os
from logging.config import fileConfig
+from alembic import context
from dotenv import load_dotenv
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
-from alembic import context
-
-
-from app.core.base import Base
+from core.base import Base
load_dotenv('.env')
diff --git a/alembic/script.py.mako b/app/alembic/script.py.mako
similarity index 100%
rename from alembic/script.py.mako
rename to app/alembic/script.py.mako
diff --git a/app/core/base.py b/app/core/base.py
index 12750f6..482870f 100644
--- a/app/core/base.py
+++ b/app/core/base.py
@@ -1,7 +1,7 @@
"""Импорты класса Base и всех моделей для Alembic."""
-from app.core.db import Base # noqa
-from app.models.models import ( # noqa
+from core.db import Base # noqa
+from models.models import ( # noqa
User,
ProductCategory,
CategoryType,
diff --git a/app/models/models.py b/app/models/models.py
index b8d57fa..a3d4ab6 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -1,12 +1,11 @@
from datetime import datetime
+from enum import Enum
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
import sqlalchemy.dialects.postgresql as pgsql_types
-from enum import Enum
-
from core.db import Base
diff --git a/app/scripts_for_db.py b/app/scripts_for_db.py
index 4325c44..b40eb89 100644
--- a/app/scripts_for_db.py
+++ b/app/scripts_for_db.py
@@ -1,3 +1,12 @@
+'''
+Скрипт тестовых данных для ветки информация о компании:
+
+INSERT INTO InformationAboutCompany (name, url)
+VALUES
+ ('Презентация компании', 'https://scid.ru/'),
+ ('Карточка компании', 'https://scid.ru/contacts');
+'''
+
'''
Скрипт для ветки узнать о продуктах и услугах:
diff --git a/docker-compose.yml b/docker-compose.yml
index 541cd0e..0898673 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,6 +16,7 @@ services:
- db
container_name: scid-bot
build: .
+ command: bash -c "cd app && poetry run alembic stamp head && poetry run alembic revision --autogenerate && poetry run alembic upgrade head && poetry run python main.py"
env_file: .env
gateway:
From 3406852b22974b1a343e37afef6ae280af7a5b64 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Sat, 12 Oct 2024 17:38:04 +0300
Subject: [PATCH 33/75] done company_about admin refactor
---
app/admin/admin_managers/__init__.py | 3 +
.../base_manager.py | 9 +-
.../create_manager.py | 42 +-
.../delete_manager.py | 29 +-
.../update_manager.py | 59 ++-
app/admin/admin_orm/__init__.py | 0
app/{const.py => admin/admin_settings.py} | 0
app/admin/filters/filters.py | 4 +-
app/admin/handlers/admin_handlers/admin.py | 339 +---------------
.../admin_about_company_handlers.py | 217 ++++------
.../admin_handlers/admin_category_handlers.py | 16 +-
.../admin_handlers/admin_info_handlers.py | 14 +-
.../admin_portfolio_handlers.py | 41 +-
.../admin_handlers/admin_product_handlers.py | 25 +-
app/admin/handlers/user.py | 8 +-
app/admin/handlers/validators.py | 2 +-
app/admin/keyboards/keyboards.py | 263 ++++--------
app/bot/handlers.py | 2 +-
app/core/base.py | 4 +-
app/core/init_db.py | 2 +-
app/core/settings.py | 14 +-
app/models/models.py | 2 +-
test.py | 380 +++++++++++++-----
23 files changed, 569 insertions(+), 906 deletions(-)
create mode 100644 app/admin/admin_managers/__init__.py
rename app/admin/{admin_orm => admin_managers}/base_manager.py (75%)
rename app/admin/{admin_orm => admin_managers}/create_manager.py (86%)
rename app/admin/{admin_orm => admin_managers}/delete_manager.py (82%)
rename app/admin/{admin_orm => admin_managers}/update_manager.py (78%)
delete mode 100644 app/admin/admin_orm/__init__.py
rename app/{const.py => admin/admin_settings.py} (100%)
diff --git a/app/admin/admin_managers/__init__.py b/app/admin/admin_managers/__init__.py
new file mode 100644
index 0000000..f1d8c1a
--- /dev/null
+++ b/app/admin/admin_managers/__init__.py
@@ -0,0 +1,3 @@
+from .delete_manager import DeleteManager, DeleteState # noqa
+from .create_manager import CreateManager, CreateState # noqa
+from .update_manager import UpdateManager, UpdateState # noqa
diff --git a/app/admin/admin_orm/base_manager.py b/app/admin/admin_managers/base_manager.py
similarity index 75%
rename from app/admin/admin_orm/base_manager.py
rename to app/admin/admin_managers/base_manager.py
index 2057bb3..8cc12d1 100644
--- a/app/admin/admin_orm/base_manager.py
+++ b/app/admin/admin_managers/base_manager.py
@@ -2,8 +2,7 @@
from aiogram.fsm.state import State, StatesGroup
-from app.admin.keyboards.keyboards import InlineKeyboardManager
-from app.crud.base_crud import CRUDBase
+from crud.base_crud import CRUDBase
class CreateUpdateState(StatesGroup):
@@ -29,11 +28,11 @@ class BaseAdminManager(ABC):
Attributes:
model_crud (CRUDBase): Объект для выполнения операций CRUD с моделью.
- keyboard (InlineKeyboardManager): Менеджер для создания интерактивных клавиатур.
+ back_option (str): Данные для возврата в меню.
"""
def __init__(
- self, model_crud: CRUDBase, keyboard: InlineKeyboardManager
+ self, model_crud: CRUDBase, back_option: str
) -> None:
self.model_crud = model_crud
- self.keyboard = keyboard
+ self.back_option = back_option
diff --git a/app/admin/admin_orm/create_manager.py b/app/admin/admin_managers/create_manager.py
similarity index 86%
rename from app/admin/admin_orm/create_manager.py
rename to app/admin/admin_managers/create_manager.py
index 7aee8d4..02089a2 100644
--- a/app/admin/admin_orm/create_manager.py
+++ b/app/admin/admin_managers/create_manager.py
@@ -1,21 +1,25 @@
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import State
+from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
-from app.admin.admin_orm.base_manager import (
+from .base_manager import (
BaseAdminManager,
- CreateUpdateState,
)
-from app.admin.keyboards.keyboards import InlineKeyboardManager
-from app.const import ADMIN_CONTENT_BUTTONS
+from admin.keyboards.keyboards import (
+ get_inline_keyboard,
+)
+from admin.admin_settings import ADMIN_CONTENT_BUTTONS
-class CreateState(CreateUpdateState):
+class CreateState(StatesGroup):
"""Класс состояний для создания объекта в БД."""
- pass
-
+ select = State()
+ name = State()
+ url = State()
+ text = State()
+ media = State()
class CreateManager(BaseAdminManager):
"""
@@ -27,7 +31,7 @@ class CreateManager(BaseAdminManager):
Attributes:
model_crud (CRUDBase): Объект для выполнения операций CRUD с моделью.
- keyboard (InlineKeyboardManager): Менеджер для создания интерактивных клавиатур.
+ back_option (str): Данные для возврата в меню.
Methods:
select_data_type(callback: CallbackQuery, state: FSMContext):
@@ -56,12 +60,11 @@ async def select_data_type(
self, callback: CallbackQuery, state: FSMContext
):
"""Выбрать тип данных для модели в БД."""
- self.keyboard.add_extra_buttons(
- ADMIN_CONTENT_BUTTONS
- )
await callback.message.edit_text(
"Выбирите способ передачи информации:",
- reply_markup=self.keyboard.create_keyboard(),
+ reply_markup=await get_inline_keyboard(
+ ADMIN_CONTENT_BUTTONS, previous_menu=self.back_option
+ ),
)
await state.set_state(CreateState.select)
@@ -75,7 +78,10 @@ async def add_obj_name(
следующее машинное состояние.
"""
await callback.message.answer(
- "Введите название:", reply_markup=self.keyboard.create_keyboard()
+ "Введите название:",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
)
await state.set_state(CreateState.name)
@@ -93,7 +99,9 @@ async def prompt_for_input(
await state.update_data(name=message.text)
await message.answer(
message_text,
- reply_markup=self.keyboard.create_keyboard(),
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
)
await state.set_state(next_state)
@@ -160,8 +168,8 @@ async def add_obj_to_db(
await message.answer(
"Данные добавлены!",
- reply_markup=InlineKeyboardManager.get_back_button(
- self.keyboard.previous_menu
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
),
)
await state.clear()
diff --git a/app/admin/admin_orm/delete_manager.py b/app/admin/admin_managers/delete_manager.py
similarity index 82%
rename from app/admin/admin_orm/delete_manager.py
rename to app/admin/admin_managers/delete_manager.py
index 74fc8b1..802b764 100644
--- a/app/admin/admin_orm/delete_manager.py
+++ b/app/admin/admin_managers/delete_manager.py
@@ -3,8 +3,11 @@
from aiogram.types import CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession
-from app.admin.admin_orm.base_manager import BaseAdminManager
-from app.admin.keyboards.keyboards import InlineKeyboardManager
+from .base_manager import BaseAdminManager
+from admin.keyboards.keyboards import (
+ get_inline_confirmation,
+ get_inline_keyboard,
+)
from models.models import Info
@@ -24,6 +27,11 @@ class DeleteManager(BaseAdminManager):
Он взаимодействует с базой данных через CRUD-операции и управляет состоянием
пользователя в процессе удаления.
+ Attributes:
+ model_crud (CRUDBase): Объект для выполнения операций CRUD с моделью.
+ back_option (str): Данные для возврата в меню.
+
+
Methods:
get_all_model_names(session: AsyncSession) -> list[str]:
Получает список названий объектов из таблицы БД.
@@ -50,10 +58,11 @@ async def select_obj_to_delete(
session: AsyncSession,
) -> None:
obj_list_by_name = await self.get_all_model_names(session)
- keyboard = self.keyboard.add_extra_buttons(obj_list_by_name)
await callback.message.edit_text(
- "Какой объект удалить?",
- reply_markup=keyboard.create_keyboard(),
+ "Какие данные удалить?",
+ reply_markup=await get_inline_keyboard(
+ obj_list_by_name, previous_menu=self.back_option
+ ),
)
await state.set_state(DeleteState.select)
@@ -72,9 +81,9 @@ async def confirm_delete(
else self.obj_to_delete.name
)
await callback.message.edit_text(
- f"Вы уверены, что хотите удалить этот вопрос?\n\n {obj_data}",
- reply_markup=InlineKeyboardManager.get_inline_confirmation(
- cancel_option=self.keyboard.previous_menu
+ f"Вы уверены, что хотите удалить эти данные?\n\n {obj_data}",
+ reply_markup=await get_inline_confirmation(
+ cancel_option=self.back_option
),
),
await state.set_state(DeleteState.confirm)
@@ -90,8 +99,8 @@ async def delete_obj(
await self.model_crud.remove(self.obj_to_delete, session)
await callback.message.edit_text(
"Данные удалены!",
- reply_markup=InlineKeyboardManager.get_back_button(
- self.keyboard.previous_menu
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
),
)
await state.clear()
diff --git a/app/admin/admin_orm/update_manager.py b/app/admin/admin_managers/update_manager.py
similarity index 78%
rename from app/admin/admin_orm/update_manager.py
rename to app/admin/admin_managers/update_manager.py
index 7e0a5a7..b879ae3 100644
--- a/app/admin/admin_orm/update_manager.py
+++ b/app/admin/admin_managers/update_manager.py
@@ -1,19 +1,25 @@
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
+from aiogram.fsm.state import State, StatesGroup
from sqlalchemy.ext.asyncio import AsyncSession
-from app.admin.admin_orm.base_manager import (
+from .base_manager import (
BaseAdminManager,
- CreateUpdateState,
)
-from app.admin.keyboards.keyboards import InlineKeyboardManager
-from app.const import ADMIN_UPDATE_BUTTONS
+from admin.keyboards.keyboards import (
+ get_inline_keyboard,
+)
+from admin.admin_settings import ADMIN_UPDATE_BUTTONS
-class UpdateState(CreateUpdateState):
+class UpdateState(StatesGroup):
"""Класс состояний для редактирования данных в БД."""
- pass
+ select = State()
+ name = State()
+ url = State()
+ text = State()
+ media = State()
class UpdateManager(BaseAdminManager):
@@ -26,6 +32,10 @@ class UpdateManager(BaseAdminManager):
в процессе редактирования и взаимодействует с базой данных
через CRUD-операции.
+ Attributes:
+ model_crud (CRUDBase): Объект для выполнения операций CRUD с моделью.
+ back_option (str): Данные для возврата в меню.
+
Methods:
get_all_model_names(session: AsyncSession) -> list[str]:
Получает список названий объектов из таблицы БД.
@@ -60,7 +70,9 @@ async def select_obj_to_update(
obj_list_by_name = await self.get_all_model_names(session)
await callback.message.edit_text(
"Какой объект отредактировать?",
- reply_markup=self.keyboard.add_extra_buttons(obj_list_by_name),
+ reply_markup=await get_inline_keyboard(
+ obj_list_by_name, previous_menu=self.back_option
+ ),
)
await state.set_state(UpdateState.select)
@@ -73,11 +85,11 @@ async def select_data_to_update(
self.obj_to_update = await self.model_crud.get_by_string(
callback.data, session
)
- keyboard = InlineKeyboardManager(ADMIN_UPDATE_BUTTONS)
- await keyboard.add_previous_menu_button(self.keyboard.previous_menu)
await callback.message.edit_text(
"Выбирите данные для обновления:",
- reply_markup=keyboard.create_keyboard(),
+ reply_markup=await get_inline_keyboard(
+ ADMIN_UPDATE_BUTTONS, previous_menu=self.back_option
+ ),
)
async def change_obj_name(
@@ -90,8 +102,8 @@ async def change_obj_name(
)
await callback.message.edit_text(
message_text,
- reply_markup=InlineKeyboardManager.get_back_button(
- self.keyboard.previous_menu
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
),
)
await state.set_state(UpdateState.name)
@@ -100,14 +112,15 @@ async def change_obj_content(
self, callback: CallbackQuery, state: FSMContext
):
"""Внести изменение в содержание объекта."""
- if not self.obj_to_update.media:
- if self.obj_to_update.url:
+ obj_fields = self.obj_to_update.__dict__.keys()
+ if "media" not in obj_fields or not self.obj_to_update.media:
+ if "url" in obj_fields and self.obj_to_update.url:
message_text = (
f"Текущий адрес ссылки: \n\n {self.obj_to_update.url} \n\n"
"Введите новый:"
)
await state.set_state(UpdateState.url)
- elif self.obj_to_update.text and not self.obj_to_update.media:
+ elif "text" in obj_fields and self.obj_to_update.text:
message_text = (
f"Текущий текст: \n\n {self.obj_to_update.text} \n\n"
"Введите новый:"
@@ -115,8 +128,8 @@ async def change_obj_content(
await state.set_state(UpdateState.text)
await callback.message.edit_text(
message_text,
- reply_markup=InlineKeyboardManager.get_back_button(
- self.keyboard.previous_menu
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
),
)
else:
@@ -127,8 +140,8 @@ async def change_obj_content(
)
await callback.message.answer(
"Добавьте новую картинку и описание",
- reply_markup=InlineKeyboardManager.get_back_button(
- self.keyboard.previous_menu,
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
),
)
await state.set_state(UpdateState.media)
@@ -139,7 +152,9 @@ async def update_obj_in_db(
"""Внести изменения объекта в БД."""
try:
current_state = await state.get_state()
- if current_state == UpdateState.url.state:
+ if current_state == UpdateState.name.state:
+ await state.update_data(name=message.text)
+ elif current_state == UpdateState.url.state:
await state.update_data(url=message.text)
elif current_state == UpdateState.text.state:
await state.update_data(text=message.text)
@@ -154,8 +169,8 @@ async def update_obj_in_db(
await message.answer(
"Данные обновлены!",
- reply_markup=InlineKeyboardManager.get_back_button(
- self.keyboard.previous_menu
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
),
)
await state.clear()
diff --git a/app/admin/admin_orm/__init__.py b/app/admin/admin_orm/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/app/const.py b/app/admin/admin_settings.py
similarity index 100%
rename from app/const.py
rename to app/admin/admin_settings.py
diff --git a/app/admin/filters/filters.py b/app/admin/filters/filters.py
index 3b99874..3f2b670 100644
--- a/app/admin/filters/filters.py
+++ b/app/admin/filters/filters.py
@@ -1,7 +1,7 @@
from aiogram.filters import Filter
from aiogram import Bot, types
-# from settings import admin_list
-from const import admin_list
+
+from admin.admin_settings import admin_list
class ChatTypeFilter(Filter):
diff --git a/app/admin/handlers/admin_handlers/admin.py b/app/admin/handlers/admin_handlers/admin.py
index dafa956..9ec44c9 100644
--- a/app/admin/handlers/admin_handlers/admin.py
+++ b/app/admin/handlers/admin_handlers/admin.py
@@ -1,23 +1,15 @@
from aiogram import Router, F
-from aiogram.types import CallbackQuery, Message
-from aiogram.filters import or_f
+from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
-from sqlalchemy.ext.asyncio import AsyncSession
-from crud.feedback_crud import feedback_crud
-from crud.user_crud import user_crud
+
from admin.filters.filters import ChatTypeFilter, IsAdmin
from admin.keyboards.keyboards import (
get_inline_keyboard,
- get_inline_paginated_keyboard,
- get_paginated_keyboard_size,
)
-from const import (
+from admin.admin_settings import (
ADMIN_BASE_KEYBOARD,
- ADMIN_BASE_REPLY_OPTIONS,
- BASE_BUTTONS,
- FEEDBACK_PAGINATION,
MAIN_MENU_BUTTONS,
MAIN_MENU_OPTIONS,
ADMIN_PORTFOLIO_KEYBOARD,
@@ -25,7 +17,6 @@
PORTFOLIO_MENU_OPTIONS,
SUPPORT_OPTIONS,
SUPPROT_MENU_BUTTONS,
- USER_CALLBACK_PAGINATION,
)
@@ -80,7 +71,7 @@ def get_condition(cls, menu_text: str):
@admin_main_router.callback_query(F.data.endswith("_"))
-async def update_category(callback: CallbackQuery, state: FSMContext):
+async def update_section_data(callback: CallbackQuery, state: FSMContext):
"""Админские кнопки для внесения изменений в разделы."""
menu = callback.data.rstrip("_")
@@ -105,325 +96,3 @@ async def update_category(callback: CallbackQuery, state: FSMContext):
)
await state.set_state(SectionState.get_condition(menu))
-
-
-@admin_main_router.message(
- F.text == ADMIN_BASE_REPLY_OPTIONS.get("callback_case"),
-)
-async def get_callback_cases_from_text(
- message: Message, state: FSMContext, session: AsyncSession
-):
-
- user_list = await user_crud.get_users_with_callback_request(session)
- users_by_name = [user.name for user in user_list]
- users_by_id = [user.id for user in user_list]
-
- await message.delete()
- await message.answer(
- "Список пользователей, ожидающих обратного звонка",
- reply_markup=await get_inline_paginated_keyboard(
- options=users_by_name,
- callback=users_by_id,
- items_per_page=USER_CALLBACK_PAGINATION,
- size=get_paginated_keyboard_size(USER_CALLBACK_PAGINATION),
- previous_menu=BASE_BUTTONS.get("main_menu"),
- previous_menu_text=BASE_BUTTONS.get("main_menu"),
- ),
- )
-
- await state.set_state(UserState.callback)
-
-
-@admin_main_router.callback_query(UserState(), F.data.startswith("page:"))
-async def get_callback_cases(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
-
- user_list = await user_crud.get_users_with_callback_request(session)
- users_by_name = [user.name for user in user_list]
- users_by_id = [user.id for user in user_list]
- current_page = int(callback.data.split(":")[1])
-
- await callback.message.edit_text(
- "Список пользователей, ожидающих обратного звонка",
- reply_markup=await get_inline_paginated_keyboard(
- options=users_by_name,
- callback=users_by_id,
- items_per_page=USER_CALLBACK_PAGINATION,
- current_page=current_page,
- size=get_paginated_keyboard_size(USER_CALLBACK_PAGINATION),
- previous_menu=BASE_BUTTONS.get("main_menu"),
- previous_menu_text=BASE_BUTTONS.get("main_menu"),
- ),
- )
-
- await state.set_state(UserState.callback)
-
-
-@admin_main_router.callback_query(
- UserState.callback, F.data != BASE_BUTTONS.get("main_menu")
-)
-async def user_callback_request_data(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- """Закрыть заявку на обратный звонок."""
-
- user = await user_crud.get(callback.data, session)
-
- await callback.message.edit_text(
- f"Пользователь: {user.name}\n\n Телефон: {user.phone}",
- reply_markup=await get_inline_keyboard(
- options=["Закрыть заявку", "Назад к списку"],
- callback=[callback.data, "page:1"],
- ),
- )
-
- await state.set_state(UserState.close_case)
-
-
-@admin_main_router.callback_query(UserState.close_case, F.data)
-async def close_case(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- """Закрыть заявку на обратный звонок."""
-
- user = await user_crud.get(callback.data, session)
-
- await user_crud.close_case(user, session)
- await callback.message.edit_text(
- "Заявка закрыта!",
- reply_markup=await get_inline_keyboard(
- ["Назад к списку"],
- callback=[
- "page:1",
- ],
- ),
- )
-
- await state.set_state(UserState.callback)
-
-
-@admin_main_router.message(
- F.text == ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
-)
-async def feedback_choice(message: Message, state: FSMContext):
- """Вывести выбор списка отзывов."""
-
- await message.delete()
- await message.answer(
- "Какие отзывы показать?",
- reply_markup=await get_inline_keyboard(
- options=[
- "Только новые отзывы",
- "Все отзывы",
- BASE_BUTTONS.get("main_menu"),
- ],
- ),
- )
-
- await state.set_state(FeedbackState.feedback_choice)
-
-
-@admin_main_router.callback_query(
- F.data == ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
-)
-async def feedback_choice_callback(callback: CallbackQuery, state: FSMContext):
- """Вывести выбор списка отзывов."""
-
- await callback.message.edit_text(
- "Какие отзывы показать?",
- reply_markup=await get_inline_keyboard(
- options=[
- "Только новые отзывы",
- "Все отзывы",
- BASE_BUTTONS.get("main_menu"),
- ],
- ),
- )
-
- await state.set_state(FeedbackState.feedback_choice)
-
-
-@admin_main_router.callback_query(
- FeedbackState.feedback_choice,
- F.data == "Только новые отзывы",
-)
-async def get_unread_feedbacks(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
-
- new_feedbacks = await feedback_crud.get_new_feedbacks(session)
- feedbacks_list = [
- f"Отзыв номер {feedback.id} от {feedback.feedback_date}"
- for feedback in new_feedbacks
- ]
- feedbacks_ids = [feedback.id for feedback in new_feedbacks]
-
- await callback.message.edit_text(
- "Только новые отзывы",
- reply_markup=await get_inline_paginated_keyboard(
- options=feedbacks_list,
- callback=feedbacks_ids,
- items_per_page=FEEDBACK_PAGINATION,
- size=get_paginated_keyboard_size(FEEDBACK_PAGINATION),
- previous_menu=ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
- ),
- )
-
- await state.set_state(FeedbackState.new_feedbacks)
-
-
-@admin_main_router.callback_query(
- or_f(
- FeedbackState.feedback_choice,
- FeedbackState.new_feedbacks,
- FeedbackState.mark_as_read,
- ),
- F.data.startswith("page:"),
-)
-async def get_paginated_unread_feedbacks(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
-
- new_feedbacks = await feedback_crud.get_new_feedbacks(session)
- feedbacks_list = [
- f"Отзыв номер {feedback.id} от {feedback.feedback_date}"
- for feedback in new_feedbacks
- ]
- feedbacks_ids = [feedback.id for feedback in new_feedbacks]
- current_page = int(callback.data.split(":")[1])
-
- await callback.message.edit_text(
- "Только новые отзывы",
- reply_markup=await get_inline_paginated_keyboard(
- options=feedbacks_list,
- callback=feedbacks_ids,
- current_page=current_page,
- items_per_page=FEEDBACK_PAGINATION,
- size=get_paginated_keyboard_size(FEEDBACK_PAGINATION),
- previous_menu=ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
- ),
- )
-
- await state.set_state(FeedbackState.new_feedbacks)
-
-
-@admin_main_router.callback_query(
- FeedbackState.feedback_choice,
- F.data == "Все отзывы",
-)
-async def get_all_feedbacks(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
-
- feedbacks = await feedback_crud.get_multi(session)
- feedbacks_list = [
- f"Отзыв номер {feedback.id} от {feedback.feedback_date}"
- for feedback in feedbacks
- ]
- feedbacks_ids = [feedback.id for feedback in feedbacks]
-
- await callback.message.edit_text(
- "Все отзывы",
- reply_markup=await get_inline_paginated_keyboard(
- options=feedbacks_list,
- callback=feedbacks_ids,
- items_per_page=FEEDBACK_PAGINATION,
- size=get_paginated_keyboard_size(FEEDBACK_PAGINATION),
- previous_menu=ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
- ),
- )
-
- await state.set_state(FeedbackState.all_feedbacks)
-
-
-@admin_main_router.callback_query(
- or_f(
- FeedbackState.feedback_choice,
- FeedbackState.all_feedbacks,
- ),
- F.data.startswith("page:"),
-)
-async def get_all_paginated_feedbacks(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
-
- feedbacks = await feedback_crud.get_multi(session)
- feedbacks_list = [
- f"Отзыв номер {feedback.id} от {feedback.feedback_date}"
- for feedback in feedbacks
- ]
- feedbacks_ids = [feedback.id for feedback in feedbacks]
- current_page = int(callback.data.split(":")[1])
-
- await callback.message.edit_text(
- "Все отзывы",
- reply_markup=await get_inline_paginated_keyboard(
- options=feedbacks_list,
- callback=feedbacks_ids,
- current_page=current_page,
- items_per_page=FEEDBACK_PAGINATION,
- size=get_paginated_keyboard_size(FEEDBACK_PAGINATION),
- previous_menu=ADMIN_BASE_REPLY_OPTIONS.get("feedback"),
- ),
- )
-
- await state.set_state(FeedbackState.all_feedbacks)
-
-
-@admin_main_router.callback_query(
- or_f(FeedbackState.all_feedbacks, FeedbackState.new_feedbacks),
- F.data != BASE_BUTTONS.get("main_menu"),
-)
-async def get_feedback(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- """Получить данные отзыва."""
-
- feedback = await feedback_crud.get(callback.data, session)
- feedback_data = (
- f"Отзыв от пользователя {feedback.author.name}:\n\n"
- f"{feedback.feedback_text}\n\n Дата: {feedback.feedback_date}"
- )
-
- if feedback.unread:
- await callback.message.edit_text(
- feedback_data,
- reply_markup=await get_inline_keyboard(
- options=["Отметить как прочитанный", "Обратно к отзывам"],
- callback=[feedback.id, "page:1"],
- ),
- )
-
- await state.set_state(FeedbackState.mark_as_read)
- else:
- await callback.message.edit_text(
- feedback_data,
- reply_markup=await get_inline_keyboard(
- options=[
- "Обратно к отзывам",
- ],
- callback=[
- "page:1",
- ],
- ),
- )
-
-
-@admin_main_router.callback_query(FeedbackState.mark_as_read, F.data)
-async def mark_as_read(callback: CallbackQuery, session: AsyncSession):
-
- feedback = await feedback_crud.get(callback.data, session)
-
- await feedback_crud.mark_as_read(feedback, session)
- await callback.message.edit_text(
- "Отзыв отмечен как прочитанный!",
- reply_markup=await get_inline_keyboard(
- options=[
- "Обратно к отзывам",
- ],
- callback=[
- "Только новые отзывы",
- ],
- ),
- )
diff --git a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
index 1b8fd16..bdce380 100644
--- a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
@@ -1,202 +1,127 @@
from aiogram import F, Router
from aiogram.filters import and_f, or_f
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from .admin import SectionState
from crud.about_crud import company_info_crud
from admin.filters.filters import ChatTypeFilter, IsAdmin
-from admin.keyboards.keyboards import (
- get_inline_confirmation_keyboard,
- get_inline_keyboard,
+from admin.admin_managers import (
+ DeleteManager,
+ DeleteState,
+ CreateState,
+ CreateManager,
+ UpdateManager,
+ UpdateState,
)
-# from settings import MAIN_MENU_OPTIONS
-MAIN_MENU_OPTIONS = {
- "company_bio": "Информация о компании",
- "products": "Продукты и услуги",
- "support": "Техническая поддержка",
- "portfolio": "Портфолио",
- "request_callback": "Связаться с менеджером",
-}
+from admin.admin_settings import (
+ ADMIN_BASE_OPTIONS,
+ ADMIN_UPDATE_OPTIONS,
+ MAIN_MENU_OPTIONS,
+)
+
+PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("company_bio")
about_router = Router()
about_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
-
-class AddAboutInfo(StatesGroup):
- name = State()
- url = State()
-
-
-class UpdateAboutInfo(StatesGroup):
- select = State()
- name = State()
- url = State()
- confirm = State()
-
-
-class DeleteAboutInfo(AddAboutInfo):
- confirm = State()
+create_manager = CreateManager(company_info_crud, PREVIOUS_MENU)
+delete_manager = DeleteManager(company_info_crud, PREVIOUS_MENU)
+update_manager = UpdateManager(company_info_crud, PREVIOUS_MENU)
-PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("company_bio")
-
-
-@about_router.callback_query(SectionState.about, F.data == "Добавить")
+@about_router.callback_query(
+ SectionState.about, F.data == ADMIN_BASE_OPTIONS.get("create")
+)
async def create_about_info(callback: CallbackQuery, state: FSMContext):
- await callback.message.answer("Введите название для ссылки:")
- await state.set_state(AddAboutInfo.name)
+ """Запустить процесс создания новой информации о разделе."""
+ await create_manager.add_obj_name(callback, state)
-@about_router.message(AddAboutInfo.name, F.text)
+@about_router.message(CreateState.name, F.text)
async def add_info_name(message: Message, state: FSMContext):
- await state.update_data(name=message.text)
- await message.answer("Добавьте ссылку:")
- await state.set_state(AddAboutInfo.url)
+ """Сохранить имя нового объекта в состояние."""
+ await create_manager.add_obj_url(message, state)
-@about_router.message(AddAboutInfo.url, F.text)
+@about_router.message(CreateState.url, F.text)
async def add_about_data(
message: Message, state: FSMContext, session: AsyncSession
):
- await state.update_data(url=message.text)
- data = await state.get_data()
- await company_info_crud.create(data, session)
- await message.answer(
- "Информация добавлена!",
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
- await state.clear()
+ """Сохранить URL нового объекта в базу данных."""
+ await create_manager.add_obj_to_db(message, state, session)
-@about_router.callback_query(SectionState.about, F.data == "Удалить")
+@about_router.callback_query(
+ SectionState.about, F.data == ADMIN_BASE_OPTIONS.get("delete")
+)
async def about_info_to_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- about_data = await company_info_crud.get_multi(session)
- info_list = [info.name for info in about_data]
- await callback.message.answer(
- "Какую информацию вы хотите удалить?",
- reply_markup=await get_inline_keyboard(
- info_list, previous_menu=PREVIOUS_MENU
- ),
- )
- await state.set_state(DeleteAboutInfo.name)
-
-
-@about_router.callback_query(DeleteAboutInfo.name, F.data)
+ """Запустить процесс выбора объекта для удаления."""
+ await delete_manager.select_obj_to_delete(callback, state, session)
+
+
+@about_router.callback_query(DeleteState.select, F.data != PREVIOUS_MENU)
async def confirm_delete_info(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- about_data = await company_info_crud.get_by_about_name(
- callback.data, session
- )
- await callback.message.edit_text(
- f"Вы уверены, что хотите удалить эту информацию?\n\n {about_data.name}",
- reply_markup=await get_inline_confirmation_keyboard(
- option=about_data.name, cancel_option=PREVIOUS_MENU
- ),
- )
- await state.set_state(DeleteAboutInfo.confirm)
-
-
-@about_router.callback_query(DeleteAboutInfo.confirm, F.data != PREVIOUS_MENU)
+ """Подтведить удаление выбранного объекта."""
+ await delete_manager.confirm_delete(callback, state, session)
+
+
+@about_router.callback_query(DeleteState.confirm, F.data != PREVIOUS_MENU)
async def delete_about_info(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- about_data = await company_info_crud.get_by_about_name(
- callback.data, session
- )
- await company_info_crud.remove(about_data, session)
- await callback.message.edit_text(
- "Информация удалена!",
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
- await state.clear()
-
-
-@about_router.callback_query(SectionState.about, F.data == "Изменить")
+ """Удалить выбранный объект из базы данных."""
+ await delete_manager.delete_obj(callback, state, session)
+
+
+@about_router.callback_query(
+ SectionState.about, F.data == ADMIN_BASE_OPTIONS.get("update")
+)
async def about_info_to_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- about_data = await company_info_crud.get_multi(session)
- info_list = [info.name for info in about_data]
- await callback.message.edit_text(
- "Какую информацию вы хотите отредактировать?",
- reply_markup=await get_inline_keyboard(
- info_list, previous_menu=PREVIOUS_MENU
- ),
- )
- await state.set_state(UpdateAboutInfo.select)
+ """Запустить процесс выбора объекта для обновления."""
+ await update_manager.select_obj_to_update(callback, state, session)
@about_router.callback_query(
- UpdateAboutInfo.select,
+ UpdateState.select,
and_f(
- F.data != "Название ссылки",
- F.data != "Адрес ссылки",
+ F.data != ADMIN_UPDATE_OPTIONS.get("name"),
+ F.data != ADMIN_UPDATE_OPTIONS.get("content"),
F.data != PREVIOUS_MENU,
),
)
-async def update_info_choise(callback: CallbackQuery, state: FSMContext):
- await state.update_data(select=callback.data)
- await callback.message.edit_text(
- "Что вы хотите отредактировать?",
- reply_markup=await get_inline_keyboard(
- ["Название ссылки", "Адрес ссылки"], previous_menu=PREVIOUS_MENU
- ),
- )
+async def update_info_choice(callback: CallbackQuery, session: FSMContext):
+ """Обработать выбор данных для обновления."""
+ await update_manager.select_data_to_update(callback, session)
@about_router.callback_query(
- UpdateAboutInfo.select, F.data == "Название ссылки"
+ UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
)
-async def about_name_update(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- about_name = await state.get_data()
- about_name_text = about_name.get("select")
- await callback.message.answer(
- f"Сейчас у ссылки такое название:\n\n {about_name_text}\n\n Введите новое название"
- )
- await state.set_state(UpdateAboutInfo.name)
+async def about_name_update(callback: CallbackQuery, state: FSMContext):
+ """Обновить имя объекта."""
+ await update_manager.change_obj_name(callback, state)
-@about_router.callback_query(UpdateAboutInfo.select, F.data == "Адрес ссылки")
-async def about_url_update(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- about_name_data = await state.get_data()
- about_name_text = about_name_data.get("select")
- about_info = await company_info_crud.get_by_about_name(
- about_name_text, session
- )
- await callback.message.answer(
- f"Сейчас у ссылки такой адрес:\n\n {about_info.url}\n\n Введите новое название"
- )
- await state.set_state(UpdateAboutInfo.url)
-
-
-@about_router.message(or_f(UpdateAboutInfo.name, UpdateAboutInfo.url), F.text)
+@about_router.callback_query(
+ UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
+)
+async def about_url_update(callback: CallbackQuery, state: FSMContext):
+ """Обновить содержимое объекта."""
+ await update_manager.change_obj_content(callback, state)
+
+
+@about_router.message(or_f(UpdateState.name, UpdateState.url), F.text)
async def update_about_info(
message: Message, state: FSMContext, session: AsyncSession
):
- current_state = await state.get_state()
- old_data = await state.get_data()
- old_about_data = await company_info_crud.get_by_about_name(
- old_data.get("select"), session
- )
- if current_state == UpdateAboutInfo.name:
- await state.update_data(name=message.text)
- elif current_state == UpdateAboutInfo.url:
- await state.update_data(url=message.text)
- update_data = await state.get_data()
- await company_info_crud.update(old_about_data, update_data, session)
- await message.answer(
- "Информация обновлена!",
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
- await state.clear()
+ """Обновить объект в базе данных на основе нового содержимого."""
+ await update_manager.update_obj_in_db(message, state, session)
diff --git a/app/admin/handlers/admin_handlers/admin_category_handlers.py b/app/admin/handlers/admin_handlers/admin_category_handlers.py
index 985264e..1926f46 100644
--- a/app/admin/handlers/admin_handlers/admin_category_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_category_handlers.py
@@ -11,19 +11,11 @@
from admin.filters.filters import ChatTypeFilter, IsAdmin
from admin.handlers.user import ProductCategory
from admin.keyboards.keyboards import (
- get_inline_confirmation_keyboard,
+ get_inline_confirmation,
get_inline_keyboard,
)
-# from settings import MAIN_MENU_OPTIONS, admin_list
-from const import admin_list
-MAIN_MENU_OPTIONS = {
- "company_bio": "Информация о компании",
- "products": "Продукты и услуги",
- "support": "Техническая поддержка",
- "portfolio": "Портфолио",
- "request_callback": "Связаться с менеджером",
-}
+from admin.admin_settings import MAIN_MENU_OPTIONS, admin_list
category_router = Router()
category_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
@@ -171,7 +163,7 @@ async def add_media_description(message: Message, state: FSMContext):
await state.update_data(media=message.photo[-1].file_id)
await message.answer(
"Добавить описание к картинке?",
- reply_markup=await get_inline_confirmation_keyboard(
+ reply_markup=await get_inline_confirmation(
"Текст", cancel_option="Нет"
),
)
@@ -225,7 +217,7 @@ async def confirm_delete(
category = await get_category_by_name(callback.data, state, session)
await callback.message.edit_text(
f"Вы уверены, что хотите удалить этот проект?\n\n {category.name}",
- reply_markup=await get_inline_confirmation_keyboard(
+ reply_markup=await get_inline_confirmation(
option=category.name, cancel_option="Назад"
),
)
diff --git a/app/admin/handlers/admin_handlers/admin_info_handlers.py b/app/admin/handlers/admin_handlers/admin_info_handlers.py
index 373c0df..10ceeec 100644
--- a/app/admin/handlers/admin_handlers/admin_info_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_info_handlers.py
@@ -8,22 +8,14 @@
from admin.filters.filters import ChatTypeFilter, IsAdmin
from admin.handlers.admin_handlers.admin import SectionState
from admin.keyboards.keyboards import (
- get_inline_confirmation_keyboard,
+ get_inline_confirmation,
get_inline_keyboard,
)
-# from settings import SUPPORT_OPTIONS
+from admin.admin_settings import SUPPORT_OPTIONS
from sqlalchemy.ext.asyncio import AsyncSession
from aiogram.fsm.state import State, StatesGroup
-
-SUPPORT_OPTIONS = {
- "faq": "Общие вопросы",
- "troubleshooting": "Проблемы с продуктами",
- "callback_request": "Запрос на обратный звонок",
-}
-
-
info_router = Router()
info_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
@@ -130,7 +122,7 @@ async def confirm_delete_question(
question = await info_crud.get_by_string(callback.data, session)
await callback.message.edit_text(
f"Вы уверены, что хотите удалить этот вопрос?\n\n {question.question}",
- reply_markup=await get_inline_confirmation_keyboard(
+ reply_markup=await get_inline_confirmation(
option=question.question, cancel_option=PREVIOUS_MENU
),
)
diff --git a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
index b4f1400..d7cd88d 100644
--- a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
@@ -10,31 +10,15 @@
from crud.portfolio_projects_crud import portfolio_crud
from admin.filters.filters import ChatTypeFilter, IsAdmin
from admin.keyboards.keyboards import (
- get_inline_confirmation_keyboard,
+ get_inline_confirmation,
get_inline_keyboard,
)
-# from settings import (
-# MAIN_MENU_OPTIONS,
-# ADMIN_PORTFOLIO_OPTIONS,
-# PORTFOLIO_MENU_OPTIONS,
-# )
-
-MAIN_MENU_OPTIONS = {
- "company_bio": "Информация о компании",
- "products": "Продукты и услуги",
- "support": "Техническая поддержка",
- "portfolio": "Портфолио",
- "request_callback": "Связаться с менеджером",
-}
-
-PORTFOLIO_MENU_OPTIONS = {
- "portfolio_button": "Наше портфолио",
- "other_projects": "Посмотреть другие проекты",
-}
-
-ADMIN_PORTFOLIO_OPTIONS = {
- "change_url": "Адрес ссылки на портфолио",
-}
+
+from admin.admin_settings import (
+ MAIN_MENU_OPTIONS,
+ ADMIN_PORTFOLIO_OPTIONS,
+ PORTFOLIO_MENU_OPTIONS,
+)
portfolio_router = Router()
portfolio_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
@@ -128,7 +112,7 @@ async def confirm_delete(
)
await callback.message.edit_text(
f"Вы уверены, что хотите удалить этот проект?\n\n {portfolio_project.project_name}",
- reply_markup=await get_inline_confirmation_keyboard(
+ reply_markup=await get_inline_confirmation(
option=portfolio_project.project_name, cancel_option=PREVIOUS_MENU
),
)
@@ -170,7 +154,11 @@ async def portfolio_project_to_update(
@portfolio_router.callback_query(
UpdateProject.select,
- and_f(F.data != "Название проекта", F.data != "Адрес ссылки", F.data != PREVIOUS_MENU),
+ and_f(
+ F.data != "Название проекта",
+ F.data != "Адрес ссылки",
+ F.data != PREVIOUS_MENU,
+ ),
)
async def update_portfolio_project_choise(
callback: CallbackQuery, state: FSMContext
@@ -188,7 +176,8 @@ async def update_portfolio_project_choise(
UpdateProject.select, F.data == "Название проекта"
)
async def about_name_update(
- callback: CallbackQuery, state: FSMContext,
+ callback: CallbackQuery,
+ state: FSMContext,
):
about_name = await state.get_data()
about_name_text = about_name.get("select")
diff --git a/app/admin/handlers/admin_handlers/admin_product_handlers.py b/app/admin/handlers/admin_handlers/admin_product_handlers.py
index 2657932..70eb333 100644
--- a/app/admin/handlers/admin_handlers/admin_product_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_product_handlers.py
@@ -10,20 +10,13 @@
from crud.product_crud import product_crud
from admin.filters.filters import ChatTypeFilter, IsAdmin
from admin.keyboards.keyboards import (
- get_inline_confirmation_keyboard,
+ get_inline_confirmation,
get_inline_keyboard,
)
-# from settings import (
-# MAIN_MENU_OPTIONS,
-# )
-
-MAIN_MENU_OPTIONS = {
- "company_bio": "Информация о компании",
- "products": "Продукты и услуги",
- "support": "Техническая поддержка",
- "portfolio": "Портфолио",
- "request_callback": "Связаться с менеджером",
-}
+
+from admin.admin_settings import (
+ MAIN_MENU_OPTIONS,
+)
product_router = Router()
product_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
@@ -91,7 +84,7 @@ async def creeate_product(
await product_crud.create(data, session)
await message.answer(
"Продукт создан! Хотите добавить к нему дополнительну информацию?",
- reply_markup=await get_inline_confirmation_keyboard(
+ reply_markup=await get_inline_confirmation(
option="Да", cancel_option=PREVIOUS_MENU
),
)
@@ -160,7 +153,7 @@ async def add_media_description(message: Message, state: FSMContext):
await message.answer(
"Добавить описание к картинке?",
- reply_markup=await get_inline_confirmation_keyboard(
+ reply_markup=await get_inline_confirmation(
"Текст", cancel_option="Нет"
),
)
@@ -188,7 +181,7 @@ async def create_product_with_data(
await category_product_crud.create(data, session)
await message.answer(
"Информация добавлена! Добавить еще?",
- reply_markup=await get_inline_confirmation_keyboard(
+ reply_markup=await get_inline_confirmation(
option="Да", cancel_option=PREVIOUS_MENU
),
)
@@ -225,7 +218,7 @@ async def confirm_delete(
await callback.message.edit_text(
f"Вы уверены, что хотите удалить "
f"этот проект?\n\n {portfolio_project.title}",
- reply_markup=await get_inline_confirmation_keyboard(
+ reply_markup=await get_inline_confirmation(
option=portfolio_project.title, cancel_option=PREVIOUS_MENU
),
)
diff --git a/app/admin/handlers/user.py b/app/admin/handlers/user.py
index c0b94b0..ee5b2d6 100644
--- a/app/admin/handlers/user.py
+++ b/app/admin/handlers/user.py
@@ -7,7 +7,7 @@
from crud.category_product import category_product_crud
from admin.filters.filters import ChatTypeFilter
-from const import (
+from admin.admin_settings import (
ADMIN_BASE_BUTTONS,
PORTFOLIO_DEFAULT_DATA,
PORTFOLIO_MENU_TEXT,
@@ -111,7 +111,6 @@ async def portfolio_info(callback: CallbackQuery, session: AsyncSession):
portlio_url.url,
],
previous_menu=BASE_BUTTONS.get("main_menu"),
- is_admin=callback.from_user.id in admin_list,
admin_update_menu=callback.data,
),
)
@@ -140,7 +139,6 @@ async def main_info(
options=about_company_buttons,
previous_menu=BASE_BUTTONS.get("main_menu"),
urls=company_about_urls,
- is_admin=callback.from_user.id in admin_list,
admin_update_menu=callback.data,
),
)
@@ -187,7 +185,6 @@ async def info_faq(
reply_markup=await get_inline_keyboard(
options=question_list,
previous_menu=MAIN_MENU_OPTIONS.get("support"),
- is_admin=callback.from_user.id in admin_list,
admin_update_menu=callback.data,
),
)
@@ -215,7 +212,6 @@ async def portfolio_other_projects(
projects_names,
previous_menu=MAIN_MENU_OPTIONS.get("portfolio"),
urls=urls,
- is_admin=callback.from_user.id in admin_list,
admin_update_menu=callback.data,
),
)
@@ -238,7 +234,6 @@ async def get_products_list(
reply_markup=await get_inline_keyboard(
products,
previous_menu=BASE_BUTTONS.get("main_menu"),
- is_admin=callback.from_user.id in admin_list,
admin_update_menu=callback.data,
),
)
@@ -265,7 +260,6 @@ async def product_category(
categories_by_name,
urls=urls,
previous_menu=MAIN_MENU_OPTIONS.get("products"),
- is_admin=callback.from_user.id in admin_list,
admin_update_menu=callback.data,
),
)
diff --git a/app/admin/handlers/validators.py b/app/admin/handlers/validators.py
index 96381b6..0294f1a 100644
--- a/app/admin/handlers/validators.py
+++ b/app/admin/handlers/validators.py
@@ -1,6 +1,6 @@
import re
-from const import PHONE_NUMBER_REGEX
+from admin.admin_settings import PHONE_NUMBER_REGEX
def phone_number_validator(phone_number: int) -> bool:
diff --git a/app/admin/keyboards/keyboards.py b/app/admin/keyboards/keyboards.py
index c2f8c4e..ebf66a2 100644
--- a/app/admin/keyboards/keyboards.py
+++ b/app/admin/keyboards/keyboards.py
@@ -1,126 +1,102 @@
from aiogram.types import (
- KeyboardButton,
InlineKeyboardButton,
InlineKeyboardMarkup,
ReplyKeyboardMarkup,
+ KeyboardButton,
)
-from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
-
-
-def get_paginated_keyboard_size(items_per_page: int):
- """Вернуть кортеж вида (1, 1, ... 1, 2, 1)"""
+from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
- return (1,) * items_per_page + (2, 1)
+class InlineKeyboardManager:
+ """
+ Менеджер для создания инлайн-клавиатур.
-async def get_inline_keyboard(
- options: list[str] | str | None = None,
- callback: list[str] | str | None = None,
- previous_menu: str | None = None,
- urls: list[str] | None = None,
- size: tuple[int] = (1,),
- is_admin: bool | None = False,
- admin_update_menu: str | None = None,
-) -> InlineKeyboardMarkup:
- """Создать набор кнопок для меню раздела."""
-
- keyboard = InlineKeyboardBuilder()
+ Этот класс позволяет добавлять кнопки, включая кнопки для администраторов
+ и кнопку "Назад".
+ """
- if not callback:
- callback = options
+ def __init__(
+ self,
+ options=None,
+ callback=None,
+ urls=None,
+ size=(1,),
+ previous_menu=None,
+ admin_update_menu=None,
+ ):
+ self.options = options if options is not None else []
+ self.callback = callback if callback is not None else self.options
+ self.urls = urls if urls is not None else []
+ self.size = size
+ self.previous_menu = previous_menu
+ self.admin_update_menu = admin_update_menu
+ self.keyboard = InlineKeyboardBuilder()
- if options:
- for index, option in enumerate(options):
- keyboard.add(
+ def add_buttons(self):
+ """Добавить основные кнопки в клавиатуру."""
+ for index, option in enumerate(self.options):
+ self.keyboard.add(
InlineKeyboardButton(
text=option,
- callback_data=str(callback[index]),
+ callback_data=str(self.callback[index]),
url=(
- urls[index]
- if urls and index in range(len(urls))
+ self.urls[index]
+ if self.urls and index < len(self.urls)
else None
),
)
)
- if previous_menu:
- keyboard.add(
- InlineKeyboardButton(
- text="Назад",
- callback_data=previous_menu,
- )
- )
-
- if is_admin:
- keyboard.add(
- InlineKeyboardButton(
- text="Редактировать🔧",
- callback_data=f"{admin_update_menu}_",
- )
- )
-
- return keyboard.adjust(*size).as_markup(resize_keyboard=True)
-
-
-async def get_inline_paginated_keyboard(
- options: list[str] | str | None = None,
- callback: list[str] | str | None = None,
- previous_menu: str | None = None,
- previous_menu_text: str | None = "Назад",
- items_per_page: int = 5,
- size: tuple[int] = (1,),
- current_page: int = 1,
-) -> InlineKeyboardMarkup:
- """Создать набор кнопок для меню раздела с поддержкой пагинации."""
-
- if not callback:
- callback = options
-
- keyboard = InlineKeyboardBuilder()
-
- total_pages = 0
- total_items = len(options) if options else 0
- total_pages = (total_items + items_per_page - 1) // items_per_page
- start_index = (current_page - 1) * items_per_page
- end_index = min(start_index + items_per_page, total_items)
- current_options = options[start_index:end_index] if options else []
-
- for index, option in enumerate(current_options):
- keyboard.add(
- InlineKeyboardButton(
- text=option,
- callback_data=str(callback[index]),
- ),
- )
-
- navigation_row = []
- if total_pages > 1:
- if current_page > 1:
- navigation_row.append(
+ def add_back_button(self):
+ """Добавить кнопку 'Назад'."""
+ if self.previous_menu:
+ self.keyboard.add(
InlineKeyboardButton(
- text="◀️ Предыдущая",
- callback_data=f"page:{current_page - 1}",
+ text="Назад",
+ callback_data=self.previous_menu,
)
)
- if current_page < total_pages:
- navigation_row.append(
+ def add_admin_button(self):
+ """Добавить кнопку 'Редактировать' для администраторов."""
+ if self.admin_update_menu:
+ self.keyboard.add(
InlineKeyboardButton(
- text="Следующая ▶️",
- callback_data=f"page:{current_page + 1}",
+ text="Редактировать🔧",
+ callback_data=f"{self.admin_update_menu}_",
)
)
- if navigation_row:
- keyboard.add(*navigation_row)
- keyboard.add(
- InlineKeyboardButton(
- text=previous_menu_text,
- callback_data=previous_menu,
- )
- )
+ def create_keyboard(self) -> InlineKeyboardMarkup:
+ """Создать клавиатуру и вернуть ее."""
+ self.add_buttons()
+ self.add_back_button()
+ self.add_admin_button()
+ return self.keyboard.adjust(*self.size).as_markup(resize_keyboard=True)
+
- return keyboard.adjust(*size).as_markup(resize_keyboard=True)
+async def get_inline_keyboard(
+ options=None,
+ callback=None,
+ urls=None,
+ previous_menu=None,
+ admin_update_menu=None,
+):
+ """Создать базовую инлайн-клавиатуру.
+
+ :param options: Список названий кнопок.
+ :param callback: Список коллбек-данных для кнопок.
+ :param urls: Список URL для кнопок.
+ :param previous_menu: Коллбек-данные для кнопки "Назад".
+ :return: Объект InlineKeyboardMarkup.
+ """
+ return InlineKeyboardManager(
+ options=options,
+ callback=callback,
+ urls=urls,
+ previous_menu=previous_menu,
+ admin_update_menu=admin_update_menu,
+ ).create_keyboard()
async def get_reply_keyboard(
@@ -151,91 +127,14 @@ async def get_delete_message_keyboard() -> InlineKeyboardMarkup:
return keyboard.adjust(1).as_markup(resize_keyboard=True)
-class InlineKeyboardManager:
- def __init__(
- self,
- options=None,
- callback=None,
- urls=None,
- size=(1,),
- ):
- self.options = options if options is not None else []
- self.callback = callback if callback is not None else self.options
- self.urls = urls if urls is not None else []
- self.size = size
- self.keyboard = InlineKeyboardBuilder()
-
- def add_buttons(self):
- """Добавить основные кнопки в клавиатуру."""
- for index, option in enumerate(self.options):
- self.keyboard.add(
- InlineKeyboardButton(
- text=option,
- callback_data=str(self.callback[index]),
- url=(
- self.urls[index]
- if self.urls and index < len(self.urls)
- else None
- ),
- )
- )
-
- def add_previous_menu_button(
- self, previous_menu: str, menu_text: str = "Назад"
- ):
- """Добавить кнопку 'Назад'."""
- self.previous_menu = previous_menu
- self.keyboard.add(
- InlineKeyboardButton(
- text=menu_text,
- callback_data=previous_menu,
- )
- )
-
- def add_admin_button(self, admin_update_menu):
- """Добавить кнопку 'Редактировать' для администраторов."""
- self.keyboard.add(
- InlineKeyboardButton(
- text="Редактировать🔧",
- callback_data=f"{admin_update_menu}_",
- )
- )
-
- def create_keyboard(self) -> InlineKeyboardMarkup:
- """Создать клавиатуру и вернуть ее."""
- self.add_buttons()
- return self.keyboard.adjust(*self.size).as_markup(resize_keyboard=True)
+async def get_inline_confirmation(
+ cancel_option: str,
+ option: str = "Да",
+) -> InlineKeyboardMarkup:
+ """Кнопка для подтверждения действий."""
- async def add_extra_buttons(
- self, options: str | list[str], callback: str | list[str]
- ):
- for index, option in enumerate(options):
- self.keyboard.add(
- InlineKeyboardButton(
- text=option,
- callback_data=str(callback[index]),
- )
- )
+ keyboard = InlineKeyboardBuilder()
+ keyboard.add(InlineKeyboardButton(text="Да", callback_data=option))
+ keyboard.add(InlineKeyboardButton(text="Нет", callback_data=cancel_option))
- @staticmethod
- def get_inline_confirmation(
- cancel_option: str,
- option: str = "Да",
- ) -> InlineKeyboardMarkup:
- """Кнопка для подтверждения действий."""
-
- keyboard = InlineKeyboardBuilder()
- keyboard.add(InlineKeyboardButton(text="Да", callback_data=option))
- keyboard.add(
- InlineKeyboardButton(text="Нет", callback_data=cancel_option)
- )
-
- return keyboard.adjust(2).as_markup(resize_keyboard=True)
-
- @staticmethod
- def get_back_button(previous_menu: str) -> InlineKeyboardMarkup:
- keyboard = InlineKeyboardBuilder()
- keyboard.add(
- InlineKeyboardButton(text="Назад", callback_data=previous_menu)
- )
- return keyboard.adjust(1).as_markup(resize_keyboard=True)
+ return keyboard.adjust(2).as_markup(resize_keyboard=True)
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index eddbebe..69de2d9 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -4,7 +4,7 @@
from aiogram.types import Message
from sqlalchemy.ext.asyncio import AsyncSession
from admin.keyboards.keyboards import get_inline_keyboard
-from const import MAIN_MENU_BUTTONS
+from app.admin.admin_settings import MAIN_MENU_BUTTONS
from bot.bot_const import (
ADMIN_NEGATIVE_ANSWER, ADMIN_POSITIVE_ANSWER, START_MESSAGE
)
diff --git a/app/core/base.py b/app/core/base.py
index 12750f6..482870f 100644
--- a/app/core/base.py
+++ b/app/core/base.py
@@ -1,7 +1,7 @@
"""Импорты класса Base и всех моделей для Alembic."""
-from app.core.db import Base # noqa
-from app.models.models import ( # noqa
+from core.db import Base # noqa
+from models.models import ( # noqa
User,
ProductCategory,
CategoryType,
diff --git a/app/core/init_db.py b/app/core/init_db.py
index 5f6f59a..a08cbee 100644
--- a/app/core/init_db.py
+++ b/app/core/init_db.py
@@ -1,6 +1,6 @@
from core.db import AsyncSessionLocal
from crud.about_crud import company_info_crud
-from const import PORTFOLIO_DEFAULT_DATA
+from app.admin.admin_settings import PORTFOLIO_DEFAULT_DATA
async def add_portfolio():
diff --git a/app/core/settings.py b/app/core/settings.py
index b367caa..88e751f 100644
--- a/app/core/settings.py
+++ b/app/core/settings.py
@@ -5,13 +5,13 @@ class Settings(BaseSettings):
database_url: str
bot_token: str
telegram_chat_ids: str
- email: str
- email_password: str
- postgres_user: str
- postgres_password: str
- postgres_db: str
- db_host: str
- db_port: str
+ # email: str
+ # email_password: str
+ # postgres_user: str
+ # postgres_password: str
+ # postgres_db: str
+ # db_host: str
+ # db_port: str
class Config:
env_file = '.env'
diff --git a/app/models/models.py b/app/models/models.py
index 87ad912..8ddf6ef 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -7,7 +7,7 @@
from enum import Enum
-from app.core.base import Base
+from core.db import Base
class RoleEnum(str, Enum):
diff --git a/test.py b/test.py
index 8aaab2d..3032058 100644
--- a/test.py
+++ b/test.py
@@ -1,92 +1,239 @@
-class QuestionManager:
- def __init__(self, session: AsyncSession):
- self.session = session
+# from typing import Any
+# from app.admin.keyboards.keyboards import InlineKeyboardManager
- async def ask_question_to_delete(
- self, callback: CallbackQuery, state: FSMContext
- ):
- current_state = await state.get_state()
- await state.set_state(DeleteQuestion.question_type)
- await state.update_data(
- question_type=await set_question_type(current_state)
- )
- question_type = (await state.get_data()).get("question_type")
- question_list = await get_question_list(question_type, self.session)
- await callback.message.edit_text(
- "Какой вопрос удалить?",
- reply_markup=await get_inline_keyboard(
- question_list, previous_menu=PREVIOUS_MENU
- ),
- )
- await state.set_state(DeleteQuestion.question)
-
- async def confirm_delete_question(
- self, callback: CallbackQuery, state: FSMContext
- ):
- question = await info_crud.get_by_question_text(
- callback.data, self.session
- )
- await callback.message.edit_text(
- f"Вы уверены, что хотите удалить этот вопрос?\n\n {question.question}",
- reply_markup=await get_inline_confirmation_keyboard(
- option=question.question, cancel_option=PREVIOUS_MENU
- ),
- )
- await state.set_state(DeleteQuestion.confirm)
-
- async def delete_question(
- self, callback: CallbackQuery, state: FSMContext
- ):
- await state.clear()
- question = await info_crud.get_by_question_text(
- callback.data, self.session
- )
- await info_crud.remove(question, self.session)
- await callback.message.edit_text(
- "Вопрос удален!",
- reply_markup=await get_inline_keyboard(
- previous_menu=question.question_type
- ),
- )
-
-
-# Использование в роутере
-@info_router.callback_query(
- or_f(SectionState.faq, SectionState.troubleshooting), F.data == "Удалить"
-)
-async def handle_delete_question(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- manager = QuestionManager(session)
- await manager.ask_question_to_delete(callback, state)
+# from aiogram.types import (
+# InlineKeyboardButton,
+# InlineKeyboardMarkup,
+# )
+# from aiogram.utils.keyboard import InlineKeyboardBuilder
+# keyboard = InlineKeyboardManager()
+# keyboard.add_previous_menu_button(previous_menu="NAZAD")
-@info_router.callback_query(DeleteQuestion.question, F.data)
-async def handle_confirm_delete_question(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- manager = QuestionManager(session)
- await manager.confirm_delete_question(callback, state)
+# test = keyboard.create_keyboard()
-@info_router.callback_query(DeleteQuestion.confirm, F.data != PREVIOUS_MENU)
-async def handle_delete_question(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- manager = QuestionManager(session)
- await manager.delete_question(callback, state)
+# # keyboard.add_admin_button("test")
+
+# keyboard.add_extra_buttons(["test"])
+# test2 = keyboard.create_keyboard()
+# # print(vars(keyboard))
+# keyboard.add_extra_buttons(["extra_test"])
+# test3 = keyboard.create_keyboard()
+
+
+def get_buttons_from_keyboard(keyboard):
+ button_list = []
+ for row in keyboard.inline_keyboard:
+ for button in row:
+ button_list.append((button.text, button.callback_data))
+ return button_list
+
+
+# keyboard.update_buttons(["updated_list"])
+# test4 = keyboard.create_keyboard()
+# print(get_buttons_from_keyboard(test))
+# print(get_buttons_from_keyboard(test2))
+# print(get_buttons_from_keyboard(test3))
+# print(get_buttons_from_keyboard(test4))
+
+
+# class InlineKeyboardManager:
+# def __init__(
+# self,
+# options=None,
+# callback=None,
+# urls=None,
+# size=(1,),
+# previous_menu=None,
+# admin_update_menu=None,
+# ):
+# self.options = options if options is not None else []
+# self.callback = callback if callback is not None else self.options
+# self.urls = urls if urls is not None else []
+# self.size = size
+# self.previous_menu = previous_menu
+# self.admin_update_menu = admin_update_menu
+# self.keyboard = InlineKeyboardBuilder()
+
+# def add_buttons(self):
+# """Добавить основные кнопки в клавиатуру."""
+# for index, option in enumerate(self.options):
+# self.keyboard.add(
+# InlineKeyboardButton(
+# text=option,
+# callback_data=str(self.callback[index]),
+# url=(
+# self.urls[index]
+# if self.urls and index < len(self.urls)
+# else None
+# ),
+# )
+# )
+
+# def add_back_button(self):
+# """Добавить кнопку 'Назад'."""
+# if self.previous_menu:
+# self.keyboard.add(
+# InlineKeyboardButton(
+# text="Назад",
+# callback_data=self.previous_menu,
+# )
+# )
+
+# def add_admin_button(self):
+# """Добавить кнопку 'Редактировать' для администраторов."""
+# self.keyboard.add(
+# InlineKeyboardButton(
+# text="Редактировать🔧",
+# callback_data=f"{self.admin_update_menu}_",
+# )
+# )
+
+# def create_keyboard(self) -> InlineKeyboardMarkup:
+# """Создать клавиатуру и вернуть ее.
+
+# :return: Объект InlineKeyboardMarkup с добавленными кнопками.
+# """
+# self.add_buttons()
+# if self.previous_menu:
+# self.add_back_button()
+# if self.admin_update_menu:
+# self.add_admin_button
+# return self.keyboard.adjust(*self.size).as_markup(resize_keyboard=True)
+
+
+# def get_base_inline_keyboard(
+# options=None,
+# callback=None,
+# urls=None,
+# previous_menu=None,
+# ):
+# return InlineKeyboardManager(
+# options=options,
+# callback=callback,
+# urls=urls,
+# previous_menu=previous_menu,
+# ).create_keyboard()
+
+
+# def get_admin_keyboard(
+# admin_update_menu,
+# options=None,
+# callback=None,
+# urls=None,
+# previous_menu=None,
+# ):
+# return InlineKeyboardManager(
+# options=options,
+# callback=callback,
+# urls=urls,
+# previous_menu=previous_menu,
+# admin_update_menu=admin_update_menu,
+# ).create_keyboard()
+
+
+# class AdminInlineKeyboard(BaseInlineKeyboardManager):
+# """
+# Класс для управления инлайн-клавиатурами с администраторскими функциями.
+# Этот класс наследует базовый класс и добавляет возможность
+# добавления кнопки "Редактировать" для администраторов.
+# """
-from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
+# def __init__(
+# self,
+# admin_update_menu: str,
+# *args,
+# **kwargs,
+# ):
+# self.admin_update_menu = admin_update_menu
+# super().__init__(*args, **kwargs)
+
+# def add_admin_button(self):
+# """Добавить кнопку 'Редактировать' для администраторов."""
+# self.keyboard.add(
+# InlineKeyboardButton(
+# text="Редактировать🔧",
+# callback_data=f"{self.admin_update_menu}_",
+# )
+# )
+
+# def create_keyboard(self) -> InlineKeyboardMarkup:
+# """Создать клавиатуру и вернуть ее.
+
+# :return: Объект InlineKeyboardMarkup с добавленными кнопками,
+# включая кнопку "Редактировать".
+# """
+# super().create_keyboard()
+# self.add_admin_button()
+# return self.keyboard
+
+
+# def get_base_inline_keyboard(options=None, callback=None, urls=None):
+# """Создать базовую инлайн-клавиатуру.
+
+# :param options: Список названий кнопок.
+# :param callback: Список коллбек-данных для кнопок.
+# :param urls: Список URL для кнопок.
+# :return: Объект InlineKeyboardMarkup.
+# """
+# return BaseInlineKeyboardManager(
+# options, callback=callback, urls=urls
+# ).create_keyboard()
+
+
+# def get_admin_inline_kb(
+# admin_update_menu,
+# options=None,
+# callback=None,
+# urls=None,
+# previous_menu=None,
+# ):
+# """Создать инлайн-клавиатуру для администраторов.
+
+# :param admin_update_menu: Коллбек-данные для кнопки "Редактировать".
+# :param options: Список названий кнопок.
+# :param callback: Список коллбек-данных для кнопок.
+# :param urls: Список URL для кнопок.
+# :param previous_menu: Коллбек-данные для кнопки "Назад".
+# :return: Объект InlineKeyboardMarkup.
+# """
+# return AdminInlineKeyboard(
+# admin_update_menu=admin_update_menu,
+# options=options,
+# callback=callback,
+# urls=urls,
+# previous_menu=previous_menu,
+# ).create_keyboard()
+
+from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
class InlineKeyboardManager:
- def __init__(self, options=None, callback=None, urls=None, size=(1,)):
+ """
+ Менеджер для создания инлайн-клавиатур.
+
+ Этот класс позволяет добавлять кнопки, включая кнопки для администраторов
+ и кнопку "Назад".
+ """
+
+ def __init__(
+ self,
+ options=None,
+ callback=None,
+ urls=None,
+ size=(1,),
+ previous_menu=None,
+ admin_update_menu=None,
+ ):
self.options = options if options is not None else []
self.callback = callback if callback is not None else self.options
self.urls = urls if urls is not None else []
self.size = size
+ self.previous_menu = previous_menu
+ self.admin_update_menu = admin_update_menu
self.keyboard = InlineKeyboardBuilder()
def add_buttons(self):
@@ -104,47 +251,76 @@ def add_buttons(self):
)
)
- def add_previous_menu_button(self, previous_menu):
+ def add_back_button(self):
"""Добавить кнопку 'Назад'."""
- self.keyboard.add(
- InlineKeyboardButton(
- text="Назад",
- callback_data=previous_menu,
+ if self.previous_menu:
+ self.keyboard.add(
+ InlineKeyboardButton(
+ text="Назад",
+ callback_data=self.previous_menu,
+ )
)
- )
- def add_admin_button(self, admin_update_menu):
+ def add_admin_button(self):
"""Добавить кнопку 'Редактировать' для администраторов."""
- self.keyboard.add(
- InlineKeyboardButton(
- text="Редактировать🔧",
- callback_data=f"{admin_update_menu}_",
+ if self.admin_update_menu:
+ self.keyboard.add(
+ InlineKeyboardButton(
+ text="Редактировать🔧",
+ callback_data=f"{self.admin_update_menu}_",
+ )
)
- )
def create_keyboard(self) -> InlineKeyboardMarkup:
"""Создать клавиатуру и вернуть ее."""
self.add_buttons()
+ self.add_back_button()
+ self.add_admin_button() # Исправлено: добавлены скобки
return self.keyboard.adjust(*self.size).as_markup(resize_keyboard=True)
-# Пример использования
-async def get_inline_keyboard(options, callback=None, urls=None, size=(1,)):
- manager = InlineKeyboardManager(options, callback, urls, size)
- return manager.create_keyboard()
-
-
-# Использование с добавлением дополнительных кнопок
-async def get_custom_keyboard(
- options, previous_menu=None, is_admin=False, admin_update_menu=None
+def get_base_inline_keyboard(
+ options=None,
+ callback=None,
+ urls=None,
+ previous_menu=None,
):
- manager = InlineKeyboardManager(options)
- manager.add_buttons()
+ """Создать базовую инлайн-клавиатуру.
- if previous_menu:
- manager.add_previous_menu_button(previous_menu)
+ :param options: Список названий кнопок.
+ :param callback: Список коллбек-данных для кнопок.
+ :param urls: Список URL для кнопок.
+ :param previous_menu: Коллбек-данные для кнопки "Назад".
+ :return: Объект InlineKeyboardMarkup.
+ """
+ return InlineKeyboardManager(
+ options=options,
+ callback=callback,
+ urls=urls,
+ previous_menu=previous_menu,
+ ).create_keyboard()
- if is_admin and admin_update_menu:
- manager.add_admin_button(admin_update_menu)
- return manager.create_keyboard()
+def get_admin_keyboard(
+ admin_update_menu,
+ options=None,
+ callback=None,
+ urls=None,
+ previous_menu=None,
+):
+ """Создать инлайн-клавиатуру для администраторов.
+
+ :param admin_update_menu: Коллбек-данные для кнопки "Редактировать".
+ :param options: Список названий кнопок.
+ :param callback: Список коллбек-данных для кнопок.
+ :param urls: Список URL для кнопок.
+ :param previous_menu: Коллбек-данные для кнопки "Назад".
+ :return: Объект InlineKeyboardMarkup.
+ """
+ return InlineKeyboardManager(
+ options=options,
+ callback=callback,
+ urls=urls,
+ previous_menu=previous_menu,
+ admin_update_menu=admin_update_menu,
+ ).create_keyboard()
From b744ae3139739506012c58a761ab8d2186812717 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Sat, 12 Oct 2024 17:55:13 +0300
Subject: [PATCH 34/75] add compose prod
---
DockerFile | 1 +
docker-compose.yml | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/DockerFile b/DockerFile
index fa43760..0c5f174 100644
--- a/DockerFile
+++ b/DockerFile
@@ -23,6 +23,7 @@ RUN poetry install --no-root
COPY . .
# Создание миграций
+#WORKDIR /app/app
#RUN poetry run alembic stamp head
#RUN poetry run alembic revision --autogenerate -m "compose commit"
#RUN poetry run alembic upgrade head
diff --git a/docker-compose.yml b/docker-compose.yml
index 0898673..9e51234 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,7 +15,8 @@ services:
depends_on:
- db
container_name: scid-bot
- build: .
+ #build: . # раскомментить для работы локально
+ image: greenvibe/scid_bot_3 # закомментить для работы локально
command: bash -c "cd app && poetry run alembic stamp head && poetry run alembic revision --autogenerate && poetry run alembic upgrade head && poetry run python main.py"
env_file: .env
From 898d6bb174f52b2f84915a17f6e02741efad08f6 Mon Sep 17 00:00:00 2001
From: Vladimir Mironov
Date: Sun, 13 Oct 2024 14:16:06 +0300
Subject: [PATCH 35/75] docker_compose
---
docker-compose.yml | 19 +++----------------
1 file changed, 3 insertions(+), 16 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 9e51234..dc0c989 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3.3'
-
volumes:
pg_data:
@@ -7,26 +5,15 @@ services:
db:
container_name: scid-db
image: postgres:13
+ restart: on-failure
env_file: .env
volumes:
- pg_data:/var/lib/postgresql/data
-
bot:
depends_on:
- db
container_name: scid-bot
- #build: . # раскомментить для работы локально
- image: greenvibe/scid_bot_3 # закомментить для работы локально
+ # build: . # раскомментить для работы локально
+ image: greenvibe/scid_bot_3
command: bash -c "cd app && poetry run alembic stamp head && poetry run alembic revision --autogenerate && poetry run alembic upgrade head && poetry run python main.py"
env_file: .env
-
- gateway:
- depends_on:
- - bot
- container_name: scid-gateway
- image: nginx:1.22.1-alpine
- env_file: .env
- volumes:
- - ./nginx.conf:/etc/nginx/conf.d/default.conf
- ports:
- - 80:80
From 8dcc34a605b1daa745c89d24345e518dbed35f27 Mon Sep 17 00:00:00 2001
From: Vladimir Mironov
Date: Sun, 13 Oct 2024 14:21:26 +0300
Subject: [PATCH 36/75] ci_cd
---
.github/workflows/main.yml | 61 ++++++++++++++++++++++++++++++++++++++
1 file changed, 61 insertions(+)
create mode 100644 .github/workflows/main.yml
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..1f67bb2
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,61 @@
+name: SCID 3 Bot Workflow
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - dev
+jobs:
+ linter:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.12'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install flake8
+ - name: Test with flake8
+ run: python -m flake8 app/
+ deploy:
+ runs-on: ubuntu-latest
+ needs: linter
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v3
+ - name: Copy docker-compose.yml via ssh
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.HOST }}
+ username: ${{ secrets.USER }}
+ key: ${{ secrets.SSH_KEY }}
+ passphrase: ${{ secrets.SSH_PASSPHRASE }}
+ source: "docker-compose.yml"
+ target: "scid3"
+ - name: Executing remote ssh commands to deploy
+ uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.HOST }}
+ username: ${{ secrets.USER }}
+ key: ${{ secrets.SSH_KEY }}
+ passphrase: ${{ secrets.SSH_PASSPHRASE }}
+ script: |
+ cd scid3
+ sudo docker compose -f docker-compose.yml pull
+ sudo docker compose -f docker-compose.yml down
+ sudo docker compose -f docker-compose.yml up -d
+ send_message:
+ runs-on: ubuntu-latest
+ needs: deploy
+ steps:
+ - name: Send message
+ uses: appleboy/telegram-action@master
+ with:
+ to: ${{ secrets.TELEGRAM_TO }}
+ token: ${{ secrets.TELEGRAM_TOKEN }}
+ message: Деплой успешно выполнен!
From 565ced360dcc2d06fd73ad125a3ce79b705e480c Mon Sep 17 00:00:00 2001
From: ikhit
Date: Sun, 13 Oct 2024 16:22:28 +0300
Subject: [PATCH 37/75] finish info managers and router, start refactor product
and category
---
alembic.ini | 4 +-
app/admin/admin_managers/__init__.py | 10 +-
app/admin/admin_managers/base_manager.py | 15 -
app/admin/admin_managers/create_manager.py | 13 +-
app/admin/admin_managers/delete_manager.py | 8 +-
app/admin/admin_managers/question_manager.py | 207 +++++++++++
app/admin/admin_managers/update_manager.py | 118 +++++--
app/admin/admin_settings.py | 8 +-
app/admin/handlers/admin_handlers/admin.py | 12 +-
.../admin_about_company_handlers.py | 30 +-
.../admin_handlers/admin_info_handlers.py | 230 ++++--------
.../admin_portfolio_handlers.py | 222 ++++--------
.../admin_handlers/admin_product_handlers.py | 70 ++--
app/admin/handlers/user.py | 11 +-
{alembic => app/alembic}/README | 0
{alembic => app/alembic}/env.py | 2 +-
{alembic => app/alembic}/script.py.mako | 0
...d4_change_all_model_name_fields_to_name.py | 0
..._add_description_fields_to_product_and_.py | 34 ++
app/bot/handlers.py | 2 +-
app/core/init_db.py | 21 +-
app/crud/about_crud.py | 13 -
app/crud/portfolio_projects_crud.py | 24 +-
app/crud/projects.py | 2 +-
app/crud/user_crud.py | 2 +-
app/main.py | 3 +-
app/models/models.py | 22 +-
app/scripts_for_db.py | 4 +-
app/set_admin.py | 18 -
test.py | 330 +-----------------
30 files changed, 611 insertions(+), 824 deletions(-)
create mode 100644 app/admin/admin_managers/question_manager.py
rename {alembic => app/alembic}/README (100%)
rename {alembic => app/alembic}/env.py (98%)
rename {alembic => app/alembic}/script.py.mako (100%)
rename {alembic => app/alembic}/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py (100%)
create mode 100644 app/alembic/versions/4c3a4964ff61_add_description_fields_to_product_and_.py
delete mode 100644 app/set_admin.py
diff --git a/alembic.ini b/alembic.ini
index d4ab1d7..d8440a8 100644
--- a/alembic.ini
+++ b/alembic.ini
@@ -3,7 +3,7 @@
[alembic]
# path to migration scripts.
# Use forward slashes (/) also on windows to provide an os agnostic path
-script_location = alembic
+script_location = app/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
@@ -11,7 +11,7 @@ script_location = alembic
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
-prepend_sys_path = .
+prepend_sys_path = app
# timezone to use when rendering the date within the migration file
# as well as the filename.
diff --git a/app/admin/admin_managers/__init__.py b/app/admin/admin_managers/__init__.py
index f1d8c1a..45e9fab 100644
--- a/app/admin/admin_managers/__init__.py
+++ b/app/admin/admin_managers/__init__.py
@@ -1,3 +1,11 @@
from .delete_manager import DeleteManager, DeleteState # noqa
from .create_manager import CreateManager, CreateState # noqa
-from .update_manager import UpdateManager, UpdateState # noqa
+from .update_manager import UpdateManager, UpdatePortfolio, UpdateState # noqa
+from .question_manager import ( # noqa
+ QuestionCreateManager,
+ QuestionUpdateManager,
+ CreateQuestionStates,
+ UpdateQuestionStates,
+ DeleteQuestionStates,
+ QuestionDeleteManager,
+)
diff --git a/app/admin/admin_managers/base_manager.py b/app/admin/admin_managers/base_manager.py
index 8cc12d1..45b18e3 100644
--- a/app/admin/admin_managers/base_manager.py
+++ b/app/admin/admin_managers/base_manager.py
@@ -1,23 +1,8 @@
from abc import ABC
-from aiogram.fsm.state import State, StatesGroup
-
from crud.base_crud import CRUDBase
-class CreateUpdateState(StatesGroup):
- """
- Базовый класс для машинных состояний при
- добавлении или обновлении данных в БД.
- """
-
- select = State()
- name = State()
- url = State()
- text = State()
- media = State()
-
-
class BaseAdminManager(ABC):
"""
Базовый абстрактный класс для менеджеров администратора.
diff --git a/app/admin/admin_managers/create_manager.py b/app/admin/admin_managers/create_manager.py
index 02089a2..2b9907c 100644
--- a/app/admin/admin_managers/create_manager.py
+++ b/app/admin/admin_managers/create_manager.py
@@ -18,9 +18,10 @@ class CreateState(StatesGroup):
select = State()
name = State()
url = State()
- text = State()
+ description = State()
media = State()
+
class CreateManager(BaseAdminManager):
"""
Менеджер для управления процессом создания объектов в базе данных.
@@ -121,7 +122,7 @@ async def add_obj_url(self, message: Message, state: FSMContext):
next_state=CreateState.url,
)
- async def add_obj_text(self, message: Message, state: FSMContext):
+ async def add_obj_description(self, message: Message, state: FSMContext):
"""
Добавить текст к объекту и перейти в
следующее машинное состояние.
@@ -131,7 +132,7 @@ async def add_obj_text(self, message: Message, state: FSMContext):
message,
message_text,
state,
- next_state=CreateState.text,
+ next_state=CreateState.description,
)
async def add_obj_media(self, message: Message, state: FSMContext):
@@ -155,12 +156,12 @@ async def add_obj_to_db(
current_state = await state.get_state()
if current_state == CreateState.url.state:
await state.update_data(url=message.text)
- elif current_state == CreateState.text.state:
- await state.update_data(text=message.text)
+ elif current_state == CreateState.description.state:
+ await state.update_data(description=message.text)
elif current_state == CreateState.media.state:
await state.update_data(
media=message.photo[-1].file_id,
- text=message.caption,
+ description=message.caption,
)
data = await state.get_data()
diff --git a/app/admin/admin_managers/delete_manager.py b/app/admin/admin_managers/delete_manager.py
index 802b764..186e071 100644
--- a/app/admin/admin_managers/delete_manager.py
+++ b/app/admin/admin_managers/delete_manager.py
@@ -8,7 +8,6 @@
get_inline_confirmation,
get_inline_keyboard,
)
-from models.models import Info
class DeleteState(StatesGroup):
@@ -75,13 +74,8 @@ async def confirm_delete(
self.obj_to_delete = await self.model_crud.get_by_string(
callback.data, session
)
- obj_data = (
- self.obj_to_delete.question
- if isinstance(self.obj_to_delete, Info)
- else self.obj_to_delete.name
- )
await callback.message.edit_text(
- f"Вы уверены, что хотите удалить эти данные?\n\n {obj_data}",
+ f"Вы уверены, что хотите удалить эти данные?\n\n {self.obj_to_delete.name}",
reply_markup=await get_inline_confirmation(
cancel_option=self.back_option
),
diff --git a/app/admin/admin_managers/question_manager.py b/app/admin/admin_managers/question_manager.py
new file mode 100644
index 0000000..630d5d6
--- /dev/null
+++ b/app/admin/admin_managers/question_manager.py
@@ -0,0 +1,207 @@
+from abc import ABC
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from aiogram.types import CallbackQuery, Message
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from admin.keyboards.keyboards import (
+ get_inline_confirmation,
+ get_inline_keyboard,
+)
+from admin.admin_settings import ADMIN_QUESTION_BUTTONS, SUPPORT_OPTIONS
+from crud.info_crud import info_crud
+
+
+class CreateQuestionStates(StatesGroup):
+ question = State()
+ answer = State()
+
+
+class UpdateQuestionStates(StatesGroup):
+ select = State()
+ question = State()
+ answer = State()
+
+
+class DeleteQuestionStates(StatesGroup):
+ select = State()
+ confirm = State()
+
+
+class QuestionBaseManager(ABC):
+
+ async def set_question_type(self, state: FSMContext):
+ state = await state.get_state()
+ self.question_type = state.split(":")[-1]
+ self.back_option = SUPPORT_OPTIONS.get(self.question_type)
+ return self.back_option
+
+
+class QuestionUpdateDeleteBase(QuestionBaseManager, ABC):
+ async def get_question_list(self, session: AsyncSession):
+ return [
+ question.question
+ for question in await info_crud.get_all_questions_by_type(
+ self.question_type, session
+ )
+ ]
+
+ async def select_question(
+ self,
+ callback: CallbackQuery,
+ state: FSMContext,
+ next_state: State,
+ session: AsyncSession,
+ ):
+ self.question_type = await self.set_question_type(state)
+ questions = await self.get_question_list(session)
+ await callback.message.edit_text(
+ "Выберте вопрос:",
+ reply_markup=await get_inline_keyboard(
+ questions, previous_menu=self.back_option
+ ),
+ )
+ await state.set_state(next_state)
+
+
+class QuestionCreateManager(QuestionBaseManager):
+ async def add_question_text(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ """
+ Добавить текст вопроса и перейти в
+ следующее машинное состояние.
+ """
+ await state.update_data(
+ question_type=await self.set_question_type(state)
+ )
+ await callback.message.answer(
+ "Введите текст вопроса:",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.set_state(CreateQuestionStates.question)
+
+ async def add_answer_text(self, message: Message, state: FSMContext):
+ """
+ Добавить ответ и перейти в
+ следующее машинное состояние.
+ """
+ await state.update_data(question=message.text)
+ await message.answer(
+ "Введите ответ на этот вопрос:",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.set_state(CreateQuestionStates.answer)
+
+ async def add_question_to_db(
+ self,
+ message: Message,
+ state: FSMContext,
+ session: AsyncSession,
+ ):
+ """Добавить вопрос в БД и сбросить машинное состояние."""
+ try:
+ await state.update_data(answer=message.text)
+ data = await state.get_data()
+ await info_crud.create(data, session=session)
+ await message.answer(
+ "Вопрос добавлен!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.clear()
+ except Exception as e:
+ await message.answer(f"Произошла ошибка: {e}")
+
+
+class QuestionUpdateManager(QuestionUpdateDeleteBase):
+ async def update_data_type(
+ self, callback: CallbackQuery, session: AsyncSession
+ ):
+ self.question = await info_crud.get_by_string(callback.data, session)
+ await callback.message.edit_text(
+ "Что отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ ADMIN_QUESTION_BUTTONS, previous_menu=self.back_option
+ ),
+ )
+
+ async def update_question(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ await callback.message.edit_text(
+ f"Текущий текст вопроса: \n\n {self.question.question}\n\n Введите новый текст:",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.set_state(UpdateQuestionStates.question)
+
+ async def update_answer(self, callback: CallbackQuery, state: FSMContext):
+ await callback.message.edit_text(
+ f"Текущий текст ответа: {self.question.answer}\n\n Введите новый текст:",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.question_type
+ ),
+ )
+ await state.set_state(UpdateQuestionStates.answer)
+
+ async def update_question_in_db(
+ self, message: Message, state: FSMContext, session: AsyncSession
+ ):
+ current_state = await state.get_state()
+ if current_state == UpdateQuestionStates.question.state:
+ await state.update_data(question=message.text)
+ elif current_state == UpdateQuestionStates.answer.state:
+ await state.update_data(answer=message.text)
+ data = await state.get_data()
+ await info_crud.update(self.question, data, session)
+ await message.answer(
+ "Данные обновлены!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.clear()
+
+
+class QuestionDeleteManager(QuestionUpdateDeleteBase):
+
+ async def confirm_delete(
+ self,
+ callback: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
+ ) -> None:
+ self.question = await info_crud.get_by_string(callback.data, session)
+ await callback.message.edit_text(
+ f"Вы уверены, что хотите удалить этот вопрос'?\n\n {self.question.question}",
+ reply_markup=await get_inline_confirmation(
+ cancel_option=self.back_option
+ ),
+ ),
+ await state.set_state(DeleteQuestionStates.confirm)
+
+ async def delete_question(
+ self,
+ callback: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
+ ) -> None:
+ """Удалить вопрос из БД."""
+ try:
+ await info_crud.remove(self.question, session)
+ await callback.message.edit_text(
+ "Вопрос удален!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.clear()
+ except Exception as e:
+ await callback.message.answer(f"Произошла ошибка: {e}")
diff --git a/app/admin/admin_managers/update_manager.py b/app/admin/admin_managers/update_manager.py
index b879ae3..4ed1734 100644
--- a/app/admin/admin_managers/update_manager.py
+++ b/app/admin/admin_managers/update_manager.py
@@ -2,6 +2,7 @@
from aiogram.types import CallbackQuery, Message
from aiogram.fsm.state import State, StatesGroup
from sqlalchemy.ext.asyncio import AsyncSession
+from crud.portfolio_projects_crud import portfolio_crud
from .base_manager import (
BaseAdminManager,
@@ -18,8 +19,9 @@ class UpdateState(StatesGroup):
select = State()
name = State()
url = State()
- text = State()
+ description = State()
media = State()
+ portolio = State()
class UpdateManager(BaseAdminManager):
@@ -120,12 +122,14 @@ async def change_obj_content(
"Введите новый:"
)
await state.set_state(UpdateState.url)
- elif "text" in obj_fields and self.obj_to_update.text:
+ elif (
+ "description" in obj_fields and self.obj_to_update.description
+ ):
message_text = (
- f"Текущий текст: \n\n {self.obj_to_update.text} \n\n"
+ f"Текущий текст: \n\n {self.obj_to_update.description} \n\n"
"Введите новый:"
)
- await state.set_state(UpdateState.text)
+ await state.set_state(UpdateState.description)
await callback.message.edit_text(
message_text,
reply_markup=await get_inline_keyboard(
@@ -136,7 +140,7 @@ async def change_obj_content(
await callback.message.answer("Текущая картинка:")
await callback.message.answer_photo(
photo=self.obj_to_update.media,
- caption=self.obj_to_update.text,
+ caption=self.obj_to_update.description,
)
await callback.message.answer(
"Добавьте новую картинку и описание",
@@ -150,29 +154,85 @@ async def update_obj_in_db(
self, message: Message, state: FSMContext, session: AsyncSession
):
"""Внести изменения объекта в БД."""
- try:
- current_state = await state.get_state()
- if current_state == UpdateState.name.state:
- await state.update_data(name=message.text)
- elif current_state == UpdateState.url.state:
- await state.update_data(url=message.text)
- elif current_state == UpdateState.text.state:
- await state.update_data(text=message.text)
- elif current_state == UpdateState.media.state:
- await state.update_data(
- media=message.photo[-1].file_id,
- text=message.caption,
- )
-
- data = await state.get_data()
- await self.model_crud.update(self.obj_to_update, data, session)
- await message.answer(
- "Данные обновлены!",
- reply_markup=await get_inline_keyboard(
- previous_menu=self.back_option
- ),
+ current_state = await state.get_state()
+ if current_state == UpdateState.name.state:
+ await state.update_data(name=message.text)
+ elif current_state == UpdateState.url.state:
+ await state.update_data(url=message.text)
+ elif current_state == UpdateState.description.state:
+ await state.update_data(description=message.text)
+ elif current_state == UpdateState.media.state:
+ await state.update_data(
+ media=message.photo[-1].file_id,
+ description=message.caption,
)
- await state.clear()
- except Exception as e:
- await message.answer(f"Произошла ошибка: {e}")
+
+ data = await state.get_data()
+ await self.model_crud.update(self.obj_to_update, data, session)
+
+ await message.answer(
+ "Данные обновлены!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.clear()
+
+
+class UpdatePortfolio:
+ """
+ Менеджер для редактирования ссылки основного портфолио в базе данных.
+
+ Attributes:
+ back_option (str): Данные для возврата в меню.
+
+ Methods:
+ update_main_portfolio_url(
+ message: Message,
+ state: FSMContext,
+ session: AsyncSession) -> None:
+ Позволяет пользователю изменить URL основного портфолио.
+ update_obj_in_db(
+ message: Message,
+ state: FSMContext,
+ session: AsyncSession) -> None:
+ Вносит изменения в объект в БД и сбрасывает состояние.
+ """
+
+ def __init__(self, back_option: str) -> None:
+ self.back_option = back_option
+
+ async def update_main_portfolio_url(
+ self, callback: CallbackQuery, state: FSMContext, session: AsyncSession
+ ):
+ """Изменить URL основного портфолио."""
+ self.obj_to_update = await portfolio_crud.get_portfolio(session)
+ message_text = (
+ f"Текущий адрес ссылки: \n\n {self.obj_to_update.url} \n\n"
+ "Введите новый:"
+ )
+ await state.set_state(UpdateState.portolio)
+ await callback.message.edit_text(
+ message_text,
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+
+ async def update_obj_in_db(
+ self, message: Message, state: FSMContext, session: AsyncSession
+ ):
+ """Внести изменения объекта в БД."""
+
+ await state.update_data(url=message.text)
+ data = await state.get_data()
+ await portfolio_crud.update(self.obj_to_update, data, session)
+
+ await message.answer(
+ "Данные обновлены!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.clear()
diff --git a/app/admin/admin_settings.py b/app/admin/admin_settings.py
index 8a434ec..0be946e 100644
--- a/app/admin/admin_settings.py
+++ b/app/admin/admin_settings.py
@@ -48,6 +48,9 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
"media": "Картинка",
}
ADMIN_CONTENT_BUTTONS = get_buttons(ADMIN_CONTENT_OPTIONS)
+
+ADMIN_QUESTION_OPTIONS = {"question": "Вопрос", "answer": "Ответ"}
+ADMIN_QUESTION_BUTTONS = get_buttons(ADMIN_QUESTION_OPTIONS)
# Кнопки экранной клавиатуры
BASE_BUTTONS = {
"main_menu": "Главное меню",
@@ -70,12 +73,13 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
# Техподдержка - кнопки и текст
SUPPORT_MENU_TEXT = "Какой вид поддержки Вам нужен?"
SUPPORT_OPTIONS = {
- "faq": "Общие вопросы",
- "troubleshooting": "Проблемы с продуктами",
+ "general_questions": "Общие вопросы",
+ "problems_with_products": "Проблемы с продуктами",
"callback_request": "Запрос на обратный звонок",
}
SUPPROT_MENU_BUTTONS = get_buttons(SUPPORT_OPTIONS)
+
# Информация о компании - кнопки и текст
COMPANY_ABOUT = "Вот несколько вариантов информации о нашей компании. Что именно вас интересует?"
diff --git a/app/admin/handlers/admin_handlers/admin.py b/app/admin/handlers/admin_handlers/admin.py
index 9ec44c9..95afd2d 100644
--- a/app/admin/handlers/admin_handlers/admin.py
+++ b/app/admin/handlers/admin_handlers/admin.py
@@ -39,8 +39,8 @@ class FeedbackState(StatesGroup):
class SectionState(StatesGroup):
"""State для определения раздела, в который вносятся измения."""
- faq = State()
- troubleshooting = State()
+ general_questions = State()
+ problems_with_products = State()
portfolio = State()
other_projects = State()
about = State()
@@ -52,10 +52,10 @@ class SectionState(StatesGroup):
def get_condition(cls, menu_text: str):
"""Выбрать категорию для раздела."""
- if menu_text == SUPPORT_OPTIONS.get("faq"):
- return cls.faq
- elif menu_text == SUPPORT_OPTIONS.get("troubleshooting"):
- return cls.troubleshooting
+ if menu_text == SUPPORT_OPTIONS.get("general_questions"):
+ return cls.general_questions
+ elif menu_text == SUPPORT_OPTIONS.get("problems_with_products"):
+ return cls.problems_with_products
elif menu_text == SUPPORT_OPTIONS.get("callback_request"):
return cls.callback_request
elif menu_text == MAIN_MENU_OPTIONS.get("company_bio"):
diff --git a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
index bdce380..09014db 100644
--- a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
@@ -27,23 +27,23 @@
about_router = Router()
about_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
-create_manager = CreateManager(company_info_crud, PREVIOUS_MENU)
-delete_manager = DeleteManager(company_info_crud, PREVIOUS_MENU)
-update_manager = UpdateManager(company_info_crud, PREVIOUS_MENU)
+about_create_manager = CreateManager(company_info_crud, PREVIOUS_MENU)
+about_delete_manager = DeleteManager(company_info_crud, PREVIOUS_MENU)
+about_update_manager = UpdateManager(company_info_crud, PREVIOUS_MENU)
@about_router.callback_query(
SectionState.about, F.data == ADMIN_BASE_OPTIONS.get("create")
)
async def create_about_info(callback: CallbackQuery, state: FSMContext):
- """Запустить процесс создания новой информации о разделе."""
- await create_manager.add_obj_name(callback, state)
+ """Запустить процесс создания новой информации о компании."""
+ await about_create_manager.add_obj_name(callback, state)
@about_router.message(CreateState.name, F.text)
async def add_info_name(message: Message, state: FSMContext):
"""Сохранить имя нового объекта в состояние."""
- await create_manager.add_obj_url(message, state)
+ await about_create_manager.add_obj_url(message, state)
@about_router.message(CreateState.url, F.text)
@@ -51,7 +51,7 @@ async def add_about_data(
message: Message, state: FSMContext, session: AsyncSession
):
"""Сохранить URL нового объекта в базу данных."""
- await create_manager.add_obj_to_db(message, state, session)
+ await about_create_manager.add_obj_to_db(message, state, session)
@about_router.callback_query(
@@ -61,7 +61,7 @@ async def about_info_to_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Запустить процесс выбора объекта для удаления."""
- await delete_manager.select_obj_to_delete(callback, state, session)
+ await about_delete_manager.select_obj_to_delete(callback, state, session)
@about_router.callback_query(DeleteState.select, F.data != PREVIOUS_MENU)
@@ -69,7 +69,7 @@ async def confirm_delete_info(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Подтведить удаление выбранного объекта."""
- await delete_manager.confirm_delete(callback, state, session)
+ await about_delete_manager.confirm_delete(callback, state, session)
@about_router.callback_query(DeleteState.confirm, F.data != PREVIOUS_MENU)
@@ -77,7 +77,7 @@ async def delete_about_info(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Удалить выбранный объект из базы данных."""
- await delete_manager.delete_obj(callback, state, session)
+ await about_delete_manager.delete_obj(callback, state, session)
@about_router.callback_query(
@@ -87,7 +87,7 @@ async def about_info_to_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Запустить процесс выбора объекта для обновления."""
- await update_manager.select_obj_to_update(callback, state, session)
+ await about_update_manager.select_obj_to_update(callback, state, session)
@about_router.callback_query(
@@ -100,7 +100,7 @@ async def about_info_to_update(
)
async def update_info_choice(callback: CallbackQuery, session: FSMContext):
"""Обработать выбор данных для обновления."""
- await update_manager.select_data_to_update(callback, session)
+ await about_update_manager.select_data_to_update(callback, session)
@about_router.callback_query(
@@ -108,7 +108,7 @@ async def update_info_choice(callback: CallbackQuery, session: FSMContext):
)
async def about_name_update(callback: CallbackQuery, state: FSMContext):
"""Обновить имя объекта."""
- await update_manager.change_obj_name(callback, state)
+ await about_update_manager.change_obj_name(callback, state)
@about_router.callback_query(
@@ -116,7 +116,7 @@ async def about_name_update(callback: CallbackQuery, state: FSMContext):
)
async def about_url_update(callback: CallbackQuery, state: FSMContext):
"""Обновить содержимое объекта."""
- await update_manager.change_obj_content(callback, state)
+ await about_update_manager.change_obj_content(callback, state)
@about_router.message(or_f(UpdateState.name, UpdateState.url), F.text)
@@ -124,4 +124,4 @@ async def update_about_info(
message: Message, state: FSMContext, session: AsyncSession
):
"""Обновить объект в базе данных на основе нового содержимого."""
- await update_manager.update_obj_in_db(message, state, session)
+ await about_update_manager.update_obj_in_db(message, state, session)
diff --git a/app/admin/handlers/admin_handlers/admin_info_handlers.py b/app/admin/handlers/admin_handlers/admin_info_handlers.py
index 10ceeec..e4af3f5 100644
--- a/app/admin/handlers/admin_handlers/admin_info_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_info_handlers.py
@@ -1,227 +1,145 @@
from aiogram import F, Router
from aiogram.filters import or_f, and_f
from aiogram.fsm.context import FSMContext
-
from aiogram.types import CallbackQuery, Message
+from sqlalchemy.ext.asyncio import AsyncSession
-from crud.info_crud import info_crud
+from admin.admin_managers import (
+ QuestionCreateManager,
+ QuestionUpdateManager,
+ CreateQuestionStates,
+ UpdateQuestionStates,
+ QuestionDeleteManager,
+ DeleteQuestionStates,
+)
from admin.filters.filters import ChatTypeFilter, IsAdmin
from admin.handlers.admin_handlers.admin import SectionState
-from admin.keyboards.keyboards import (
- get_inline_confirmation,
- get_inline_keyboard,
+from admin.admin_settings import (
+ ADMIN_BASE_OPTIONS,
+ ADMIN_QUESTION_OPTIONS,
+ SUPPORT_OPTIONS,
)
-from admin.admin_settings import SUPPORT_OPTIONS
-from sqlalchemy.ext.asyncio import AsyncSession
-from aiogram.fsm.state import State, StatesGroup
+PROBLEMS_MENU = SUPPORT_OPTIONS.get("problems_with_products")
+GENERAL_QEUSTIONS_MENU = SUPPORT_OPTIONS.get("general_questions")
+
info_router = Router()
info_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
-
-class AddQuestion(StatesGroup):
- question = State()
- answer = State()
- question_type = State()
-
-
-class UpdateQuestion(StatesGroup):
- question = State()
- answer = State()
- question_type = State()
- confirm = State()
-
-
-class DeleteQuestion(AddQuestion):
- confirm = State()
-
-
-PREVIOUS_MENU = SUPPORT_OPTIONS.get("faq")
-
-
-async def set_question_type(state: str):
- question_type = state.split(":")[-1]
- return SUPPORT_OPTIONS.get(question_type)
-
-
-async def get_question_list(question_type: str, session: AsyncSession):
- return [
- question.question
- for question in await info_crud.get_all_questions_by_type(
- question_type, session
- )
- ]
+question_create_manager = QuestionCreateManager()
+question_update_manager = QuestionUpdateManager()
+question_delete_manager = QuestionDeleteManager()
@info_router.callback_query(
- or_f(SectionState.faq, SectionState.troubleshooting), F.data == "Добавить"
+ or_f(SectionState.general_questions, SectionState.problems_with_products),
+ F.data == ADMIN_BASE_OPTIONS.get("create"),
)
async def add_question(callback: CallbackQuery, state: FSMContext):
- current_state = await state.get_state()
- await state.set_state(AddQuestion.question_type)
- await state.update_data(
- question_type=await set_question_type(current_state)
- )
- await callback.message.answer("Введите текст нового вопрос")
- await state.set_state(AddQuestion.question)
+ await question_create_manager.add_question_text(callback, state)
-@info_router.message(AddQuestion.question, F.text)
+@info_router.message(CreateQuestionStates.question, F.text)
async def add_question_text(message: Message, state: FSMContext):
- await state.update_data(question=message.text)
- await message.answer("Введите ответ на этот вопрос")
- print(await state.get_data())
- await state.set_state(AddQuestion.answer)
+ await question_create_manager.add_answer_text(message, state)
-@info_router.message(AddQuestion.answer, F.text)
+@info_router.message(CreateQuestionStates.answer, F.text)
async def add_question_answer(
message: Message,
state: FSMContext,
session: AsyncSession,
):
- await state.update_data(answer=message.text)
- data = await state.get_data()
- await info_crud.create(data, session=session)
- await message.answer(
- "Вопрос добавлен!",
- reply_markup=await get_inline_keyboard(
- previous_menu=data.get("question_type")
- ),
- )
- await state.clear()
+ await question_create_manager.add_question_to_db(message, state, session)
@info_router.callback_query(
- or_f(SectionState.faq, SectionState.troubleshooting), F.data == "Удалить"
+ or_f(SectionState.general_questions, SectionState.problems_with_products),
+ F.data == ADMIN_BASE_OPTIONS.get("delete"),
)
async def question_to_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- current_state = await state.get_state()
- await state.set_state(DeleteQuestion.question_type)
- await state.update_data(
- question_type=await set_question_type(current_state)
+ await question_delete_manager.select_question(
+ callback,
+ state,
+ next_state=DeleteQuestionStates.select,
+ session=session,
)
- question_type = (await state.get_data()).get("question_type")
- question_list = await get_question_list(question_type, session)
- await callback.message.edit_text(
- "Какой вопрос удалить?",
- reply_markup=await get_inline_keyboard(
- question_list, previous_menu=PREVIOUS_MENU
- ),
- )
- await state.set_state(DeleteQuestion.question)
-@info_router.callback_query(DeleteQuestion.question, F.data)
+@info_router.callback_query(
+ DeleteQuestionStates.select,
+ and_f(F.data != PROBLEMS_MENU, F.data != GENERAL_QEUSTIONS_MENU),
+)
async def confirm_delete_question(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- question = await info_crud.get_by_string(callback.data, session)
- await callback.message.edit_text(
- f"Вы уверены, что хотите удалить этот вопрос?\n\n {question.question}",
- reply_markup=await get_inline_confirmation(
- option=question.question, cancel_option=PREVIOUS_MENU
- ),
- )
- await state.set_state(DeleteQuestion.confirm)
+ await question_delete_manager.confirm_delete(callback, state, session)
-@info_router.callback_query(DeleteQuestion.confirm, F.data != PREVIOUS_MENU)
+@info_router.callback_query(
+ DeleteQuestionStates.confirm,
+ and_f(F.data != PROBLEMS_MENU, F.data != GENERAL_QEUSTIONS_MENU),
+)
async def delete_question(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- await state.clear()
- question = await info_crud.get_by_string(callback.data, session)
- await info_crud.remove(question, session)
- await callback.message.edit_text(
- "Вопрос удален!",
- reply_markup=await get_inline_keyboard(
- previous_menu=question.question_type
- ),
- )
+ await question_delete_manager.delete_question(callback, state, session)
@info_router.callback_query(
- or_f(SectionState.faq, SectionState.troubleshooting),
- F.data == "Изменить",
+ or_f(SectionState.general_questions, SectionState.problems_with_products),
+ F.data == ADMIN_BASE_OPTIONS.get("update"),
)
async def update_question(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- current_state = await state.get_state()
- await state.set_state(UpdateQuestion.question_type)
- await state.update_data(
- question_type=await set_question_type(current_state)
+ await question_update_manager.select_question(
+ callback,
+ state,
+ next_state=UpdateQuestionStates.select,
+ session=session,
)
- question_type = (await state.get_data()).get("question_type")
- question_list = await get_question_list(question_type, session)
- await callback.message.edit_text(
- "Какой вопрос отредактировать?",
- reply_markup=await get_inline_keyboard(
- question_list, previous_menu=PREVIOUS_MENU
- ),
- )
- await state.set_state(UpdateQuestion.question)
@info_router.callback_query(
- UpdateQuestion.question, and_f(F.data != "Вопрос", F.data != "Ответ")
+ UpdateQuestionStates.select,
+ and_f(
+ F.data != ADMIN_QUESTION_OPTIONS.get("question"),
+ F.data != ADMIN_QUESTION_OPTIONS.get("answer"),
+ F.data != PROBLEMS_MENU,
+ F.data != GENERAL_QEUSTIONS_MENU,
+ ),
)
-async def update_question_choice(callback: CallbackQuery, state: FSMContext):
- await state.update_data(question=callback.data)
- await callback.message.answer(
- "Что вы хотите отредактировать?",
- reply_markup=await get_inline_keyboard(
- ["Вопрос", "Ответ"], previous_menu=PREVIOUS_MENU
- ),
- )
+async def update_question_choice(
+ callback: CallbackQuery, session: AsyncSession
+):
+ await question_update_manager.update_data_type(callback, session)
-@info_router.callback_query(UpdateQuestion.question, F.data == "Вопрос")
+@info_router.callback_query(
+ UpdateQuestionStates.select,
+ F.data == ADMIN_QUESTION_OPTIONS.get("question"),
+)
async def update_question_text(callback: CallbackQuery, state: FSMContext):
- question_text = (await state.get_data()).get("question")
- await callback.message.answer(
- f"Сейчас вопрос записан вот так:\n\n{question_text}\n\n Введите новый текст"
- )
- await state.set_state(UpdateQuestion.confirm)
+ await question_update_manager.update_question(callback, state)
-@info_router.callback_query(UpdateQuestion.question, F.data == "Ответ")
-async def update_question_answer(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- question_text = (await state.get_data()).get("question")
- answer = await info_crud.get_by_string(question_text, session)
- await callback.message.answer(
- f"Сейчас ответ записан вот так:\n\n{answer.answer}\n\n Введите новый текст"
- )
- await state.set_state(UpdateQuestion.answer)
+@info_router.callback_query(
+ UpdateQuestionStates.select, F.data == ADMIN_QUESTION_OPTIONS.get("answer")
+)
+async def update_question_answer(callback: CallbackQuery, state: FSMContext):
+ await question_update_manager.update_answer(callback, state)
@info_router.message(
- or_f(UpdateQuestion.confirm, UpdateQuestion.answer), F.text
+ or_f(UpdateQuestionStates.question, UpdateQuestionStates.answer), F.text
)
async def update_question_data(
message: Message, state: FSMContext, session: AsyncSession
):
- current_state = await state.get_state()
- old_data = await state.get_data()
- question = await info_crud.get_by_string(old_data.get("question"), session)
-
- if current_state == UpdateQuestion.confirm:
- await state.update_data(question=message.text)
- elif current_state == UpdateQuestion.answer:
- await state.update_data(answer=message.text)
-
- updated_data = await state.get_data()
- await info_crud.update(question, updated_data, session=session)
- await message.answer(
- "Вопрос обновлен!",
- reply_markup=await get_inline_keyboard(
- previous_menu=updated_data.get("question_type")
- ),
+ await question_update_manager.update_question_in_db(
+ message, state, session
)
- await state.clear()
diff --git a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
index d7cd88d..5742064 100644
--- a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
@@ -1,254 +1,166 @@
from aiogram import F, Router
from aiogram.filters import and_f, or_f
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from admin.handlers.admin_handlers.admin import SectionState
-from crud.about_crud import company_info_crud
from crud.portfolio_projects_crud import portfolio_crud
from admin.filters.filters import ChatTypeFilter, IsAdmin
-from admin.keyboards.keyboards import (
- get_inline_confirmation,
- get_inline_keyboard,
+from admin.admin_managers import (
+ DeleteManager,
+ DeleteState,
+ CreateState,
+ CreateManager,
+ UpdateManager,
+ UpdateState,
+ UpdatePortfolio,
)
-
from admin.admin_settings import (
+ ADMIN_UPDATE_OPTIONS,
MAIN_MENU_OPTIONS,
ADMIN_PORTFOLIO_OPTIONS,
PORTFOLIO_MENU_OPTIONS,
+ ADMIN_BASE_OPTIONS,
)
portfolio_router = Router()
portfolio_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
-
-class UpdatePortfolio(StatesGroup):
- url = State()
-
-
-class AddProject(StatesGroup):
- project_name = State()
- url = State()
-
-
-class UpdateProject(StatesGroup):
- select = State()
- project_name = State()
- url = State()
- confirm = State()
-
-
-class DeleteProject(AddProject):
- confirm = State()
-
-
PREVIOUS_MENU = PORTFOLIO_MENU_OPTIONS.get("other_projects")
-
-async def get_portfolio_project_list(session: AsyncSession):
- """Получить список названий проектов для портфолио."""
- projects = [
- project.project_name
- for project in await portfolio_crud.get_multi(session)
- ]
- return projects
+portfolio_create_manager = CreateManager(portfolio_crud, PREVIOUS_MENU)
+portfolio_delete_manager = DeleteManager(portfolio_crud, PREVIOUS_MENU)
+portfolio_update_manager = UpdateManager(portfolio_crud, PREVIOUS_MENU)
+main_portfolio_url_update_manager = UpdatePortfolio(
+ MAIN_MENU_OPTIONS.get("portfolio")
+)
@portfolio_router.callback_query(
- SectionState.other_projects, F.data == "Добавить"
+ SectionState.other_projects, F.data == ADMIN_BASE_OPTIONS.get("create")
)
async def add_portfolio_project_name(
callback: CallbackQuery, state: FSMContext
):
- await callback.message.answer("Введите название проекта:")
- await state.set_state(AddProject.project_name)
+ """Запустить процесс создания нового портфолио."""
+ await portfolio_create_manager.add_obj_name(callback, state)
-@portfolio_router.message(AddProject.project_name, F.text)
+@portfolio_router.message(CreateState.name, F.text)
async def add_portfolio_project_url(message: Message, state: FSMContext):
- await state.update_data(project_name=message.text)
- await message.answer("Добавьте ссылку:")
- await state.set_state(AddProject.url)
+ """Сохранить имя нового объекта в состояние."""
+ await portfolio_create_manager.add_obj_url(message, state)
-@portfolio_router.message(AddProject.url, F.text)
+@portfolio_router.message(CreateState.url, F.text)
async def create_portfolio_project(
message: Message, state: FSMContext, session: AsyncSession
):
- await state.update_data(url=message.text)
- data = await state.get_data()
- await portfolio_crud.create(data, session)
- await message.answer(
- "Проект добавлен!",
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
- await state.clear()
+ """Сохранить URL нового объекта в базу данных."""
+ await portfolio_create_manager.add_obj_to_db(message, state, session)
@portfolio_router.callback_query(
- SectionState.other_projects, F.data == "Удалить"
+ SectionState.other_projects, F.data == ADMIN_BASE_OPTIONS.get("delete")
)
async def portfolio_project_to_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- await callback.message.answer(
- "Какой проект Вы хотите удалить?",
- reply_markup=await get_inline_keyboard(
- options=await get_portfolio_project_list(session),
- previous_menu=PREVIOUS_MENU,
- ),
+ """Запустить процесс выбора объекта для удаления."""
+ await portfolio_delete_manager.select_obj_to_delete(
+ callback, state, session
)
- await state.set_state(DeleteProject.project_name)
-@portfolio_router.callback_query(DeleteProject.project_name, F.data)
+@portfolio_router.callback_query(DeleteState.select, F.data != PREVIOUS_MENU)
async def confirm_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- portfolio_project = await portfolio_crud.get_by_project_name(
- callback.data, session
- )
- await callback.message.edit_text(
- f"Вы уверены, что хотите удалить этот проект?\n\n {portfolio_project.project_name}",
- reply_markup=await get_inline_confirmation(
- option=portfolio_project.project_name, cancel_option=PREVIOUS_MENU
- ),
- )
- await state.set_state(DeleteProject.confirm)
+ """Подтведить удаление выбранного объекта."""
+ await portfolio_delete_manager.confirm_delete(callback, state, session)
-@portfolio_router.callback_query(
- DeleteProject.confirm, F.data != PREVIOUS_MENU
-)
+@portfolio_router.callback_query(DeleteState.confirm, F.data != PREVIOUS_MENU)
async def delete_protfolio_project(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- portfolio_project = await portfolio_crud.get_by_project_name(
- callback.data, session
- )
- await portfolio_crud.remove(portfolio_project, session)
- await callback.message.edit_text(
- "Проект удален!",
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
- await state.clear()
+ """Удалить выбранный объект из базы данных."""
+ await portfolio_delete_manager.delete_obj(callback, state, session)
@portfolio_router.callback_query(
- SectionState.other_projects, F.data == "Изменить"
+ SectionState.other_projects, F.data == ADMIN_BASE_OPTIONS.get("update")
)
async def portfolio_project_to_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- await callback.message.edit_text(
- "Какую информацию вы хотите отредактировать?",
- reply_markup=await get_inline_keyboard(
- options=await get_portfolio_project_list(session),
- previous_menu=PREVIOUS_MENU,
- ),
+ """Запустить процесс выбора объекта для обновления."""
+ await portfolio_update_manager.select_obj_to_update(
+ callback, state, session
)
- await state.set_state(UpdateProject.select)
@portfolio_router.callback_query(
- UpdateProject.select,
+ UpdateState.select,
and_f(
- F.data != "Название проекта",
- F.data != "Адрес ссылки",
+ F.data != ADMIN_UPDATE_OPTIONS.get("name"),
+ F.data != ADMIN_UPDATE_OPTIONS.get("content"),
F.data != PREVIOUS_MENU,
),
)
async def update_portfolio_project_choise(
- callback: CallbackQuery, state: FSMContext
+ callback: CallbackQuery, session: AsyncSession
):
- await state.update_data(select=callback.data)
- await callback.message.edit_text(
- "Что вы хотите отредактировать?",
- reply_markup=await get_inline_keyboard(
- ["Название проекта", "Адрес ссылки"], previous_menu=PREVIOUS_MENU
- ),
- )
+ """Обработать выбор данных для обновления."""
+ await portfolio_update_manager.select_data_to_update(callback, session)
@portfolio_router.callback_query(
- UpdateProject.select, F.data == "Название проекта"
+ UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
)
-async def about_name_update(
+async def portfolio_name_update(
callback: CallbackQuery,
state: FSMContext,
):
- about_name = await state.get_data()
- about_name_text = about_name.get("select")
- await callback.message.answer(
- f"Сейчас у проекта такое название:\n\n {about_name_text}\n\n Введите новое название"
- )
- await state.set_state(UpdateProject.project_name)
+ """Обновить имя объекта."""
+ await portfolio_update_manager.change_obj_name(callback, state)
@portfolio_router.callback_query(
- UpdateProject.select, F.data == "Адрес ссылки"
+ UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
)
-async def about_url_update(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- about_name_data = await state.get_data()
- about_name_text = about_name_data.get("select")
- about_info = await portfolio_crud.get_by_project_name(
- about_name_text, session
- )
- await callback.message.answer(
- f"Сейчас у ссылки такой адрес:\n\n {about_info.url}\n\n Введите новое название"
- )
- await state.set_state(UpdateProject.url)
+async def about_url_update(callback: CallbackQuery, state: FSMContext):
+ """Обновить содержимое объекта."""
+ await portfolio_update_manager.change_obj_content(callback, state)
-@portfolio_router.message(
- or_f(UpdateProject.project_name, UpdateProject.url), F.text
-)
+@portfolio_router.message(or_f(UpdateState.name, UpdateState.url), F.text)
async def update_about_info(
message: Message, state: FSMContext, session: AsyncSession
):
- current_state = await state.get_state()
- old_data = await state.get_data()
- old_portfolio_data = await portfolio_crud.get_by_project_name(
- old_data.get("select"), session
- )
- if current_state == UpdateProject.project_name:
- await state.update_data(project_name=message.text)
- elif current_state == UpdateProject.url:
- await state.update_data(url=message.text)
- update_data = await state.get_data()
- await portfolio_crud.update(old_portfolio_data, update_data, session)
- await message.answer(
- "Информация обновлена!",
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
- await state.clear()
+ """Обновить объект в базе данных на основе нового содержимого."""
+ await portfolio_update_manager.update_obj_in_db(message, state, session)
@portfolio_router.callback_query(
SectionState.portfolio,
F.data == ADMIN_PORTFOLIO_OPTIONS.get("change_url"),
)
-async def change_portfolio_url(callback: CallbackQuery, state: FSMContext):
- await callback.message.answer("Введите новый адрес ссылки на портфолио")
- await state.set_state(UpdatePortfolio.url)
+async def change_portfolio_url(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Обновить адрес ссылки основного портфолио."""
+ await main_portfolio_url_update_manager.update_main_portfolio_url(
+ callback, state, session
+ )
-@portfolio_router.message(UpdatePortfolio.url, F.text)
+@portfolio_router.message(UpdateState.portolio, F.text)
async def update_portfolio_button(
message: Message, state: FSMContext, session: AsyncSession
):
- await state.update_data(url=message.text)
- updated_data = await state.get_data()
- portfolio = await company_info_crud.get_portfolio(session)
- await company_info_crud.update(portfolio, updated_data, session)
- await message.answer(
- "Изменения внесены!",
- reply_markup=await get_inline_keyboard(
- previous_menu=MAIN_MENU_OPTIONS.get("portfolio")
- ),
+ """Обновить адрес ссылки основного портфолио в базе данных."""
+ await main_portfolio_url_update_manager.update_obj_in_db(
+ message, state, session
)
- await state.clear()
diff --git a/app/admin/handlers/admin_handlers/admin_product_handlers.py b/app/admin/handlers/admin_handlers/admin_product_handlers.py
index 70eb333..9bf9456 100644
--- a/app/admin/handlers/admin_handlers/admin_product_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_product_handlers.py
@@ -9,12 +9,17 @@
from crud.category_product import category_product_crud
from crud.product_crud import product_crud
from admin.filters.filters import ChatTypeFilter, IsAdmin
-from admin.keyboards.keyboards import (
- get_inline_confirmation,
- get_inline_keyboard,
+from admin.admin_managers import (
+ DeleteManager,
+ DeleteState,
+ CreateState,
+ CreateManager,
+ UpdateManager,
+ UpdateState,
)
from admin.admin_settings import (
+ ADMIN_BASE_OPTIONS,
MAIN_MENU_OPTIONS,
)
@@ -23,21 +28,24 @@
PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("products")
+product_create_manager = CreateManager(product_crud, PREVIOUS_MENU)
+product_update_manager = UpdateManager(product_crud, PREVIOUS_MENU)
+product_delete_manager = DeleteManager(product_crud, PREVIOUS_MENU)
-class AddProduct(StatesGroup):
- title = State()
- response = State()
+# class AddProduct(StatesGroup):
+# title = State()
+# response = State()
-class UpdateProduct(StatesGroup):
- select = State()
- title = State()
- response = State()
- confirm = State()
+# class UpdateProduct(StatesGroup):
+# select = State()
+# title = State()
+# response = State()
+# confirm = State()
-class DeleteProduct(AddProduct):
- confirm = State()
+# class DeleteProduct(AddProduct):
+# confirm = State()
class AddProductInfo(StatesGroup):
@@ -49,47 +57,27 @@ class AddProductInfo(StatesGroup):
media_description = State()
-async def get_products_list(session: AsyncSession):
- """Получить список названий проектов для портфолио."""
-
- return [project.title for project in await product_crud.get_multi(session)]
-
-
-@product_router.callback_query(SectionState.product, F.data == "Добавить")
+@product_router.callback_query(
+ SectionState.product, F.data == ADMIN_BASE_OPTIONS.get("create")
+)
async def add_product(callback: CallbackQuery, state: FSMContext):
"""Добавить название."""
-
- await callback.message.answer("Введите название проекта или услуги")
- await state.set_state(AddProduct.title)
+ await product_create_manager.add_obj_name(callback, state)
-@product_router.message(AddProduct.title, F.text)
+@product_router.message(CreateState.name, F.text)
async def add_product_description(message: Message, state: FSMContext):
"""Добавить описание."""
+ await product_create_manager.add_obj_description(message, state)
- await state.update_data(title=message.text)
- await message.answer("Добавьте описание к продукту или услуге")
- await state.set_state(AddProduct.response)
-
-@product_router.message(AddProduct.response, F.text)
+@product_router.message(CreateState.description, F.text)
async def creeate_product(
message: Message, state: FSMContext, session: AsyncSession
):
"""Создать продкет в БД."""
- await state.update_data(response=message.text)
- data = await state.get_data()
-
- await product_crud.create(data, session)
- await message.answer(
- "Продукт создан! Хотите добавить к нему дополнительну информацию?",
- reply_markup=await get_inline_confirmation(
- option="Да", cancel_option=PREVIOUS_MENU
- ),
- )
-
- await state.set_state(AddProductInfo.product_id)
+ await product_create_manager.add_obj_to_db(message, state, session)
@product_router.callback_query(AddProductInfo.product_id, F.data == "Да")
diff --git a/app/admin/handlers/user.py b/app/admin/handlers/user.py
index ee5b2d6..1170de1 100644
--- a/app/admin/handlers/user.py
+++ b/app/admin/handlers/user.py
@@ -102,7 +102,7 @@ async def main_menu_callback(callback: CallbackQuery, state: FSMContext):
@user_router.callback_query(F.data == MAIN_MENU_OPTIONS.get("portfolio"))
async def portfolio_info(callback: CallbackQuery, session: AsyncSession):
- portlio_url = await company_info_crud.get_portfolio(session)
+ portlio_url = await portfolio_crud.get_portfolio(session)
await callback.message.edit_text(
PORTFOLIO_MENU_TEXT,
reply_markup=await get_inline_keyboard(
@@ -159,8 +159,8 @@ async def support_menu(callback: CallbackQuery):
@user_router.callback_query(
or_f(
- F.data == SUPPORT_OPTIONS.get("faq"),
- F.data == SUPPORT_OPTIONS.get("troubleshooting"),
+ F.data == SUPPORT_OPTIONS.get("general_questions"),
+ F.data == SUPPORT_OPTIONS.get("problems_with_products"),
)
)
async def info_faq(
@@ -201,9 +201,8 @@ async def portfolio_other_projects(
"""Получить список других проектов компании."""
await state.clear()
-
projects = await portfolio_crud.get_multi(session)
- projects_names = [project.project_name for project in projects]
+ projects_names = [project.name for project in projects]
urls = [project.url for project in projects]
await callback.message.edit_text(
@@ -255,7 +254,7 @@ async def product_category(
urls = [category.url for category in categories]
await callback.message.edit_text(
- f"{product.response}",
+ f"{product.description}",
reply_markup=await get_inline_keyboard(
categories_by_name,
urls=urls,
diff --git a/alembic/README b/app/alembic/README
similarity index 100%
rename from alembic/README
rename to app/alembic/README
diff --git a/alembic/env.py b/app/alembic/env.py
similarity index 98%
rename from alembic/env.py
rename to app/alembic/env.py
index c557683..53db126 100644
--- a/alembic/env.py
+++ b/app/alembic/env.py
@@ -10,7 +10,7 @@
from alembic import context
-from app.core.base import Base
+from core.base import Base
load_dotenv('.env')
diff --git a/alembic/script.py.mako b/app/alembic/script.py.mako
similarity index 100%
rename from alembic/script.py.mako
rename to app/alembic/script.py.mako
diff --git a/alembic/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py b/app/alembic/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py
similarity index 100%
rename from alembic/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py
rename to app/alembic/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py
diff --git a/app/alembic/versions/4c3a4964ff61_add_description_fields_to_product_and_.py b/app/alembic/versions/4c3a4964ff61_add_description_fields_to_product_and_.py
new file mode 100644
index 0000000..b69acd2
--- /dev/null
+++ b/app/alembic/versions/4c3a4964ff61_add_description_fields_to_product_and_.py
@@ -0,0 +1,34 @@
+"""add description fields to product and category, rename RoleEnum
+
+Revision ID: 4c3a4964ff61
+Revises: 42e8d0d2bdd4
+Create Date: 2024-10-13 16:09:40.786385
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '4c3a4964ff61'
+down_revision: Union[str, None] = '42e8d0d2bdd4'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('categorytype', sa.Column('description', sa.TEXT(), nullable=False))
+ op.add_column('productcategory', sa.Column('description', sa.TEXT(), nullable=False))
+ op.drop_column('productcategory', 'response')
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('productcategory', sa.Column('response', sa.TEXT(), nullable=False))
+ op.drop_column('productcategory', 'description')
+ op.drop_column('categorytype', 'description')
+ # ### end Alembic commands ###
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index 69de2d9..797a575 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -4,7 +4,7 @@
from aiogram.types import Message
from sqlalchemy.ext.asyncio import AsyncSession
from admin.keyboards.keyboards import get_inline_keyboard
-from app.admin.admin_settings import MAIN_MENU_BUTTONS
+from admin.admin_settings import MAIN_MENU_BUTTONS
from bot.bot_const import (
ADMIN_NEGATIVE_ANSWER, ADMIN_POSITIVE_ANSWER, START_MESSAGE
)
diff --git a/app/core/init_db.py b/app/core/init_db.py
index a08cbee..2b0c267 100644
--- a/app/core/init_db.py
+++ b/app/core/init_db.py
@@ -1,14 +1,23 @@
+from app.crud.users import create_user_id
+from crud.user_crud import user_crud
from core.db import AsyncSessionLocal
-from crud.about_crud import company_info_crud
-from app.admin.admin_settings import PORTFOLIO_DEFAULT_DATA
+from crud.portfolio_projects_crud import portfolio_crud
+from admin.admin_settings import PORTFOLIO_DEFAULT_DATA
+from admin.admin_settings import admin_list
async def add_portfolio():
"""Добавить ссылку на портфолио при запуске бота."""
async with AsyncSessionLocal() as async_session:
- if not await company_info_crud.get_by_about_name(
+ if not await portfolio_crud.get_by_string(
PORTFOLIO_DEFAULT_DATA.get("name"), async_session
):
- await company_info_crud.create(
- PORTFOLIO_DEFAULT_DATA, async_session
- )
+ await portfolio_crud.create(PORTFOLIO_DEFAULT_DATA, async_session)
+
+
+async def set_admin():
+ async with AsyncSessionLocal() as session:
+ for admin in admin_list:
+ if not user_crud.get_user_by_tg_id(admin, session):
+ user = await create_user_id(admin, session)
+ await user_crud.update(user, {"role": "ADMIN"}, session)
diff --git a/app/crud/about_crud.py b/app/crud/about_crud.py
index 84fae91..da6a9f8 100644
--- a/app/crud/about_crud.py
+++ b/app/crud/about_crud.py
@@ -4,10 +4,6 @@
from sqlalchemy.ext.asyncio import AsyncSession
from models.models import InformationAboutCompany
-# from core.settings import PORTFOLIO_DEFAULT_DATA
-
-
-PORTFOLIO_DEFAULT_DATA = {"name": "Портфолио", "url": "https://scid.ru/cases"}
class AboutCRUD(CRUDBase):
@@ -22,15 +18,6 @@ async def get_by_about_name(
)
return db_obj.scalars().first()
- async def get_portfolio(self, session: AsyncSession):
- """Получить объект в котором хранится ссылка на портфолио."""
- portfolio_obj = await session.execute(
- select(self.model).where(
- self.model.name == PORTFOLIO_DEFAULT_DATA.get("name")
- )
- )
- return portfolio_obj.scalars().first()
-
async def get_multi(self, session: AsyncSession):
"""Получить список всех объектов модели из БД."""
db_objs = await session.execute(
diff --git a/app/crud/portfolio_projects_crud.py b/app/crud/portfolio_projects_crud.py
index 3fb9935..9673e47 100644
--- a/app/crud/portfolio_projects_crud.py
+++ b/app/crud/portfolio_projects_crud.py
@@ -7,16 +7,22 @@
class PortfolioProjectsCRUD(CRUDBase):
- async def get_by_project_name(
- self,
- project_name: str,
- session: AsyncSession,
- ):
- """Получить проект портфолио по тексту названия."""
- portfolio_project = await session.execute(
- select(self.model).where(self.model.name == project_name)
+ async def get_portfolio(self, session: AsyncSession):
+ """Получить объект в котором хранится ссылка на портфолио."""
+ portfolio_obj = await session.execute(
+ select(self.model).where(self.model.id == 1)
)
- return portfolio_project.scalars().first()
+ return portfolio_obj.scalars().first()
+
+ async def get_multi(self, session: AsyncSession):
+ """
+ Получить список всех объектов модели из БД,
+ кроме основного портфолио.
+ """
+ db_objs = await session.execute(
+ select(self.model).where(self.model.id != 1)
+ )
+ return db_objs.scalars().all()
portfolio_crud = PortfolioProjectsCRUD(CheckCompanyPortfolio)
diff --git a/app/crud/projects.py b/app/crud/projects.py
index 48e30c8..0e358c5 100644
--- a/app/crud/projects.py
+++ b/app/crud/projects.py
@@ -22,7 +22,7 @@ async def response_text_by_id(id: int, session: AsyncSession) -> str:
"""Возвращает ответ на выбранную категорию."""
result = await session.execute(
- select(ProductCategory.response).where(ProductCategory.id == id)
+ select(ProductCategory.description).where(ProductCategory.id == id)
)
return result.scalar()
diff --git a/app/crud/user_crud.py b/app/crud/user_crud.py
index 4138ae7..5bf1ff9 100644
--- a/app/crud/user_crud.py
+++ b/app/crud/user_crud.py
@@ -27,7 +27,7 @@ async def get_user_by_tg_id(self, tg_id: int, session: AsyncSession):
"""Получить пользователя по его tg_id."""
user = await session.execute(
- select(self.model).where(self.model.telegram_id == tg_id)
+ select(self.model).where(self.model.tg_id == tg_id)
)
return user.scalars().first()
diff --git a/app/main.py b/app/main.py
index 5e1dba5..bb42c21 100644
--- a/app/main.py
+++ b/app/main.py
@@ -7,7 +7,7 @@
from bot.handlers import router as message_router
from bot.callbacks import router as callback_router
from bot.fsm_context import router as fsm_context_router
-from core.init_db import add_portfolio
+from core.init_db import add_portfolio, set_admin
from admin.handlers.admin_handlers import admin_router
from admin.handlers.user import user_router
@@ -44,6 +44,7 @@ async def main() -> None:
DataBaseSession(session_pool=AsyncSessionLocal)
)
await add_portfolio()
+ await set_admin()
await dispatcher.start_polling(bot)
except Exception as e:
diff --git a/app/models/models.py b/app/models/models.py
index 8ddf6ef..597b5a0 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -11,14 +11,14 @@
class RoleEnum(str, Enum):
- USER = 'U'
- ADMIN = 'A'
- MANAGER = 'M'
+ USER = "Пользователь"
+ ADMIN = "Админ"
+ MANAGER = "Mенеджер"
class QuestionEnum(str, Enum):
- GENERAL_QUESTIONS = 'Общие вопросы'
- PROBLEMS_WITH_PRODUCTS = 'Проблемы с продуктами'
+ GENERAL_QUESTIONS = "Общие вопросы"
+ PROBLEMS_WITH_PRODUCTS = "Проблемы с продуктами"
class User(Base):
@@ -49,7 +49,7 @@ class ProductCategory(Base):
name: Mapped[str] = mapped_column(pgsql_types.VARCHAR(150))
- response: Mapped[str] = mapped_column(pgsql_types.TEXT)
+ description: Mapped[str] = mapped_column(pgsql_types.TEXT)
categories = relationship(
"CategoryType",
@@ -64,7 +64,7 @@ class CategoryType(Base):
name: Mapped[str] = mapped_column(pgsql_types.VARCHAR(150), nullable=False)
product_id: Mapped[int] = mapped_column(
- ForeignKey('productcategory.id', ondelete='CASCADE'),
+ ForeignKey("productcategory.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
@@ -73,6 +73,8 @@ class CategoryType(Base):
media: Mapped[str] = mapped_column(pgsql_types.VARCHAR(128), nullable=True)
+ description: Mapped[str] = mapped_column(pgsql_types.TEXT)
+
product_category = relationship(
"ProductCategory", back_populates="categories"
)
@@ -89,9 +91,7 @@ class InformationAboutCompany(Base):
class CheckCompanyPortfolio(Base):
"""Бд модель информации о проектах."""
- name: Mapped[str] = mapped_column(
- pgsql_types.VARCHAR(48), nullable=False
- )
+ name: Mapped[str] = mapped_column(pgsql_types.VARCHAR(48), nullable=False)
url: Mapped[str] = mapped_column(pgsql_types.VARCHAR(128))
@@ -101,7 +101,7 @@ class Info(Base):
question_type: Mapped[QuestionEnum] = mapped_column(
pgsql_types.ENUM(
- QuestionEnum, name='question_enum', create_type=False
+ QuestionEnum, name="question_enum", create_type=False
),
nullable=False,
)
diff --git a/app/scripts_for_db.py b/app/scripts_for_db.py
index 4325c44..2d8af6a 100644
--- a/app/scripts_for_db.py
+++ b/app/scripts_for_db.py
@@ -1,7 +1,7 @@
'''
Скрипт для ветки узнать о продуктах и услугах:
-INSERT INTO ProductCategory (title, response) VALUES
+INSERT INTO ProductCategory (name, response) VALUES
('Разработка сайтов', 'Текст для разработки сайтов'),
('Создание порталов', 'Текст для создания порталов'),
('Разработка мобильных приложений', 'Текст для мобильных приложений'),
@@ -36,7 +36,7 @@
'''
Скрипт тестовыхх данных для ветки посмотреть портфолио
-INSERT INTO checkcompanyportfolio (project_name, url) VALUES
+INSERT INTO checkcompanyportfolio (name, url) VALUES
('Project Alpha', 'https://example.com/project-alpha'),
('Project Beta', 'https://example.com/project-beta'),
('Project Gamma', 'https://example.com/project-gamma'),
diff --git a/app/set_admin.py b/app/set_admin.py
deleted file mode 100644
index 2290fbc..0000000
--- a/app/set_admin.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import asyncio
-
-from crud.user_crud import user_crud
-from core.db import AsyncSessionLocal
-
-
-async def set_admin():
- """
- После первого запуска бота и чистой БД добавить себя в список админов.
- Не забыть удалить потом этот файл.
- """
- async with AsyncSessionLocal() as session:
- user = await user_crud.get(1, session)
- await user_crud.update(user, {"role": "ADMIN"}, session)
-
-
-if __name__ == "__main__":
- asyncio.run(set_admin())
diff --git a/test.py b/test.py
index 3032058..9a2d1ac 100644
--- a/test.py
+++ b/test.py
@@ -1,326 +1,18 @@
-# from typing import Any
-# from app.admin.keyboards.keyboards import InlineKeyboardManager
+import asyncio
-# from aiogram.types import (
-# InlineKeyboardButton,
-# InlineKeyboardMarkup,
-# )
-# from aiogram.utils.keyboard import InlineKeyboardBuilder
+from app.crud.info_crud import info_crud
+from app.core.db import AsyncSessionLocal
-# keyboard = InlineKeyboardManager()
-# keyboard.add_previous_menu_button(previous_menu="NAZAD")
-
-# test = keyboard.create_keyboard()
-
-# # keyboard.add_admin_button("test")
-
-# keyboard.add_extra_buttons(["test"])
-# test2 = keyboard.create_keyboard()
-# # print(vars(keyboard))
-# keyboard.add_extra_buttons(["extra_test"])
-# test3 = keyboard.create_keyboard()
-
-
-def get_buttons_from_keyboard(keyboard):
- button_list = []
- for row in keyboard.inline_keyboard:
- for button in row:
- button_list.append((button.text, button.callback_data))
- return button_list
-
-
-# keyboard.update_buttons(["updated_list"])
-# test4 = keyboard.create_keyboard()
-# print(get_buttons_from_keyboard(test))
-# print(get_buttons_from_keyboard(test2))
-# print(get_buttons_from_keyboard(test3))
-# print(get_buttons_from_keyboard(test4))
-
-
-# class InlineKeyboardManager:
-# def __init__(
-# self,
-# options=None,
-# callback=None,
-# urls=None,
-# size=(1,),
-# previous_menu=None,
-# admin_update_menu=None,
-# ):
-# self.options = options if options is not None else []
-# self.callback = callback if callback is not None else self.options
-# self.urls = urls if urls is not None else []
-# self.size = size
-# self.previous_menu = previous_menu
-# self.admin_update_menu = admin_update_menu
-# self.keyboard = InlineKeyboardBuilder()
-
-# def add_buttons(self):
-# """Добавить основные кнопки в клавиатуру."""
-# for index, option in enumerate(self.options):
-# self.keyboard.add(
-# InlineKeyboardButton(
-# text=option,
-# callback_data=str(self.callback[index]),
-# url=(
-# self.urls[index]
-# if self.urls and index < len(self.urls)
-# else None
-# ),
-# )
-# )
-
-# def add_back_button(self):
-# """Добавить кнопку 'Назад'."""
-# if self.previous_menu:
-# self.keyboard.add(
-# InlineKeyboardButton(
-# text="Назад",
-# callback_data=self.previous_menu,
-# )
-# )
-
-# def add_admin_button(self):
-# """Добавить кнопку 'Редактировать' для администраторов."""
-# self.keyboard.add(
-# InlineKeyboardButton(
-# text="Редактировать🔧",
-# callback_data=f"{self.admin_update_menu}_",
-# )
-# )
-
-# def create_keyboard(self) -> InlineKeyboardMarkup:
-# """Создать клавиатуру и вернуть ее.
-
-# :return: Объект InlineKeyboardMarkup с добавленными кнопками.
-# """
-# self.add_buttons()
-# if self.previous_menu:
-# self.add_back_button()
-# if self.admin_update_menu:
-# self.add_admin_button
-# return self.keyboard.adjust(*self.size).as_markup(resize_keyboard=True)
-
-
-# def get_base_inline_keyboard(
-# options=None,
-# callback=None,
-# urls=None,
-# previous_menu=None,
-# ):
-# return InlineKeyboardManager(
-# options=options,
-# callback=callback,
-# urls=urls,
-# previous_menu=previous_menu,
-# ).create_keyboard()
-
-
-# def get_admin_keyboard(
-# admin_update_menu,
-# options=None,
-# callback=None,
-# urls=None,
-# previous_menu=None,
-# ):
-# return InlineKeyboardManager(
-# options=options,
-# callback=callback,
-# urls=urls,
-# previous_menu=previous_menu,
-# admin_update_menu=admin_update_menu,
-# ).create_keyboard()
-
-
-# class AdminInlineKeyboard(BaseInlineKeyboardManager):
-# """
-# Класс для управления инлайн-клавиатурами с администраторскими функциями.
-
-# Этот класс наследует базовый класс и добавляет возможность
-# добавления кнопки "Редактировать" для администраторов.
-# """
-
-# def __init__(
-# self,
-# admin_update_menu: str,
-# *args,
-# **kwargs,
-# ):
-# self.admin_update_menu = admin_update_menu
-# super().__init__(*args, **kwargs)
-
-# def add_admin_button(self):
-# """Добавить кнопку 'Редактировать' для администраторов."""
-# self.keyboard.add(
-# InlineKeyboardButton(
-# text="Редактировать🔧",
-# callback_data=f"{self.admin_update_menu}_",
-# )
-# )
-
-# def create_keyboard(self) -> InlineKeyboardMarkup:
-# """Создать клавиатуру и вернуть ее.
-
-# :return: Объект InlineKeyboardMarkup с добавленными кнопками,
-# включая кнопку "Редактировать".
-# """
-# super().create_keyboard()
-# self.add_admin_button()
-# return self.keyboard
-
-
-# def get_base_inline_keyboard(options=None, callback=None, urls=None):
-# """Создать базовую инлайн-клавиатуру.
-
-# :param options: Список названий кнопок.
-# :param callback: Список коллбек-данных для кнопок.
-# :param urls: Список URL для кнопок.
-# :return: Объект InlineKeyboardMarkup.
-# """
-# return BaseInlineKeyboardManager(
-# options, callback=callback, urls=urls
-# ).create_keyboard()
-
-
-# def get_admin_inline_kb(
-# admin_update_menu,
-# options=None,
-# callback=None,
-# urls=None,
-# previous_menu=None,
-# ):
-# """Создать инлайн-клавиатуру для администраторов.
-
-# :param admin_update_menu: Коллбек-данные для кнопки "Редактировать".
-# :param options: Список названий кнопок.
-# :param callback: Список коллбек-данных для кнопок.
-# :param urls: Список URL для кнопок.
-# :param previous_menu: Коллбек-данные для кнопки "Назад".
-# :return: Объект InlineKeyboardMarkup.
-# """
-# return AdminInlineKeyboard(
-# admin_update_menu=admin_update_menu,
-# options=options,
-# callback=callback,
-# urls=urls,
-# previous_menu=previous_menu,
-# ).create_keyboard()
-
-from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
-from aiogram.utils.keyboard import InlineKeyboardBuilder
-
-
-class InlineKeyboardManager:
+async def set_admin():
"""
- Менеджер для создания инлайн-клавиатур.
-
- Этот класс позволяет добавлять кнопки, включая кнопки для администраторов
- и кнопку "Назад".
+ После первого запуска бота и чистой БД добавить себя в список админов.
+ Не забыть удалить потом этот файл.
"""
+ async with AsyncSessionLocal() as session:
+ info = await info_crud.get(1, session)
+ await info_crud.remove(info, session)
- def __init__(
- self,
- options=None,
- callback=None,
- urls=None,
- size=(1,),
- previous_menu=None,
- admin_update_menu=None,
- ):
- self.options = options if options is not None else []
- self.callback = callback if callback is not None else self.options
- self.urls = urls if urls is not None else []
- self.size = size
- self.previous_menu = previous_menu
- self.admin_update_menu = admin_update_menu
- self.keyboard = InlineKeyboardBuilder()
- def add_buttons(self):
- """Добавить основные кнопки в клавиатуру."""
- for index, option in enumerate(self.options):
- self.keyboard.add(
- InlineKeyboardButton(
- text=option,
- callback_data=str(self.callback[index]),
- url=(
- self.urls[index]
- if self.urls and index < len(self.urls)
- else None
- ),
- )
- )
-
- def add_back_button(self):
- """Добавить кнопку 'Назад'."""
- if self.previous_menu:
- self.keyboard.add(
- InlineKeyboardButton(
- text="Назад",
- callback_data=self.previous_menu,
- )
- )
-
- def add_admin_button(self):
- """Добавить кнопку 'Редактировать' для администраторов."""
- if self.admin_update_menu:
- self.keyboard.add(
- InlineKeyboardButton(
- text="Редактировать🔧",
- callback_data=f"{self.admin_update_menu}_",
- )
- )
-
- def create_keyboard(self) -> InlineKeyboardMarkup:
- """Создать клавиатуру и вернуть ее."""
- self.add_buttons()
- self.add_back_button()
- self.add_admin_button() # Исправлено: добавлены скобки
- return self.keyboard.adjust(*self.size).as_markup(resize_keyboard=True)
-
-
-def get_base_inline_keyboard(
- options=None,
- callback=None,
- urls=None,
- previous_menu=None,
-):
- """Создать базовую инлайн-клавиатуру.
-
- :param options: Список названий кнопок.
- :param callback: Список коллбек-данных для кнопок.
- :param urls: Список URL для кнопок.
- :param previous_menu: Коллбек-данные для кнопки "Назад".
- :return: Объект InlineKeyboardMarkup.
- """
- return InlineKeyboardManager(
- options=options,
- callback=callback,
- urls=urls,
- previous_menu=previous_menu,
- ).create_keyboard()
-
-
-def get_admin_keyboard(
- admin_update_menu,
- options=None,
- callback=None,
- urls=None,
- previous_menu=None,
-):
- """Создать инлайн-клавиатуру для администраторов.
-
- :param admin_update_menu: Коллбек-данные для кнопки "Редактировать".
- :param options: Список названий кнопок.
- :param callback: Список коллбек-данных для кнопок.
- :param urls: Список URL для кнопок.
- :param previous_menu: Коллбек-данные для кнопки "Назад".
- :return: Объект InlineKeyboardMarkup.
- """
- return InlineKeyboardManager(
- options=options,
- callback=callback,
- urls=urls,
- previous_menu=previous_menu,
- admin_update_menu=admin_update_menu,
- ).create_keyboard()
+if __name__ == "__main__":
+ asyncio.run(set_admin())
From df79edaa783c378f74c55e7095b9aaf54e784fa5 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Sun, 13 Oct 2024 21:38:51 +0300
Subject: [PATCH 38/75] add product and category managers and routs
---
app/admin/admin_managers/__init__.py | 8 +
app/admin/admin_managers/base_manager.py | 5 +-
app/admin/admin_managers/category_manager.py | 164 ++++++++
app/admin/admin_managers/create_manager.py | 36 +-
app/admin/admin_managers/delete_manager.py | 15 +-
app/admin/admin_managers/update_manager.py | 29 +-
app/admin/admin_settings.py | 3 +-
.../admin_about_company_handlers.py | 2 +-
.../admin_handlers/admin_category_handlers.py | 360 ++++++------------
.../admin_handlers/admin_product_handlers.py | 258 ++-----------
10 files changed, 381 insertions(+), 499 deletions(-)
create mode 100644 app/admin/admin_managers/category_manager.py
diff --git a/app/admin/admin_managers/__init__.py b/app/admin/admin_managers/__init__.py
index 45e9fab..ea865eb 100644
--- a/app/admin/admin_managers/__init__.py
+++ b/app/admin/admin_managers/__init__.py
@@ -9,3 +9,11 @@
DeleteQuestionStates,
QuestionDeleteManager,
)
+from .category_manager import ( # noqa
+ CreateCategoryManager,
+ UpdateCategoryManager,
+ DeleteCategoryManager,
+ CategoryCreateState,
+ CategoryUpdateState,
+ CategoryDeleteState,
+)
diff --git a/app/admin/admin_managers/base_manager.py b/app/admin/admin_managers/base_manager.py
index 45b18e3..db83bb8 100644
--- a/app/admin/admin_managers/base_manager.py
+++ b/app/admin/admin_managers/base_manager.py
@@ -1,5 +1,7 @@
from abc import ABC
+from aiogram.fsm.state import StatesGroup
+
from crud.base_crud import CRUDBase
@@ -17,7 +19,8 @@ class BaseAdminManager(ABC):
"""
def __init__(
- self, model_crud: CRUDBase, back_option: str
+ self, model_crud: CRUDBase, back_option: str, states_group: StatesGroup
) -> None:
self.model_crud = model_crud
self.back_option = back_option
+ self.states_group = states_group
diff --git a/app/admin/admin_managers/category_manager.py b/app/admin/admin_managers/category_manager.py
new file mode 100644
index 0000000..2234d35
--- /dev/null
+++ b/app/admin/admin_managers/category_manager.py
@@ -0,0 +1,164 @@
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from aiogram.types import CallbackQuery
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from admin.keyboards.keyboards import (
+ get_inline_confirmation,
+ get_inline_keyboard,
+)
+from admin.admin_settings import ADMIN_CONTENT_OPTIONS, ADMIN_UPDATE_BUTTONS
+from crud.base_crud import CRUDBase
+from crud.category_product import category_product_crud
+from .create_manager import CreateManager
+from .update_manager import UpdateManager
+from .delete_manager import DeleteManager
+
+
+class CategoryCreateState(StatesGroup):
+ select = State()
+ name = State()
+ url = State()
+ description = State()
+ media = State()
+
+
+class CategoryUpdateState(StatesGroup):
+ select = State()
+ name = State()
+ url = State()
+ description = State()
+ media = State()
+
+
+class CategoryDeleteState(StatesGroup):
+ select = State()
+ confirm = State()
+
+
+class CreateCategoryManager(CreateManager):
+ def __init__(
+ self,
+ back_option: str,
+ model_crud: CRUDBase = category_product_crud,
+ states_group: StatesGroup = CategoryCreateState(),
+ ) -> None:
+ super().__init__(model_crud, back_option, states_group)
+
+ async def add_obj_name(
+ self,
+ product_id: int,
+ callback: CallbackQuery,
+ state: FSMContext,
+ ):
+ """
+ Добавить название объекта и перейти в
+ следующее машинное состояние.
+ """
+ await state.update_data(product_id=product_id)
+ await callback.message.answer(
+ "Введите название:",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ if callback.data == ADMIN_CONTENT_OPTIONS.get("url"):
+ await state.set_state(self.states_group.url)
+ elif callback.data == ADMIN_CONTENT_OPTIONS.get("description"):
+ await state.set_state(self.states_group.description)
+ elif callback.data == ADMIN_CONTENT_OPTIONS.get("media"):
+ await state.set_state(self.states_group.media)
+
+
+class UpdateCategoryManager(UpdateManager):
+ def __init__(
+ self,
+ back_option: str,
+ model_crud: CRUDBase = category_product_crud,
+ states_group: StatesGroup = CategoryUpdateState(),
+ ) -> None:
+ super().__init__(model_crud, back_option, states_group)
+
+ async def select_obj_to_update(
+ self,
+ product_id: int,
+ callback: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
+ ) -> None:
+ obj_list = await self.model_crud.get_category_by_product_id(
+ product_id, session
+ )
+ obj_names = [obj.name for obj in obj_list]
+ obj_ids = [obj.id for obj in obj_list]
+ await callback.message.edit_text(
+ "Какой объект отредактировать?",
+ reply_markup=await get_inline_keyboard(
+ options=obj_names,
+ callback=obj_ids,
+ previous_menu=self.back_option,
+ ),
+ )
+ await state.set_state(self.states_group.select)
+
+ async def select_data_to_update(
+ self,
+ callback: CallbackQuery,
+ session: AsyncSession,
+ ):
+ """Выбрать поле редактирования для модели в БД."""
+ self.obj_to_update = await self.model_crud.get(callback.data, session)
+ await callback.message.edit_text(
+ "Выбирите данные для обновления:",
+ reply_markup=await get_inline_keyboard(
+ ADMIN_UPDATE_BUTTONS, previous_menu=self.back_option
+ ),
+ )
+
+
+class DeleteCategoryManager(DeleteManager):
+ def __init__(
+ self,
+ back_option: str,
+ model_crud: CRUDBase = category_product_crud,
+ states_group: StatesGroup = CategoryDeleteState(),
+ ) -> None:
+ super().__init__(model_crud, back_option, states_group)
+
+ async def select_obj_to_delete(
+ self,
+ product_id: int,
+ callback: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
+ ) -> None:
+ obj_list = await self.model_crud.get_category_by_product_id(
+ product_id, session
+ )
+ obj_names = [obj.name for obj in obj_list]
+ obj_ids = [obj.id for obj in obj_list]
+ await callback.message.edit_text(
+ "Какие данные удалить?",
+ reply_markup=await get_inline_keyboard(
+ options=obj_names,
+ callback=obj_ids,
+ previous_menu=self.back_option,
+ ),
+ )
+ await state.set_state(self.states_group.select)
+
+ async def confirm_delete(
+ self,
+ callback: CallbackQuery,
+ state: FSMContext,
+ session: AsyncSession,
+ ) -> None:
+ """Подтвердить выбор объекта для удаления."""
+ self.obj_to_delete = await self.model_crud.get(callback.data, session)
+ await callback.message.edit_text(
+ f"Вы уверены, что хотите удалить эти данные?\n\n {self.obj_to_delete.name}",
+ reply_markup=await get_inline_confirmation(
+ cancel_option=self.back_option
+ ),
+ ),
+ await state.set_state(self.states_group.confirm)
diff --git a/app/admin/admin_managers/create_manager.py b/app/admin/admin_managers/create_manager.py
index 2b9907c..35ef004 100644
--- a/app/admin/admin_managers/create_manager.py
+++ b/app/admin/admin_managers/create_manager.py
@@ -3,6 +3,8 @@
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
+from crud.base_crud import CRUDBase
+
from .base_manager import (
BaseAdminManager,
)
@@ -57,17 +59,23 @@ class CreateManager(BaseAdminManager):
Добавляет объект в базу данных и сбрасывает машинное состояние.
"""
- async def select_data_type(
- self, callback: CallbackQuery, state: FSMContext
- ):
+ def __init__(
+ self,
+ model_crud: CRUDBase,
+ back_option: str,
+ states_group: StatesGroup = CreateState(),
+ ) -> None:
+ super().__init__(model_crud, back_option, states_group)
+
+ async def select_data_type(self, callback: CallbackQuery, state: FSMContext):
"""Выбрать тип данных для модели в БД."""
- await callback.message.edit_text(
+ await callback.message.answer(
"Выбирите способ передачи информации:",
reply_markup=await get_inline_keyboard(
ADMIN_CONTENT_BUTTONS, previous_menu=self.back_option
),
)
- await state.set_state(CreateState.select)
+ await state.set_state(self.states_group.select)
async def add_obj_name(
self,
@@ -84,7 +92,7 @@ async def add_obj_name(
previous_menu=self.back_option
),
)
- await state.set_state(CreateState.name)
+ await state.set_state(self.states_group.name)
async def prompt_for_input(
self,
@@ -97,7 +105,9 @@ async def prompt_for_input(
Добавить название объекта в state_data и перейти
к заполнению следующего поля.
"""
- await state.update_data(name=message.text)
+ data = await state.get_data()
+ if not data.get("name"):
+ await state.update_data(name=message.text)
await message.answer(
message_text,
reply_markup=await get_inline_keyboard(
@@ -119,7 +129,7 @@ async def add_obj_url(self, message: Message, state: FSMContext):
message,
message_text,
state,
- next_state=CreateState.url,
+ next_state=self.states_group.url,
)
async def add_obj_description(self, message: Message, state: FSMContext):
@@ -132,7 +142,7 @@ async def add_obj_description(self, message: Message, state: FSMContext):
message,
message_text,
state,
- next_state=CreateState.description,
+ next_state=self.states_group.description,
)
async def add_obj_media(self, message: Message, state: FSMContext):
@@ -145,7 +155,7 @@ async def add_obj_media(self, message: Message, state: FSMContext):
message,
message_text,
state,
- next_state=CreateState.media,
+ next_state=self.states_group.media,
)
async def add_obj_to_db(
@@ -154,11 +164,11 @@ async def add_obj_to_db(
"""Добавить объект в БД и сбросить машинное состояние."""
try:
current_state = await state.get_state()
- if current_state == CreateState.url.state:
+ if current_state == self.states_group.url.state:
await state.update_data(url=message.text)
- elif current_state == CreateState.description.state:
+ elif current_state == self.states_group.description.state:
await state.update_data(description=message.text)
- elif current_state == CreateState.media.state:
+ elif current_state == self.states_group.media.state:
await state.update_data(
media=message.photo[-1].file_id,
description=message.caption,
diff --git a/app/admin/admin_managers/delete_manager.py b/app/admin/admin_managers/delete_manager.py
index 186e071..576ed44 100644
--- a/app/admin/admin_managers/delete_manager.py
+++ b/app/admin/admin_managers/delete_manager.py
@@ -3,6 +3,8 @@
from aiogram.types import CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession
+from crud.base_crud import CRUDBase
+
from .base_manager import BaseAdminManager
from admin.keyboards.keyboards import (
get_inline_confirmation,
@@ -44,6 +46,13 @@ class DeleteManager(BaseAdminManager):
delete_obj(callback: CallbackQuery, state: FSMContext, session: AsyncSession) -> None:
Удаляет выбранный объект из базы данных и сбрасывает состояние.
"""
+ def __init__(
+ self,
+ model_crud: CRUDBase,
+ back_option: str,
+ states_group: StatesGroup = DeleteState(),
+ ) -> None:
+ super().__init__(model_crud, back_option, states_group)
async def get_all_model_names(self, session: AsyncSession) -> list[str]:
"""Получить список названий объектов из таблицы БД."""
@@ -56,6 +65,7 @@ async def select_obj_to_delete(
state: FSMContext,
session: AsyncSession,
) -> None:
+ """Выбрать объкт для удаления."""
obj_list_by_name = await self.get_all_model_names(session)
await callback.message.edit_text(
"Какие данные удалить?",
@@ -63,7 +73,7 @@ async def select_obj_to_delete(
obj_list_by_name, previous_menu=self.back_option
),
)
- await state.set_state(DeleteState.select)
+ await state.set_state(self.states_group.select)
async def confirm_delete(
self,
@@ -71,6 +81,7 @@ async def confirm_delete(
state: FSMContext,
session: AsyncSession,
) -> None:
+ """Подтвердить выбор объекта для удаления."""
self.obj_to_delete = await self.model_crud.get_by_string(
callback.data, session
)
@@ -80,7 +91,7 @@ async def confirm_delete(
cancel_option=self.back_option
),
),
- await state.set_state(DeleteState.confirm)
+ await state.set_state(self.states_group.confirm)
async def delete_obj(
self,
diff --git a/app/admin/admin_managers/update_manager.py b/app/admin/admin_managers/update_manager.py
index 4ed1734..4ec2efe 100644
--- a/app/admin/admin_managers/update_manager.py
+++ b/app/admin/admin_managers/update_manager.py
@@ -2,6 +2,7 @@
from aiogram.types import CallbackQuery, Message
from aiogram.fsm.state import State, StatesGroup
from sqlalchemy.ext.asyncio import AsyncSession
+from crud.base_crud import CRUDBase
from crud.portfolio_projects_crud import portfolio_crud
from .base_manager import (
@@ -58,6 +59,14 @@ class UpdateManager(BaseAdminManager):
Вносит изменения в объект в БД и сбрасывает состояние.
"""
+ def __init__(
+ self,
+ model_crud: CRUDBase,
+ back_option: str,
+ states_group: StatesGroup = UpdateState(),
+ ) -> None:
+ super().__init__(model_crud, back_option, states_group)
+
async def get_all_model_names(self, session: AsyncSession) -> list[str]:
"""Получить список названий объектов из таблицы БД."""
models = await self.model_crud.get_multi(session)
@@ -76,7 +85,7 @@ async def select_obj_to_update(
obj_list_by_name, previous_menu=self.back_option
),
)
- await state.set_state(UpdateState.select)
+ await state.set_state(self.states_group.select)
async def select_data_to_update(
self,
@@ -108,7 +117,7 @@ async def change_obj_name(
previous_menu=self.back_option
),
)
- await state.set_state(UpdateState.name)
+ await state.set_state(self.states_group.name)
async def change_obj_content(
self, callback: CallbackQuery, state: FSMContext
@@ -121,7 +130,7 @@ async def change_obj_content(
f"Текущий адрес ссылки: \n\n {self.obj_to_update.url} \n\n"
"Введите новый:"
)
- await state.set_state(UpdateState.url)
+ await state.set_state(self.states_group.url)
elif (
"description" in obj_fields and self.obj_to_update.description
):
@@ -129,7 +138,7 @@ async def change_obj_content(
f"Текущий текст: \n\n {self.obj_to_update.description} \n\n"
"Введите новый:"
)
- await state.set_state(UpdateState.description)
+ await state.set_state(self.states_group.description)
await callback.message.edit_text(
message_text,
reply_markup=await get_inline_keyboard(
@@ -148,7 +157,7 @@ async def change_obj_content(
previous_menu=self.back_option
),
)
- await state.set_state(UpdateState.media)
+ await state.set_state(self.states_group.media)
async def update_obj_in_db(
self, message: Message, state: FSMContext, session: AsyncSession
@@ -156,13 +165,13 @@ async def update_obj_in_db(
"""Внести изменения объекта в БД."""
current_state = await state.get_state()
- if current_state == UpdateState.name.state:
+ if current_state == self.states_group.name.state:
await state.update_data(name=message.text)
- elif current_state == UpdateState.url.state:
+ elif current_state == self.states_group.url.state:
await state.update_data(url=message.text)
- elif current_state == UpdateState.description.state:
+ elif current_state == self.states_group.description.state:
await state.update_data(description=message.text)
- elif current_state == UpdateState.media.state:
+ elif current_state == self.states_group.media.state:
await state.update_data(
media=message.photo[-1].file_id,
description=message.caption,
@@ -212,7 +221,7 @@ async def update_main_portfolio_url(
f"Текущий адрес ссылки: \n\n {self.obj_to_update.url} \n\n"
"Введите новый:"
)
- await state.set_state(UpdateState.portolio)
+ await state.set_state(self.states_group.portolio)
await callback.message.edit_text(
message_text,
reply_markup=await get_inline_keyboard(
diff --git a/app/admin/admin_settings.py b/app/admin/admin_settings.py
index 0be946e..54c715f 100644
--- a/app/admin/admin_settings.py
+++ b/app/admin/admin_settings.py
@@ -44,7 +44,7 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
ADMIN_UPDATE_BUTTONS = get_buttons(ADMIN_UPDATE_OPTIONS)
ADMIN_CONTENT_OPTIONS = {
"url": "Ссылка",
- "text": "Текст",
+ "description": "Текст",
"media": "Картинка",
}
ADMIN_CONTENT_BUTTONS = get_buttons(ADMIN_CONTENT_OPTIONS)
@@ -99,7 +99,6 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
PRODUCT_LIST_TEXT = (
"Мы предлагаем следющие продукты и услуги. Что Вас интересует?"
)
-PRODUCT_LIST = []
# Константы проекта
DEFAULT_STR_LEN = 150
diff --git a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
index 09014db..60f2920 100644
--- a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
@@ -98,7 +98,7 @@ async def about_info_to_update(
F.data != PREVIOUS_MENU,
),
)
-async def update_info_choice(callback: CallbackQuery, session: FSMContext):
+async def update_info_choice(callback: CallbackQuery, session: AsyncSession):
"""Обработать выбор данных для обновления."""
await about_update_manager.select_data_to_update(callback, session)
diff --git a/app/admin/handlers/admin_handlers/admin_category_handlers.py b/app/admin/handlers/admin_handlers/admin_category_handlers.py
index 1926f46..b2f6a4d 100644
--- a/app/admin/handlers/admin_handlers/admin_category_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_category_handlers.py
@@ -1,75 +1,54 @@
from aiogram import F, Router
from aiogram.filters import or_f, and_f
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from admin.handlers.admin_handlers.admin import SectionState
-from crud.category_product import category_product_crud
-from crud.product_crud import product_crud
from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.admin_managers import (
+ CreateCategoryManager,
+ UpdateCategoryManager,
+ DeleteCategoryManager,
+ CategoryCreateState,
+ CategoryUpdateState,
+ CategoryDeleteState,
+)
from admin.handlers.user import ProductCategory
-from admin.keyboards.keyboards import (
- get_inline_confirmation,
- get_inline_keyboard,
+from admin.admin_settings import (
+ ADMIN_BASE_OPTIONS,
+ ADMIN_CONTENT_OPTIONS,
+ ADMIN_UPDATE_OPTIONS,
+ MAIN_MENU_OPTIONS,
)
-
-from admin.admin_settings import MAIN_MENU_OPTIONS, admin_list
+from admin.keyboards.keyboards import get_inline_keyboard
+from crud.category_product import category_product_crud
+from crud.product_crud import product_crud
category_router = Router()
category_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+PREVIOUS_MENU = "Назад"
-class AddCategory(StatesGroup):
- name = State()
- url = State()
- media = State()
- description = State()
-
-
-class UpdateCategory(StatesGroup):
- name = State()
- url = State()
- media = State()
- description = State()
- select = State()
-
+category_create_manager = CreateCategoryManager(PREVIOUS_MENU)
+category_update_manager = UpdateCategoryManager(PREVIOUS_MENU)
+category_delete_manager = DeleteCategoryManager(PREVIOUS_MENU)
-class DeleteCategory(AddCategory):
- confirm = State()
-
-async def get_category_list(state: FSMContext, session: AsyncSession):
- """Получить список вариантов для проекта."""
+async def get_categoties_by_product_id(state: FSMContext):
+ """Получить id продукта из состояния."""
fsm_data = await state.get_data()
- product_id = fsm_data.get("product_id")
- return [
- category
- for category in await category_product_crud.get_multi_for_product(
- product_id, session
- )
- ]
-
-
-async def get_category_by_name(
- field_name: str, state: FSMContext, session: AsyncSession
-):
- fsm_data = await state.get_data()
- product_id = fsm_data.get("product_id")
- return await category_product_crud.get_category_by_name(
- product_id=product_id, field_name=field_name, session=session
- )
+ return fsm_data.get("product_id")
@category_router.callback_query(
or_f(
- AddCategory(),
- UpdateCategory(),
- DeleteCategory(),
+ CategoryCreateState(),
+ CategoryUpdateState(),
+ CategoryDeleteState(),
SectionState.category,
),
- or_f(F.data == "Назад", F.data == "Отмена"),
+ or_f(F.data == PREVIOUS_MENU),
)
async def get_back_to_category_menu(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
@@ -83,241 +62,159 @@ async def get_back_to_category_menu(
categories_by_name = [category.name for category in categories]
urls = [category.url for category in categories]
await callback.message.edit_text(
- f"{product.response}",
+ f"{product.description}",
reply_markup=await get_inline_keyboard(
categories_by_name,
urls=urls,
previous_menu=MAIN_MENU_OPTIONS.get("products"),
- is_admin=callback.from_user.id in admin_list,
admin_update_menu=callback.data,
),
)
await state.set_state(ProductCategory.product_id)
-@category_router.callback_query(SectionState.category, F.data == "Добавить")
-async def add_new_category(callback: CallbackQuery, state: FSMContext):
- """Добавить основные варианты для продукта."""
- await callback.message.answer(
- "Введите название для дополнительной информации",
- reply_markup=await get_inline_keyboard(previous_menu="Назад"),
- )
- await state.set_state(AddCategory.name)
+@category_router.callback_query(
+ SectionState.category, F.data == ADMIN_BASE_OPTIONS.get("create")
+)
+async def select_new_category_type(callback: CallbackQuery, state: FSMContext):
+ """Выбрать тип данных для дополнительной информации о продукте."""
+ await category_create_manager.select_data_type(callback, state)
-@category_router.message(AddCategory.name, F.text)
+@category_router.callback_query(
+ CategoryCreateState.select,
+ or_f(
+ F.data == ADMIN_CONTENT_OPTIONS.get("url"),
+ F.data == ADMIN_CONTENT_OPTIONS.get("description"),
+ F.data == ADMIN_CONTENT_OPTIONS.get("media"),
+ ),
+)
async def add_product_category_name(
- message: Message,
+ callback: CallbackQuery,
state: FSMContext,
):
- """Выбрать тип данных для основных вариантов."""
+ """Добавить название."""
+ product_id = await get_categoties_by_product_id(state)
+ await category_create_manager.add_obj_name(product_id, callback, state)
- await state.update_data(name=message.text)
- await message.answer(
- "Выберете способ передачи информации:",
- reply_markup=await get_inline_keyboard(
- ["Ссылка", "Текст", "Картинка"],
- previous_menu="Назад",
- ),
- )
+@category_router.message(CategoryCreateState.url, F.text)
+async def add_product_category_url(message: Message, state: FSMContext):
+ """Добавить ссылку."""
+ await category_create_manager.add_obj_url(message, state)
-@category_router.callback_query(
- or_f(AddCategory.name, AddCategory.description),
- or_f(F.data == "Ссылка", F.data == "Текст", F.data == "Картинка"),
-)
-async def add_product_category_data(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
+
+@category_router.message(CategoryCreateState.description, F.text)
+async def add_product_category_description(
+ message: Message, state: FSMContext
):
- """Добавить информацию в основной вариант."""
-
- if callback.data == "Ссылка":
- info_type = "ссылку"
- await state.set_state(AddCategory.url)
- elif callback.data == "Текст":
- info_type = "текст"
- fsm_data = await state.get_data()
- category_name = fsm_data.get("select")
- product_id = fsm_data.get("product_id")
- category = await category_product_crud.get_category_by_name(
- product_id, category_name, session
- )
- if category:
- await state.set_state(UpdateCategory.description)
- else:
- await state.set_state(AddCategory.description)
- elif callback.data == "Картинка":
- info_type = "Картинку"
- await state.set_state(AddCategory.media)
- await callback.message.answer(
- f"Добавьте {info_type}",
- reply_markup=await get_inline_keyboard(previous_menu="Назад"),
- )
+ """Добавить текст."""
+ await category_create_manager.add_obj_description(message, state)
-@category_router.message(
- or_f(AddCategory.media, UpdateCategory.media), F.photo
-)
-async def add_media_description(message: Message, state: FSMContext):
- """Добавить описание к картинке."""
- await state.update_data(media=message.photo[-1].file_id)
- await message.answer(
- "Добавить описание к картинке?",
- reply_markup=await get_inline_confirmation(
- "Текст", cancel_option="Нет"
- ),
- )
- await state.set_state(AddCategory.description)
+@category_router.message(CategoryCreateState.media, F.text)
+async def add_product_category_media(message: Message, state: FSMContext):
+ """Добавить картинку."""
+ await category_create_manager.add_obj_media(message, state)
@category_router.message(
- or_f(AddCategory.description, AddCategory.url),
- or_f(F.text, F.photo, F.data == "Нет"),
+ or_f(
+ CategoryCreateState.description,
+ CategoryCreateState.url,
+ CategoryCreateState.media,
+ ),
+ or_f(F.text, F.photo),
)
async def create_product_with_data(
message: Message, state: FSMContext, session: AsyncSession
):
- """Создать вариант для продукта в БД и предложить добавить следующий."""
- current_state = await state.get_state()
- if current_state == AddCategory.description:
- await state.update_data(description=message.text)
- elif current_state == AddCategory.url:
- await state.update_data(url=message.text)
- data = await state.get_data()
- await category_product_crud.create(data, session)
- await message.answer(
- "Информация добавлена!",
- reply_markup=await get_inline_keyboard(previous_menu="Назад"),
- )
+ """
+ Создать информацию для продукта в БД и предложить добавить следующий.
+ """
+ await category_create_manager.add_obj_to_db(message, state, session)
-@category_router.callback_query(SectionState.category, F.data == "Удалить")
+@category_router.callback_query(
+ SectionState.category, F.data == ADMIN_BASE_OPTIONS.get("delete")
+)
async def product_to_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- """Выбор продукта на удаление."""
- categories = [
- category.name for category in await get_category_list(state, session)
- ]
- await callback.message.edit_text(
- "Какой проект вы хотите удалить?",
- reply_markup=await get_inline_keyboard(
- options=categories,
- previous_menu="Назад",
- ),
+ """Выбор информации на удаление."""
+ product_id = await get_categoties_by_product_id(state)
+ await category_delete_manager.select_obj_to_delete(
+ product_id, callback, state, session
)
- await state.set_state(DeleteCategory.name)
-@category_router.callback_query(DeleteCategory.name, F.data)
+@category_router.callback_query(CategoryDeleteState.select, F.data)
async def confirm_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Подтверждение удаления."""
- category = await get_category_by_name(callback.data, state, session)
- await callback.message.edit_text(
- f"Вы уверены, что хотите удалить этот проект?\n\n {category.name}",
- reply_markup=await get_inline_confirmation(
- option=category.name, cancel_option="Назад"
- ),
- )
- await state.set_state(DeleteCategory.confirm)
+ await category_delete_manager.confirm_delete(callback, state, session)
-@category_router.callback_query(DeleteCategory.confirm, F.data != "Назад")
-async def delete_product(
+@category_router.callback_query(
+ CategoryDeleteState.confirm, F.data != PREVIOUS_MENU
+)
+async def delete_category(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- """Удалить продукт из БД."""
- category = await get_category_by_name(callback.data, state, session)
- await category_product_crud.remove(category, session)
- await callback.message.edit_text(
- "Услуга удалена!",
- reply_markup=await get_inline_keyboard(previous_menu="Назад"),
- )
+ """Удалить информацию из БД."""
+ await category_delete_manager.delete_obj(callback, state, session)
-@category_router.callback_query(SectionState.category, F.data == "Изменить")
-async def product_to_update(
+@category_router.callback_query(
+ SectionState.category, F.data == ADMIN_BASE_OPTIONS.get("update")
+)
+async def category_to_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- """Выбор продукта для редактирования."""
- categories = [
- category.name for category in await get_category_list(state, session)
- ]
- await callback.message.edit_text(
- "Какую услугу вы хотите отредактировать?",
- reply_markup=await get_inline_keyboard(
- options=categories, previous_menu="Назад"
- ),
+ """Выбор информации для редактирования."""
+ product_id = await get_categoties_by_product_id(state)
+ await category_update_manager.select_obj_to_update(
+ product_id, callback, state, session
)
- await state.set_state(UpdateCategory.select)
@category_router.callback_query(
- UpdateCategory.select,
- and_f(F.data != "Название", F.data != "Содержание"),
+ CategoryUpdateState.select,
+ and_f(
+ F.data != ADMIN_UPDATE_OPTIONS.get("name"),
+ F.data != ADMIN_UPDATE_OPTIONS.get("content"),
+ ),
)
-async def update_portfolio_project_choise(
- callback: CallbackQuery, state: FSMContext
+async def select_category_data_to_update(
+ callback: CallbackQuery, session: AsyncSession
):
"""Выбор поля для редактирования."""
- await state.update_data(select=callback.data)
- await callback.message.edit_text(
- "Что вы хотите отредактировать?",
- reply_markup=await get_inline_keyboard(
- ["Название", "Содержание"], previous_menu="Назад"
- ),
- )
+ await category_update_manager.select_data_to_update(callback, session)
-@category_router.callback_query(UpdateCategory.select, F.data == "Название")
-async def about_name_update(callback: CallbackQuery, state: FSMContext):
- """Ввести новое название продукта."""
- fsm_data = await state.get_data()
- category_name = fsm_data.get("select")
- await callback.message.answer(
- f"Текущее название:\n\n {category_name}\n\n Введите новое название"
- )
- await state.set_state(UpdateCategory.name)
+@category_router.callback_query(
+ CategoryUpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
+)
+async def category_name_update(callback: CallbackQuery, state: FSMContext):
+ """Ввести новое название информации."""
+ await category_update_manager.change_obj_name(callback, state)
-@category_router.callback_query(UpdateCategory.select, F.data == "Содержание")
-async def about_url_update(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- """Изменить содержание продукта."""
- fsm_data = await state.get_data()
- category_choice = fsm_data.get("select")
- category = await get_category_by_name(category_choice, state, session)
- if category.url:
- await callback.message.answer(
- f"Текущий адрес ссылки:\n\n {category.url}\n\n Введите новый адрес ссылки:"
- )
- await state.set_state(UpdateCategory.url)
- if category.description and not category.media:
- await callback.message.answer(
- f"Текущий текст:\n\n {category.description}\n\n Введите новый текст:"
- )
- await state.set_state(UpdateCategory.description)
- if category.media:
- await callback.message.answer("Текущая картинка:")
- await callback.message.answer_photo(
- photo=category.media, caption=category.description
- )
- await callback.message.answer(
- "Добавьте новую картинку и описание",
- reply_markup=await get_inline_keyboard(previous_menu="Назад"),
- )
- await state.set_state(UpdateCategory.media)
+@category_router.callback_query(
+ CategoryUpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
+)
+async def about_url_update(callback: CallbackQuery, state: FSMContext):
+ """Изменить содержание информации."""
+ await category_update_manager.change_obj_content(callback, state)
@category_router.message(
or_f(
- UpdateCategory.name,
- UpdateCategory.media,
- UpdateCategory.url,
- UpdateCategory.description,
+ CategoryUpdateState.name,
+ CategoryUpdateState.media,
+ CategoryUpdateState.url,
+ CategoryUpdateState.description,
),
or_f(
F.text,
@@ -327,24 +224,5 @@ async def about_url_update(
async def update_about_info(
message: Message, state: FSMContext, session: AsyncSession
):
- """Внести изменения продукта в БД."""
- current_state = await state.get_state()
- old_data = await state.get_data()
- print(old_data)
- old_category_data = await get_category_by_name(
- old_data.get("select"), state, session
- )
- if current_state == UpdateCategory.name:
- await state.update_data(name=message.text)
- elif current_state == UpdateCategory.url:
- await state.update_data(url=message.text)
- elif current_state == UpdateCategory.media:
- await state.update_data(media=message.photo)
- elif current_state == UpdateCategory.description:
- await state.update_data(description=message.text)
- update_data = await state.get_data()
- await category_product_crud.update(old_category_data, update_data, session)
- await message.answer(
- "Информация обновлена!",
- reply_markup=await get_inline_keyboard(previous_menu="Назад"),
- )
+ """Внести изменения информации в БД."""
+ await category_update_manager.update_obj_in_db(message, state, session)
diff --git a/app/admin/handlers/admin_handlers/admin_product_handlers.py b/app/admin/handlers/admin_handlers/admin_product_handlers.py
index 9bf9456..8d8207a 100644
--- a/app/admin/handlers/admin_handlers/admin_product_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_product_handlers.py
@@ -1,25 +1,24 @@
from aiogram import F, Router
from aiogram.filters import or_f, and_f
from aiogram.fsm.context import FSMContext
-from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from .admin import SectionState
-from crud.category_product import category_product_crud
from crud.product_crud import product_crud
from admin.filters.filters import ChatTypeFilter, IsAdmin
from admin.admin_managers import (
- DeleteManager,
DeleteState,
+ UpdateState,
CreateState,
CreateManager,
UpdateManager,
- UpdateState,
+ DeleteManager,
)
from admin.admin_settings import (
ADMIN_BASE_OPTIONS,
+ ADMIN_UPDATE_OPTIONS,
MAIN_MENU_OPTIONS,
)
@@ -32,30 +31,6 @@
product_update_manager = UpdateManager(product_crud, PREVIOUS_MENU)
product_delete_manager = DeleteManager(product_crud, PREVIOUS_MENU)
-# class AddProduct(StatesGroup):
-# title = State()
-# response = State()
-
-
-# class UpdateProduct(StatesGroup):
-# select = State()
-# title = State()
-# response = State()
-# confirm = State()
-
-
-# class DeleteProduct(AddProduct):
-# confirm = State()
-
-
-class AddProductInfo(StatesGroup):
- name = State()
- product_id = State()
- url = State()
- media = State()
- description = State()
- media_description = State()
-
@product_router.callback_query(
SectionState.product, F.data == ADMIN_BASE_OPTIONS.get("create")
@@ -80,258 +55,83 @@ async def creeate_product(
await product_create_manager.add_obj_to_db(message, state, session)
-@product_router.callback_query(AddProductInfo.product_id, F.data == "Да")
-async def add_product_categoty(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- """Добавить основные варианты для продукта."""
-
- await state.clear()
-
- last_product = await product_crud.get_last_added_product(session)
- await state.update_data(product_id=last_product.id)
-
- await callback.message.answer(
- "Введите название для дополнительной информации"
- )
-
- await state.set_state(AddProductInfo.name)
-
-
-@product_router.message(AddProductInfo.name, F.text)
-async def add_product_category_name(message: Message, state: FSMContext):
- """Выбрать тип данных для основных вариантов."""
-
- await state.update_data(name=message.text)
-
- await message.answer(
- "Выберете способ передачи информации:",
- reply_markup=await get_inline_keyboard(
- ["Ссылка", "Текст", "Картинка"], previous_menu=PREVIOUS_MENU
- ),
- )
-
-
@product_router.callback_query(
- or_f(AddProductInfo.name, AddProductInfo.description),
- or_f(F.data == "Ссылка", F.data == "Текст", F.data == "Картинка"),
+ SectionState.product, F.data == ADMIN_BASE_OPTIONS.get("delete")
)
-async def add_product_category_data(
- callback: CallbackQuery, state: FSMContext
-):
- """Добавить информацию в основной вариант."""
-
- if callback.data == "Ссылка":
- await state.set_state(AddProductInfo.url)
- info_type = "ссылку"
- elif callback.data == "Текст":
- await state.set_state(AddProductInfo.description)
- info_type = "текст"
- elif callback.data == "Картинка":
- await state.set_state(AddProductInfo.media)
- info_type = "Картинку"
- await callback.message.answer(f"Добавьте {info_type}")
-
-
-@product_router.message(AddProductInfo.media, F.photo)
-async def add_media_description(message: Message, state: FSMContext):
- """Добавить описание к картинке."""
-
- await state.update_data(media=message.photo[-1].file_id)
-
- await message.answer(
- "Добавить описание к картинке?",
- reply_markup=await get_inline_confirmation(
- "Текст", cancel_option="Нет"
- ),
- )
-
- print(await state.get_data())
- await state.set_state(AddProductInfo.description)
-
-
-@product_router.message(
- or_f(AddProductInfo.description, AddProductInfo.url),
- or_f(F.text, F.photo, F.data == "Нет"),
-)
-async def create_product_with_data(
- message: Message, state: FSMContext, session: AsyncSession
-):
- """Создать вариант для продукта в БД и предложить добавить следующий."""
-
- current_state = await state.get_state()
- if current_state == AddProductInfo.description:
- await state.update_data(description=message.text)
- elif current_state == AddProductInfo.url:
- await state.update_data(url=message.text)
- data = await state.get_data()
-
- await category_product_crud.create(data, session)
- await message.answer(
- "Информация добавлена! Добавить еще?",
- reply_markup=await get_inline_confirmation(
- option="Да", cancel_option=PREVIOUS_MENU
- ),
- )
-
- await state.set_state(AddProductInfo.product_id)
-
-
-@product_router.callback_query(SectionState.product, F.data == "Удалить")
async def product_to_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- """Выбор продукта на удаление."""
+ """Выбор продукта для удаление."""
+ await product_delete_manager.select_obj_to_delete(callback, state, session)
- await callback.message.edit_text(
- "Какой проект вы хотите удалить?",
- reply_markup=await get_inline_keyboard(
- options=await get_products_list(session),
- previous_menu=PREVIOUS_MENU,
- ),
- )
- await state.set_state(DeleteProduct.title)
-
-
-@product_router.callback_query(DeleteProduct.title, F.data)
+@product_router.callback_query(DeleteState.select, F.data != PREVIOUS_MENU)
async def confirm_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Подтверждение удаления."""
- portfolio_project = await product_crud.get_by_product_name(
- callback.data, session
- )
- await callback.message.edit_text(
- f"Вы уверены, что хотите удалить "
- f"этот проект?\n\n {portfolio_project.title}",
- reply_markup=await get_inline_confirmation(
- option=portfolio_project.title, cancel_option=PREVIOUS_MENU
- ),
- )
+ await product_delete_manager.confirm_delete(callback, state, session)
- await state.set_state(DeleteProduct.confirm)
-
-@product_router.callback_query(DeleteProduct.confirm, F.data != PREVIOUS_MENU)
+@product_router.callback_query(DeleteState.confirm, F.data != PREVIOUS_MENU)
async def delete_product(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Удалить продукт из БД."""
- portfolio_project = await product_crud.get_by_product_name(
- callback.data, session
- )
-
- await product_crud.remove(portfolio_project, session)
- await callback.message.edit_text(
- "Услуга удалена!",
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
+ await product_delete_manager.delete_obj(callback, state, session)
- await state.clear()
-
-@product_router.callback_query(SectionState.product, F.data == "Изменить")
+@product_router.callback_query(
+ SectionState.product, F.data == ADMIN_BASE_OPTIONS.get("update")
+)
async def product_to_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Выбор продукта для редактирования."""
- await callback.message.edit_text(
- "Какую услугу вы хотите отредактировать?",
- reply_markup=await get_inline_keyboard(
- options=await get_products_list(session),
- previous_menu=PREVIOUS_MENU,
- ),
- ),
-
- await state.set_state(UpdateProduct.select)
+ await product_update_manager.select_obj_to_update(callback, state, session)
@product_router.callback_query(
- UpdateProduct.select,
+ UpdateState.select,
and_f(
- F.data != "Название проекта",
- F.data != "Описание",
+ F.data != ADMIN_UPDATE_OPTIONS.get("name"),
+ F.data != ADMIN_UPDATE_OPTIONS.get("content"),
F.data != PREVIOUS_MENU,
),
)
-async def update_portfolio_project_choise(
- callback: CallbackQuery, state: FSMContext
-):
+async def update_choice(callback: CallbackQuery, session: AsyncSession):
"""Выбор поля для редактирования."""
- await state.update_data(select=callback.data)
- await callback.message.edit_text(
- "Что вы хотите отредактировать?",
- reply_markup=await get_inline_keyboard(
- ["Название проекта", "Описание"], previous_menu=PREVIOUS_MENU
- ),
- )
+ await product_update_manager.select_data_to_update(callback, session)
@product_router.callback_query(
- UpdateProduct.select, F.data == "Название проекта"
+ UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
)
-async def about_name_update(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
+async def about_name_update(callback: CallbackQuery, state: FSMContext):
"""Ввести новое название продукта."""
- product_data = await state.get_data()
- product_name = product_data.get("select")
+ await product_update_manager.change_obj_name(callback, state)
- await callback.message.answer(
- f"Текущее название:\n\n {product_name}\n\n Введите новое название"
- )
- await state.set_state(UpdateProduct.title)
-
-
-@product_router.callback_query(UpdateProduct.select, F.data == "Описание")
-async def about_url_update(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
+@product_router.callback_query(
+ UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
+)
+async def about_url_update(callback: CallbackQuery, state: FSMContext):
"""Ввести новое описание продукта."""
- product_data = await state.get_data()
- product_title = product_data.get("select")
- product = await product_crud.get_by_product_name(product_title, session)
-
- await callback.message.answer(
- f"Текущее описание:\n\n {product.response}\n\n Введите новое описание"
- )
-
- await state.set_state(UpdateProduct.response)
+ await product_update_manager.change_obj_content(callback, state)
@product_router.message(
- or_f(UpdateProduct.title, UpdateProduct.response), F.text
+ or_f(UpdateState.name, UpdateState.description), F.text
)
async def update_about_info(
message: Message, state: FSMContext, session: AsyncSession
):
"""Внести изменения продукта в БД."""
- current_state = await state.get_state()
- old_data = await state.get_data()
- old_product_data = await product_crud.get_by_product_name(
- old_data.get("select"), session
- )
-
- if current_state == UpdateProduct.title:
- await state.update_data(title=message.text)
- elif current_state == UpdateProduct.response:
- await state.update_data(response=message.text)
-
- update_data = await state.get_data()
- await product_crud.update(old_product_data, update_data, session)
-
- await message.answer(
- "Информация обновлена!",
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
-
- await state.clear()
+ await product_update_manager.update_obj_in_db(message, state, session)
From 648d6d02b6bfd1974f5792596caab93c5f09cd3c Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Sun, 13 Oct 2024 23:37:10 +0300
Subject: [PATCH 39/75] some fix
---
alembic.ini | 2 +-
{alembic => app/alembic}/README | 0
{alembic => app/alembic}/env.py | 0
{alembic => app/alembic}/script.py.mako | 0
.../alembic}/versions/fd795bdbba90_update_feedback.py | 0
app/bot/feedback_context.py | 1 -
app/models/models.py | 2 +-
7 files changed, 2 insertions(+), 3 deletions(-)
rename {alembic => app/alembic}/README (100%)
rename {alembic => app/alembic}/env.py (100%)
rename {alembic => app/alembic}/script.py.mako (100%)
rename {alembic => app/alembic}/versions/fd795bdbba90_update_feedback.py (100%)
diff --git a/alembic.ini b/alembic.ini
index d4ab1d7..ada65d0 100644
--- a/alembic.ini
+++ b/alembic.ini
@@ -83,7 +83,7 @@ sqlalchemy.url = driver://user:pass@localhost/dbname
keys = root,sqlalchemy,alembic
[handlers]
-keys = console
+keys = generic
[formatters]
keys = generic
diff --git a/alembic/README b/app/alembic/README
similarity index 100%
rename from alembic/README
rename to app/alembic/README
diff --git a/alembic/env.py b/app/alembic/env.py
similarity index 100%
rename from alembic/env.py
rename to app/alembic/env.py
diff --git a/alembic/script.py.mako b/app/alembic/script.py.mako
similarity index 100%
rename from alembic/script.py.mako
rename to app/alembic/script.py.mako
diff --git a/alembic/versions/fd795bdbba90_update_feedback.py b/app/alembic/versions/fd795bdbba90_update_feedback.py
similarity index 100%
rename from alembic/versions/fd795bdbba90_update_feedback.py
rename to app/alembic/versions/fd795bdbba90_update_feedback.py
diff --git a/app/bot/feedback_context.py b/app/bot/feedback_context.py
index 4127e16..f8fb850 100644
--- a/app/bot/feedback_context.py
+++ b/app/bot/feedback_context.py
@@ -71,7 +71,6 @@ async def process_description(
logger.info(f'Пользователь {message.from_user.id} ввел текст.')
feedback_data = await state.get_data()
- print(feedback_data)
await create_feedback(feedback_data, session)
logger.info(f'Запись создана в БД с ID: {feedback_data.id}.')
diff --git a/app/models/models.py b/app/models/models.py
index 4058445..2c3061f 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -6,7 +6,7 @@
from sqlalchemy.sql import func
import sqlalchemy.dialects.postgresql as pgsql_types
-from core.db import Base
+from app.core.db import Base
class RoleEnum(str, Enum):
From fdb89d869bb9b328d77f741565f2846ced7acd44 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Mon, 14 Oct 2024 00:01:50 +0300
Subject: [PATCH 40/75] done with product type and category type, fix models,
add nullabe true for category type content, add logging to admin zone
---
app/admin/admin_managers/category_manager.py | 9 +-
app/admin/admin_managers/create_manager.py | 58 +++++-
app/admin/admin_managers/delete_manager.py | 1 -
app/admin/admin_managers/update_manager.py | 2 -
.../admin_about_company_handlers.py | 72 +++++++-
.../admin_handlers/admin_category_handlers.py | 173 ++++++++++++++----
.../admin_handlers/admin_info_handlers.py | 61 +++++-
.../admin_portfolio_handlers.py | 81 +++++++-
.../admin_handlers/admin_product_handlers.py | 133 +++++++++++---
app/admin/handlers/user.py | 2 +-
..._add_description_fields_to_product_and_.py | 34 ----
...3_add_nullable_true_for_category_type_.py} | 13 +-
app/core/init_db.py | 4 +-
app/models/models.py | 4 +-
14 files changed, 519 insertions(+), 128 deletions(-)
delete mode 100644 app/alembic/versions/4c3a4964ff61_add_description_fields_to_product_and_.py
rename app/alembic/versions/{42e8d0d2bdd4_change_all_model_name_fields_to_name.py => ce91f683ad23_add_nullable_true_for_category_type_.py} (93%)
diff --git a/app/admin/admin_managers/category_manager.py b/app/admin/admin_managers/category_manager.py
index 2234d35..5b5aca0 100644
--- a/app/admin/admin_managers/category_manager.py
+++ b/app/admin/admin_managers/category_manager.py
@@ -7,7 +7,7 @@
get_inline_confirmation,
get_inline_keyboard,
)
-from admin.admin_settings import ADMIN_CONTENT_OPTIONS, ADMIN_UPDATE_BUTTONS
+from admin.admin_settings import ADMIN_UPDATE_BUTTONS
from crud.base_crud import CRUDBase
from crud.category_product import category_product_crud
from .create_manager import CreateManager
@@ -62,12 +62,7 @@ async def add_obj_name(
previous_menu=self.back_option
),
)
- if callback.data == ADMIN_CONTENT_OPTIONS.get("url"):
- await state.set_state(self.states_group.url)
- elif callback.data == ADMIN_CONTENT_OPTIONS.get("description"):
- await state.set_state(self.states_group.description)
- elif callback.data == ADMIN_CONTENT_OPTIONS.get("media"):
- await state.set_state(self.states_group.media)
+ await state.set_state(self.states_group.name)
class UpdateCategoryManager(UpdateManager):
diff --git a/app/admin/admin_managers/create_manager.py b/app/admin/admin_managers/create_manager.py
index 35ef004..dcc11c7 100644
--- a/app/admin/admin_managers/create_manager.py
+++ b/app/admin/admin_managers/create_manager.py
@@ -67,9 +67,10 @@ def __init__(
) -> None:
super().__init__(model_crud, back_option, states_group)
- async def select_data_type(self, callback: CallbackQuery, state: FSMContext):
+ async def select_data_type(self, message: Message, state: FSMContext):
"""Выбрать тип данных для модели в БД."""
- await callback.message.answer(
+ await state.update_data(name=message.text)
+ await message.answer(
"Выбирите способ передачи информации:",
reply_markup=await get_inline_keyboard(
ADMIN_CONTENT_BUTTONS, previous_menu=self.back_option
@@ -105,9 +106,7 @@ async def prompt_for_input(
Добавить название объекта в state_data и перейти
к заполнению следующего поля.
"""
- data = await state.get_data()
- if not data.get("name"):
- await state.update_data(name=message.text)
+ await state.update_data(name=message.text)
await message.answer(
message_text,
reply_markup=await get_inline_keyboard(
@@ -132,6 +131,24 @@ async def add_obj_url(self, message: Message, state: FSMContext):
next_state=self.states_group.url,
)
+ async def add_obj_url_callback(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ """
+ Добавить ссылку к объекту и перейти в
+ следующее машинное состояние.
+ """
+ await callback.message.answer(
+ (
+ "Ссылка обязательно должна начинаться с 'https://'\n\n "
+ "Введите адрес ссылки:"
+ ),
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.set_state(self.states_group.url)
+
async def add_obj_description(self, message: Message, state: FSMContext):
"""
Добавить текст к объекту и перейти в
@@ -145,6 +162,21 @@ async def add_obj_description(self, message: Message, state: FSMContext):
next_state=self.states_group.description,
)
+ async def add_obj_description_callback(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ """
+ Добавить текст к объекту и перейти в
+ следующее машинное состояние.
+ """
+ await callback.message.answer(
+ "Введите текст:",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.set_state(self.states_group.description)
+
async def add_obj_media(self, message: Message, state: FSMContext):
"""
Добавить картинку к объекту и перейти в
@@ -158,6 +190,21 @@ async def add_obj_media(self, message: Message, state: FSMContext):
next_state=self.states_group.media,
)
+ async def add_obj_media_callback(
+ self, callback: CallbackQuery, state: FSMContext
+ ):
+ """
+ Добавить картинку к объекту и перейти в
+ следующее машинное состояние.
+ """
+ await callback.message.answer(
+ "Добавьте картинку и текст к ней:",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ await state.set_state(self.states_group.media)
+
async def add_obj_to_db(
self, message: Message, state: FSMContext, session: AsyncSession
):
@@ -183,6 +230,5 @@ async def add_obj_to_db(
previous_menu=self.back_option
),
)
- await state.clear()
except Exception as e:
await message.answer(f"Произошла ошибка: {e}")
diff --git a/app/admin/admin_managers/delete_manager.py b/app/admin/admin_managers/delete_manager.py
index 576ed44..6da9a76 100644
--- a/app/admin/admin_managers/delete_manager.py
+++ b/app/admin/admin_managers/delete_manager.py
@@ -108,6 +108,5 @@ async def delete_obj(
previous_menu=self.back_option
),
)
- await state.clear()
except Exception as e:
await callback.message.answer(f"Произошла ошибка: {e}")
diff --git a/app/admin/admin_managers/update_manager.py b/app/admin/admin_managers/update_manager.py
index 4ec2efe..3f7ab73 100644
--- a/app/admin/admin_managers/update_manager.py
+++ b/app/admin/admin_managers/update_manager.py
@@ -186,7 +186,6 @@ async def update_obj_in_db(
previous_menu=self.back_option
),
)
- await state.clear()
class UpdatePortfolio:
@@ -244,4 +243,3 @@ async def update_obj_in_db(
previous_menu=self.back_option
),
)
- await state.clear()
diff --git a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
index 60f2920..e83a97b 100644
--- a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
@@ -1,10 +1,14 @@
+import logging
+
from aiogram import F, Router
from aiogram.filters import and_f, or_f
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
+
from .admin import SectionState
+from bot.exceptions import message_exception_handler
from crud.about_crud import company_info_crud
from admin.filters.filters import ChatTypeFilter, IsAdmin
from admin.admin_managers import (
@@ -22,6 +26,8 @@
MAIN_MENU_OPTIONS,
)
+logger = logging.getLogger(__name__)
+
PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("company_bio")
about_router = Router()
@@ -32,28 +38,49 @@
about_update_manager = UpdateManager(company_info_crud, PREVIOUS_MENU)
+@message_exception_handler(
+ log_error_text="Ошибка при создании новой информации о компании"
+)
@about_router.callback_query(
SectionState.about, F.data == ADMIN_BASE_OPTIONS.get("create")
)
async def create_about_info(callback: CallbackQuery, state: FSMContext):
"""Запустить процесс создания новой информации о компании."""
await about_create_manager.add_obj_name(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} начал создание новой информации о компании."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при сохранении имени нового объекта"
+)
@about_router.message(CreateState.name, F.text)
async def add_info_name(message: Message, state: FSMContext):
"""Сохранить имя нового объекта в состояние."""
await about_create_manager.add_obj_url(message, state)
+ logger.info(
+ f"Пользователь {message.from_user.id} сохранил имя нового объекта."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при сохранении URL нового объекта"
+)
@about_router.message(CreateState.url, F.text)
async def add_about_data(
message: Message, state: FSMContext, session: AsyncSession
):
"""Сохранить URL нового объекта в базу данных."""
await about_create_manager.add_obj_to_db(message, state, session)
+ logger.info(
+ f"Пользователь {message.from_user.id} сохранил URL нового объекта."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при выборе объекта для удаления"
+)
@about_router.callback_query(
SectionState.about, F.data == ADMIN_BASE_OPTIONS.get("delete")
)
@@ -62,24 +89,42 @@ async def about_info_to_delete(
):
"""Запустить процесс выбора объекта для удаления."""
await about_delete_manager.select_obj_to_delete(callback, state, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал объект для удаления."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при подтверждении удаления информации"
+)
@about_router.callback_query(DeleteState.select, F.data != PREVIOUS_MENU)
async def confirm_delete_info(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- """Подтведить удаление выбранного объекта."""
+ """Подтвердить удаление выбранного объекта."""
await about_delete_manager.confirm_delete(callback, state, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} подтвердил удаление информации."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при удалении информации из базы данных"
+)
@about_router.callback_query(DeleteState.confirm, F.data != PREVIOUS_MENU)
async def delete_about_info(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Удалить выбранный объект из базы данных."""
await about_delete_manager.delete_obj(callback, state, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} удалил информацию из базы данных."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при выборе объекта для обновления"
+)
@about_router.callback_query(
SectionState.about, F.data == ADMIN_BASE_OPTIONS.get("update")
)
@@ -88,8 +133,14 @@ async def about_info_to_update(
):
"""Запустить процесс выбора объекта для обновления."""
await about_update_manager.select_obj_to_update(callback, state, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал объект для обновления."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при обработке выбора данных для обновления"
+)
@about_router.callback_query(
UpdateState.select,
and_f(
@@ -101,27 +152,46 @@ async def about_info_to_update(
async def update_info_choice(callback: CallbackQuery, session: AsyncSession):
"""Обработать выбор данных для обновления."""
await about_update_manager.select_data_to_update(callback, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал данные для обновления."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при обновлении имени объекта"
+)
@about_router.callback_query(
UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
)
async def about_name_update(callback: CallbackQuery, state: FSMContext):
"""Обновить имя объекта."""
await about_update_manager.change_obj_name(callback, state)
+ logger.info(f"Пользователь {callback.from_user.id} обновил имя объекта.")
+@message_exception_handler(
+ log_error_text="Ошибка при обновлении содержимого объекта"
+)
@about_router.callback_query(
UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
)
async def about_url_update(callback: CallbackQuery, state: FSMContext):
"""Обновить содержимое объекта."""
await about_update_manager.change_obj_content(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} обновил содержимое объекта."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при обновлении информации в базе данных"
+)
@about_router.message(or_f(UpdateState.name, UpdateState.url), F.text)
async def update_about_info(
message: Message, state: FSMContext, session: AsyncSession
):
"""Обновить объект в базе данных на основе нового содержимого."""
await about_update_manager.update_obj_in_db(message, state, session)
+ logger.info(
+ f"Пользователь {message.from_user.id} обновил информацию о компании в базе данных."
+ )
diff --git a/app/admin/handlers/admin_handlers/admin_category_handlers.py b/app/admin/handlers/admin_handlers/admin_category_handlers.py
index b2f6a4d..32790f4 100644
--- a/app/admin/handlers/admin_handlers/admin_category_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_category_handlers.py
@@ -1,6 +1,9 @@
+import logging
+
from aiogram import F, Router
from aiogram.filters import or_f, and_f
from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
@@ -22,14 +25,18 @@
MAIN_MENU_OPTIONS,
)
from admin.keyboards.keyboards import get_inline_keyboard
+from bot.exceptions import message_exception_handler
from crud.category_product import category_product_crud
from crud.product_crud import product_crud
+logger = logging.getLogger(__name__)
+
category_router = Router()
category_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
PREVIOUS_MENU = "Назад"
+
category_create_manager = CreateCategoryManager(PREVIOUS_MENU)
category_update_manager = UpdateCategoryManager(PREVIOUS_MENU)
category_delete_manager = DeleteCategoryManager(PREVIOUS_MENU)
@@ -41,14 +48,18 @@ async def get_categoties_by_product_id(state: FSMContext):
return fsm_data.get("product_id")
+@message_exception_handler(
+ log_error_text="Ошибка при возврате к меню категорий"
+)
@category_router.callback_query(
or_f(
+ State(None),
CategoryCreateState(),
CategoryUpdateState(),
CategoryDeleteState(),
SectionState.category,
),
- or_f(F.data == PREVIOUS_MENU),
+ F.data == PREVIOUS_MENU,
)
async def get_back_to_category_menu(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
@@ -71,53 +82,91 @@ async def get_back_to_category_menu(
),
)
await state.set_state(ProductCategory.product_id)
+ logger.info(
+ f"Пользователь {callback.from_user.id} вернулся в меню категорий."
+ )
-@category_router.callback_query(
- SectionState.category, F.data == ADMIN_BASE_OPTIONS.get("create")
+@message_exception_handler(
+ log_error_text="Ошибка при добавлении названия категории"
)
-async def select_new_category_type(callback: CallbackQuery, state: FSMContext):
- """Выбрать тип данных для дополнительной информации о продукте."""
- await category_create_manager.select_data_type(callback, state)
-
-
@category_router.callback_query(
- CategoryCreateState.select,
- or_f(
- F.data == ADMIN_CONTENT_OPTIONS.get("url"),
- F.data == ADMIN_CONTENT_OPTIONS.get("description"),
- F.data == ADMIN_CONTENT_OPTIONS.get("media"),
- ),
+ SectionState.category, F.data == ADMIN_BASE_OPTIONS.get("create")
)
async def add_product_category_name(
callback: CallbackQuery,
state: FSMContext,
):
- """Добавить название."""
+ """Добавить название категории."""
product_id = await get_categoties_by_product_id(state)
await category_create_manager.add_obj_name(product_id, callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} добавил название категории."
+ )
-@category_router.message(CategoryCreateState.url, F.text)
-async def add_product_category_url(message: Message, state: FSMContext):
- """Добавить ссылку."""
- await category_create_manager.add_obj_url(message, state)
+@message_exception_handler(
+ log_error_text="Ошибка при выборе типа данных для категории"
+)
+@category_router.message(CategoryCreateState.name, F.text)
+async def select_new_category_type(message: Message, state: FSMContext):
+ """Выбрать тип данных для дополнительной информации о продукте."""
+ await category_create_manager.select_data_type(message, state)
+ logger.info(
+ f"Пользователь {message.from_user.id} выбрал тип данных для категории."
+ )
-@category_router.message(CategoryCreateState.description, F.text)
+@message_exception_handler(
+ log_error_text="Ошибка при добавлении ссылки на категорию"
+)
+@category_router.callback_query(
+ CategoryCreateState.select, F.data == ADMIN_CONTENT_OPTIONS.get("url")
+)
+async def add_product_category_url(callback: CallbackQuery, state: FSMContext):
+ """Добавить ссылку на категорию."""
+ await category_create_manager.add_obj_url_callback(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} добавил ссылку на категорию."
+ )
+
+
+@message_exception_handler(
+ log_error_text="Ошибка при добавлении описания категории"
+)
+@category_router.callback_query(
+ CategoryCreateState.select,
+ F.data == ADMIN_CONTENT_OPTIONS.get("description"),
+)
async def add_product_category_description(
- message: Message, state: FSMContext
+ callback: CallbackQuery, state: FSMContext
):
- """Добавить текст."""
- await category_create_manager.add_obj_description(message, state)
+ """Добавить текст описания категории."""
+ await category_create_manager.add_obj_description_callback(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} добавил описание категории."
+ )
-@category_router.message(CategoryCreateState.media, F.text)
-async def add_product_category_media(message: Message, state: FSMContext):
- """Добавить картинку."""
- await category_create_manager.add_obj_media(message, state)
+@message_exception_handler(
+ log_error_text="Ошибка при добавлении медиа для категории"
+)
+@category_router.callback_query(
+ CategoryCreateState.select, F.data == ADMIN_CONTENT_OPTIONS.get("media")
+)
+async def add_product_category_media(
+ callback: CallbackQuery, state: FSMContext
+):
+ """Добавить картинку для категории."""
+ await category_create_manager.add_obj_media_callback(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} добавил медиа для категории."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при создании информации для продукта в БД"
+)
@category_router.message(
or_f(
CategoryCreateState.description,
@@ -129,12 +178,16 @@ async def add_product_category_media(message: Message, state: FSMContext):
async def create_product_with_data(
message: Message, state: FSMContext, session: AsyncSession
):
- """
- Создать информацию для продукта в БД и предложить добавить следующий.
- """
+ """Создать информацию для продукта в БД."""
await category_create_manager.add_obj_to_db(message, state, session)
+ logger.info(
+ f"Пользователь {message.from_user.id} создал информацию для продукта в БД."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при выборе категории для удаления"
+)
@category_router.callback_query(
SectionState.category, F.data == ADMIN_BASE_OPTIONS.get("delete")
)
@@ -146,39 +199,63 @@ async def product_to_delete(
await category_delete_manager.select_obj_to_delete(
product_id, callback, state, session
)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал категорию для удаления."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при подтверждении удаления категории"
+)
@category_router.callback_query(CategoryDeleteState.select, F.data)
async def confirm_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- """Подтверждение удаления."""
+ """Подтверждение удаления категории."""
await category_delete_manager.confirm_delete(callback, state, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} подтвердил удаление категории."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при удалении категории из БД"
+)
@category_router.callback_query(
CategoryDeleteState.confirm, F.data != PREVIOUS_MENU
)
async def delete_category(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- """Удалить информацию из БД."""
+ """Удалить информацию о категории из БД."""
await category_delete_manager.delete_obj(callback, state, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} удалил категорию из БД."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при выборе категории для обновления"
+)
@category_router.callback_query(
SectionState.category, F.data == ADMIN_BASE_OPTIONS.get("update")
)
async def category_to_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- """Выбор информации для редактирования."""
+ """Выбор информации для редактирования категории."""
product_id = await get_categoties_by_product_id(state)
await category_update_manager.select_obj_to_update(
product_id, callback, state, session
)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал категорию для обновления."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при выборе данных для обновления категории"
+)
@category_router.callback_query(
CategoryUpdateState.select,
and_f(
@@ -189,26 +266,44 @@ async def category_to_update(
async def select_category_data_to_update(
callback: CallbackQuery, session: AsyncSession
):
- """Выбор поля для редактирования."""
+ """Выбор поля для редактирования категории."""
await category_update_manager.select_data_to_update(callback, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал данные для обновления категории."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при обновлении имени категории"
+)
@category_router.callback_query(
CategoryUpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
)
async def category_name_update(callback: CallbackQuery, state: FSMContext):
- """Ввести новое название информации."""
+ """Ввести новое название категории."""
await category_update_manager.change_obj_name(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} обновил название категории."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при обновлении содержания категории"
+)
@category_router.callback_query(
CategoryUpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
)
async def about_url_update(callback: CallbackQuery, state: FSMContext):
- """Изменить содержание информации."""
+ """Изменить содержание категории."""
await category_update_manager.change_obj_content(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} обновил содержание категории."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при обновлении информации о категории в БД"
+)
@category_router.message(
or_f(
CategoryUpdateState.name,
@@ -224,5 +319,9 @@ async def about_url_update(callback: CallbackQuery, state: FSMContext):
async def update_about_info(
message: Message, state: FSMContext, session: AsyncSession
):
- """Внести изменения информации в БД."""
+ """Внести изменения информации о категории в БД."""
await category_update_manager.update_obj_in_db(message, state, session)
+ logger.info(
+ f"Пользователь {message.from_user.id} обновил информацию о категории в БД."
+ )
+
diff --git a/app/admin/handlers/admin_handlers/admin_info_handlers.py b/app/admin/handlers/admin_handlers/admin_info_handlers.py
index e4af3f5..bdd44c9 100644
--- a/app/admin/handlers/admin_handlers/admin_info_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_info_handlers.py
@@ -1,3 +1,5 @@
+import logging
+
from aiogram import F, Router
from aiogram.filters import or_f, and_f
from aiogram.fsm.context import FSMContext
@@ -19,6 +21,9 @@
ADMIN_QUESTION_OPTIONS,
SUPPORT_OPTIONS,
)
+from bot.exceptions import message_exception_handler
+
+logger = logging.getLogger(__name__)
PROBLEMS_MENU = SUPPORT_OPTIONS.get("problems_with_products")
GENERAL_QEUSTIONS_MENU = SUPPORT_OPTIONS.get("general_questions")
@@ -31,29 +36,46 @@
question_update_manager = QuestionUpdateManager()
question_delete_manager = QuestionDeleteManager()
-
+@message_exception_handler(
+ log_error_text='Ошибка при добавлении вопроса'
+)
@info_router.callback_query(
or_f(SectionState.general_questions, SectionState.problems_with_products),
F.data == ADMIN_BASE_OPTIONS.get("create"),
)
async def add_question(callback: CallbackQuery, state: FSMContext):
+ """Добавить вопрос."""
await question_create_manager.add_question_text(callback, state)
+ logger.info(f'Пользователь {callback.from_user.id} добавил новый вопрос.')
+@message_exception_handler(
+ log_error_text='Ошибка при добавлении текста вопроса'
+)
@info_router.message(CreateQuestionStates.question, F.text)
async def add_question_text(message: Message, state: FSMContext):
+ """Добавить текст вопроса."""
await question_create_manager.add_answer_text(message, state)
+ logger.info(f'Пользователь {message.from_user.id} добавил текст вопроса.')
+@message_exception_handler(
+ log_error_text='Ошибка при добавлении ответа на вопрос'
+)
@info_router.message(CreateQuestionStates.answer, F.text)
async def add_question_answer(
message: Message,
state: FSMContext,
session: AsyncSession,
):
+ """Добавить ответ на вопрос в БД."""
await question_create_manager.add_question_to_db(message, state, session)
+ logger.info(f'Пользователь {message.from_user.id} добавил ответ на вопрос в БД.')
+@message_exception_handler(
+ log_error_text='Ошибка при выборе вопроса для удаления'
+)
@info_router.callback_query(
or_f(SectionState.general_questions, SectionState.problems_with_products),
F.data == ADMIN_BASE_OPTIONS.get("delete"),
@@ -61,14 +83,19 @@ async def add_question_answer(
async def question_to_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
+ """Запустить процесс выбора вопроса для удаления."""
await question_delete_manager.select_question(
callback,
state,
next_state=DeleteQuestionStates.select,
session=session,
)
+ logger.info(f'Пользователь {callback.from_user.id} выбрал вопрос для удаления.')
+@message_exception_handler(
+ log_error_text='Ошибка при подтверждении удаления вопроса'
+)
@info_router.callback_query(
DeleteQuestionStates.select,
and_f(F.data != PROBLEMS_MENU, F.data != GENERAL_QEUSTIONS_MENU),
@@ -76,9 +103,14 @@ async def question_to_delete(
async def confirm_delete_question(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
+ """Подтвердить удаление вопроса."""
await question_delete_manager.confirm_delete(callback, state, session)
+ logger.info(f'Пользователь {callback.from_user.id} подтвердил удаление вопроса.')
+@message_exception_handler(
+ log_error_text='Ошибка при удалении вопроса из БД'
+)
@info_router.callback_query(
DeleteQuestionStates.confirm,
and_f(F.data != PROBLEMS_MENU, F.data != GENERAL_QEUSTIONS_MENU),
@@ -86,9 +118,14 @@ async def confirm_delete_question(
async def delete_question(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
+ """Удалить вопрос из базы данных."""
await question_delete_manager.delete_question(callback, state, session)
+ logger.info(f'Пользователь {callback.from_user.id} удалил вопрос из БД.')
+@message_exception_handler(
+ log_error_text='Ошибка при выборе вопроса для обновления'
+)
@info_router.callback_query(
or_f(SectionState.general_questions, SectionState.problems_with_products),
F.data == ADMIN_BASE_OPTIONS.get("update"),
@@ -96,14 +133,19 @@ async def delete_question(
async def update_question(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
+ """Запустить процесс выбора вопроса для обновления."""
await question_update_manager.select_question(
callback,
state,
next_state=UpdateQuestionStates.select,
session=session,
)
+ logger.info(f'Пользователь {callback.from_user.id} выбрал вопрос для обновления.')
+@message_exception_handler(
+ log_error_text='Ошибка при выборе данных для обновления вопроса'
+)
@info_router.callback_query(
UpdateQuestionStates.select,
and_f(
@@ -116,30 +158,47 @@ async def update_question(
async def update_question_choice(
callback: CallbackQuery, session: AsyncSession
):
+ """Обработать выбор данных для обновления вопроса."""
await question_update_manager.update_data_type(callback, session)
+ logger.info(f'Пользователь {callback.from_user.id} выбрал данные для обновления вопроса.')
+@message_exception_handler(
+ log_error_text='Ошибка при обновлении текста вопроса'
+)
@info_router.callback_query(
UpdateQuestionStates.select,
F.data == ADMIN_QUESTION_OPTIONS.get("question"),
)
async def update_question_text(callback: CallbackQuery, state: FSMContext):
+ """Обновить текст вопроса."""
await question_update_manager.update_question(callback, state)
+ logger.info(f'Пользователь {callback.from_user.id} обновил текст вопроса.')
+@message_exception_handler(
+ log_error_text='Ошибка при обновлении ответа на вопрос'
+)
@info_router.callback_query(
UpdateQuestionStates.select, F.data == ADMIN_QUESTION_OPTIONS.get("answer")
)
async def update_question_answer(callback: CallbackQuery, state: FSMContext):
+ """Обновить ответ на вопрос."""
await question_update_manager.update_answer(callback, state)
+ logger.info(f'Пользователь {callback.from_user.id} обновил ответ на вопрос.')
+@message_exception_handler(
+ log_error_text='Ошибка при обновлении данных вопроса в БД'
+)
@info_router.message(
or_f(UpdateQuestionStates.question, UpdateQuestionStates.answer), F.text
)
async def update_question_data(
message: Message, state: FSMContext, session: AsyncSession
):
+ """Обновить вопрос в базе данных на основе нового содержимого."""
await question_update_manager.update_question_in_db(
message, state, session
)
+ logger.info(f'Пользователь {message.from_user.id} обновил данные вопроса в БД.')
diff --git a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
index 5742064..f2c1c80 100644
--- a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
@@ -1,3 +1,5 @@
+import logging
+
from aiogram import F, Router
from aiogram.filters import and_f, or_f
from aiogram.fsm.context import FSMContext
@@ -5,6 +7,7 @@
from sqlalchemy.ext.asyncio import AsyncSession
from admin.handlers.admin_handlers.admin import SectionState
+from bot.exceptions import message_exception_handler
from crud.portfolio_projects_crud import portfolio_crud
from admin.filters.filters import ChatTypeFilter, IsAdmin
from admin.admin_managers import (
@@ -24,6 +27,8 @@
ADMIN_BASE_OPTIONS,
)
+logger = logging.getLogger(__name__)
+
portfolio_router = Router()
portfolio_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
@@ -37,6 +42,9 @@
)
+@message_exception_handler(
+ log_error_text="Ошибка при добавлении имени проекта портфолио"
+)
@portfolio_router.callback_query(
SectionState.other_projects, F.data == ADMIN_BASE_OPTIONS.get("create")
)
@@ -45,22 +53,40 @@ async def add_portfolio_project_name(
):
"""Запустить процесс создания нового портфолио."""
await portfolio_create_manager.add_obj_name(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} запустил процесс создания нового портфолио."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при сохранении имени нового объекта портфолио"
+)
@portfolio_router.message(CreateState.name, F.text)
async def add_portfolio_project_url(message: Message, state: FSMContext):
"""Сохранить имя нового объекта в состояние."""
await portfolio_create_manager.add_obj_url(message, state)
+ logger.info(
+ f"Пользователь {message.from_user.id} сохранил имя нового объекта портфолио."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при сохранении URL нового объекта в БД"
+)
@portfolio_router.message(CreateState.url, F.text)
async def create_portfolio_project(
message: Message, state: FSMContext, session: AsyncSession
):
"""Сохранить URL нового объекта в базу данных."""
await portfolio_create_manager.add_obj_to_db(message, state, session)
+ logger.info(
+ f"Пользователь {message.from_user.id} создал новый проект портфолио в БД."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при выборе портфолио для удаления"
+)
@portfolio_router.callback_query(
SectionState.other_projects, F.data == ADMIN_BASE_OPTIONS.get("delete")
)
@@ -71,24 +97,38 @@ async def portfolio_project_to_delete(
await portfolio_delete_manager.select_obj_to_delete(
callback, state, session
)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал проект для удаления."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при подтверждении удаления проекта"
+)
@portfolio_router.callback_query(DeleteState.select, F.data != PREVIOUS_MENU)
async def confirm_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- """Подтведить удаление выбранного объекта."""
+ """Подтвердить удаление выбранного объекта."""
await portfolio_delete_manager.confirm_delete(callback, state, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} подтвердил удаление проекта."
+ )
+@message_exception_handler(log_error_text="Ошибка при удалении проекта из БД")
@portfolio_router.callback_query(DeleteState.confirm, F.data != PREVIOUS_MENU)
-async def delete_protfolio_project(
+async def delete_portfolio_project(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Удалить выбранный объект из базы данных."""
await portfolio_delete_manager.delete_obj(callback, state, session)
+ logger.info(f"Пользователь {callback.from_user.id} удалил проект из БД.")
+@message_exception_handler(
+ log_error_text="Ошибка при выборе проекта для обновления"
+)
@portfolio_router.callback_query(
SectionState.other_projects, F.data == ADMIN_BASE_OPTIONS.get("update")
)
@@ -99,8 +139,14 @@ async def portfolio_project_to_update(
await portfolio_update_manager.select_obj_to_update(
callback, state, session
)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал проект для обновления."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при выборе данных для обновления"
+)
@portfolio_router.callback_query(
UpdateState.select,
and_f(
@@ -109,13 +155,19 @@ async def portfolio_project_to_update(
F.data != PREVIOUS_MENU,
),
)
-async def update_portfolio_project_choise(
+async def update_portfolio_project_choice(
callback: CallbackQuery, session: AsyncSession
):
"""Обработать выбор данных для обновления."""
await portfolio_update_manager.select_data_to_update(callback, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал данные для обновления."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при обновлении имени объекта"
+)
@portfolio_router.callback_query(
UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
)
@@ -125,24 +177,38 @@ async def portfolio_name_update(
):
"""Обновить имя объекта."""
await portfolio_update_manager.change_obj_name(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} обновил имя объекта портфолио."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при обновлении содержимого объекта"
+)
@portfolio_router.callback_query(
UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
)
async def about_url_update(callback: CallbackQuery, state: FSMContext):
"""Обновить содержимое объекта."""
await portfolio_update_manager.change_obj_content(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} обновил содержимое объекта портфолио."
+ )
+@message_exception_handler(log_error_text="Ошибка при обновлении объекта в БД")
@portfolio_router.message(or_f(UpdateState.name, UpdateState.url), F.text)
async def update_about_info(
message: Message, state: FSMContext, session: AsyncSession
):
"""Обновить объект в базе данных на основе нового содержимого."""
await portfolio_update_manager.update_obj_in_db(message, state, session)
+ logger.info(f"Пользователь {message.from_user.id} обновил объект в БД.")
+@message_exception_handler(
+ log_error_text="Ошибка при обновлении адреса ссылки основного портфолио"
+)
@portfolio_router.callback_query(
SectionState.portfolio,
F.data == ADMIN_PORTFOLIO_OPTIONS.get("change_url"),
@@ -154,8 +220,14 @@ async def change_portfolio_url(
await main_portfolio_url_update_manager.update_main_portfolio_url(
callback, state, session
)
+ logger.info(
+ f"Пользователь {callback.from_user.id} обновил адрес ссылки основного портфолио."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при обновлении адреса ссылки основного портфолио в БД"
+)
@portfolio_router.message(UpdateState.portolio, F.text)
async def update_portfolio_button(
message: Message, state: FSMContext, session: AsyncSession
@@ -164,3 +236,6 @@ async def update_portfolio_button(
await main_portfolio_url_update_manager.update_obj_in_db(
message, state, session
)
+ logger.info(
+ f"Пользователь {message.from_user.id} обновил адрес ссылки основного портфолио в БД."
+ )
diff --git a/app/admin/handlers/admin_handlers/admin_product_handlers.py b/app/admin/handlers/admin_handlers/admin_product_handlers.py
index 8d8207a..c41f7fc 100644
--- a/app/admin/handlers/admin_handlers/admin_product_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_product_handlers.py
@@ -1,16 +1,18 @@
+import logging
+
from aiogram import F, Router
from aiogram.filters import or_f, and_f
from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
+from bot.exceptions import message_exception_handler
+
from .admin import SectionState
from crud.product_crud import product_crud
from admin.filters.filters import ChatTypeFilter, IsAdmin
from admin.admin_managers import (
- DeleteState,
- UpdateState,
- CreateState,
CreateManager,
UpdateManager,
DeleteManager,
@@ -22,67 +24,125 @@
MAIN_MENU_OPTIONS,
)
+logger = logging.getLogger(__name__)
+
+
+class ProductCreateState(StatesGroup):
+ select = State()
+ name = State()
+ url = State()
+ description = State()
+ media = State()
+
+
+class ProductUpdateState(StatesGroup):
+ select = State()
+ name = State()
+ url = State()
+ description = State()
+ media = State()
+
+
+class ProductDeleteState(StatesGroup):
+ select = State()
+ confirm = State()
+
+
product_router = Router()
product_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("products")
-product_create_manager = CreateManager(product_crud, PREVIOUS_MENU)
-product_update_manager = UpdateManager(product_crud, PREVIOUS_MENU)
-product_delete_manager = DeleteManager(product_crud, PREVIOUS_MENU)
+product_create_manager = CreateManager(
+ product_crud, PREVIOUS_MENU, ProductCreateState()
+)
+product_update_manager = UpdateManager(
+ product_crud, PREVIOUS_MENU, ProductUpdateState()
+)
+product_delete_manager = DeleteManager(
+ product_crud, PREVIOUS_MENU, ProductDeleteState()
+)
+@message_exception_handler(log_error_text="Ошибка при добавлении продукта")
@product_router.callback_query(
SectionState.product, F.data == ADMIN_BASE_OPTIONS.get("create")
)
async def add_product(callback: CallbackQuery, state: FSMContext):
"""Добавить название."""
await product_create_manager.add_obj_name(callback, state)
+ logger.info(f"Пользователь {callback.from_user.id} добавил продукт.")
-@product_router.message(CreateState.name, F.text)
+@message_exception_handler(
+ log_error_text="Ошибка при добавлении описания продукта"
+)
+@product_router.message(ProductCreateState.name, F.text)
async def add_product_description(message: Message, state: FSMContext):
"""Добавить описание."""
await product_create_manager.add_obj_description(message, state)
+ logger.info(
+ f"Пользователь {message.from_user.id} добавил описание продукта."
+ )
-@product_router.message(CreateState.description, F.text)
-async def creeate_product(
+@message_exception_handler(log_error_text="Ошибка при создании продукта в БД")
+@product_router.message(ProductCreateState.description, F.text)
+async def create_product(
message: Message, state: FSMContext, session: AsyncSession
):
- """Создать продкет в БД."""
-
+ """Создать продукт в БД."""
await product_create_manager.add_obj_to_db(message, state, session)
+ logger.info(f"Пользователь {message.from_user.id} создал продукт в БД.")
+@message_exception_handler(
+ log_error_text="Ошибка при выборе продукта для удаления"
+)
@product_router.callback_query(
SectionState.product, F.data == ADMIN_BASE_OPTIONS.get("delete")
)
async def product_to_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- """Выбор продукта для удаление."""
+ """Выбор продукта для удаления."""
await product_delete_manager.select_obj_to_delete(callback, state, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал продукт для удаления."
+ )
-@product_router.callback_query(DeleteState.select, F.data != PREVIOUS_MENU)
+@message_exception_handler(
+ log_error_text="Ошибка при подтверждении удаления продукта"
+)
+@product_router.callback_query(
+ ProductDeleteState.select, F.data != PREVIOUS_MENU
+)
async def confirm_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Подтверждение удаления."""
-
await product_delete_manager.confirm_delete(callback, state, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} подтвердил удаление продукта."
+ )
-@product_router.callback_query(DeleteState.confirm, F.data != PREVIOUS_MENU)
+@message_exception_handler(log_error_text="Ошибка при удалении продукта из БД")
+@product_router.callback_query(
+ ProductDeleteState.confirm, F.data != PREVIOUS_MENU
+)
async def delete_product(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Удалить продукт из БД."""
-
await product_delete_manager.delete_obj(callback, state, session)
+ logger.info(f"Пользователь {callback.from_user.id} удалил продукт из БД.")
+@message_exception_handler(
+ log_error_text="Ошибка при выборе продукта для редактирования"
+)
@product_router.callback_query(
SectionState.product, F.data == ADMIN_BASE_OPTIONS.get("update")
)
@@ -90,12 +150,17 @@ async def product_to_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Выбор продукта для редактирования."""
-
await product_update_manager.select_obj_to_update(callback, state, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал продукт для редактирования."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при выборе поля для редактирования"
+)
@product_router.callback_query(
- UpdateState.select,
+ ProductUpdateState.select,
and_f(
F.data != ADMIN_UPDATE_OPTIONS.get("name"),
F.data != ADMIN_UPDATE_OPTIONS.get("content"),
@@ -104,34 +169,52 @@ async def product_to_update(
)
async def update_choice(callback: CallbackQuery, session: AsyncSession):
"""Выбор поля для редактирования."""
-
await product_update_manager.select_data_to_update(callback, session)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал поле для редактирования."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при вводе нового названия продукта"
+)
@product_router.callback_query(
- UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
+ ProductUpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
)
async def about_name_update(callback: CallbackQuery, state: FSMContext):
"""Ввести новое название продукта."""
-
await product_update_manager.change_obj_name(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} ввел новое название продукта."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при вводе нового описания продукта"
+)
@product_router.callback_query(
- UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
+ ProductUpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
)
async def about_url_update(callback: CallbackQuery, state: FSMContext):
"""Ввести новое описание продукта."""
-
await product_update_manager.change_obj_content(callback, state)
+ logger.info(
+ f"Пользователь {callback.from_user.id} ввел новое описание продукта."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при внесении изменений продукта в БД"
+)
@product_router.message(
- or_f(UpdateState.name, UpdateState.description), F.text
+ or_f(ProductUpdateState.name, ProductUpdateState.description), F.text
)
async def update_about_info(
message: Message, state: FSMContext, session: AsyncSession
):
"""Внести изменения продукта в БД."""
-
await product_update_manager.update_obj_in_db(message, state, session)
+ logger.info(
+ f"Пользователь {message.from_user.id} внес изменения продукта в БД."
+ )
+
diff --git a/app/admin/handlers/user.py b/app/admin/handlers/user.py
index 1170de1..438e7fc 100644
--- a/app/admin/handlers/user.py
+++ b/app/admin/handlers/user.py
@@ -223,7 +223,7 @@ async def get_products_list(
"""Получить список продуктов."""
products = [
- product.title for product in await product_crud.get_multi(session)
+ product.name for product in await product_crud.get_multi(session)
]
await state.clear()
diff --git a/app/alembic/versions/4c3a4964ff61_add_description_fields_to_product_and_.py b/app/alembic/versions/4c3a4964ff61_add_description_fields_to_product_and_.py
deleted file mode 100644
index b69acd2..0000000
--- a/app/alembic/versions/4c3a4964ff61_add_description_fields_to_product_and_.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""add description fields to product and category, rename RoleEnum
-
-Revision ID: 4c3a4964ff61
-Revises: 42e8d0d2bdd4
-Create Date: 2024-10-13 16:09:40.786385
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision: str = '4c3a4964ff61'
-down_revision: Union[str, None] = '42e8d0d2bdd4'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.add_column('categorytype', sa.Column('description', sa.TEXT(), nullable=False))
- op.add_column('productcategory', sa.Column('description', sa.TEXT(), nullable=False))
- op.drop_column('productcategory', 'response')
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.add_column('productcategory', sa.Column('response', sa.TEXT(), nullable=False))
- op.drop_column('productcategory', 'description')
- op.drop_column('categorytype', 'description')
- # ### end Alembic commands ###
diff --git a/app/alembic/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py b/app/alembic/versions/ce91f683ad23_add_nullable_true_for_category_type_.py
similarity index 93%
rename from app/alembic/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py
rename to app/alembic/versions/ce91f683ad23_add_nullable_true_for_category_type_.py
index 2918ed3..08e07c1 100644
--- a/app/alembic/versions/42e8d0d2bdd4_change_all_model_name_fields_to_name.py
+++ b/app/alembic/versions/ce91f683ad23_add_nullable_true_for_category_type_.py
@@ -1,8 +1,8 @@
-"""change all model name fields to name
+"""add nullable True for category type content
-Revision ID: 42e8d0d2bdd4
+Revision ID: ce91f683ad23
Revises:
-Create Date: 2024-10-10 10:21:14.189895
+Create Date: 2024-10-13 23:29:19.198485
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
-revision: str = '42e8d0d2bdd4'
+revision: str = 'ce91f683ad23'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -52,7 +52,7 @@ def upgrade() -> None:
)
op.create_table('productcategory',
sa.Column('name', sa.VARCHAR(length=150), nullable=False),
- sa.Column('response', sa.TEXT(), nullable=False),
+ sa.Column('description', sa.TEXT(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
@@ -67,8 +67,9 @@ def upgrade() -> None:
op.create_table('categorytype',
sa.Column('name', sa.VARCHAR(length=150), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
- sa.Column('url', sa.VARCHAR(length=128), nullable=False),
+ sa.Column('url', sa.VARCHAR(length=128), nullable=True),
sa.Column('media', sa.VARCHAR(length=128), nullable=True),
+ sa.Column('description', sa.TEXT(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
diff --git a/app/core/init_db.py b/app/core/init_db.py
index 2b0c267..8b6a85e 100644
--- a/app/core/init_db.py
+++ b/app/core/init_db.py
@@ -1,4 +1,4 @@
-from app.crud.users import create_user_id
+from crud.users import create_user_id
from crud.user_crud import user_crud
from core.db import AsyncSessionLocal
from crud.portfolio_projects_crud import portfolio_crud
@@ -18,6 +18,6 @@ async def add_portfolio():
async def set_admin():
async with AsyncSessionLocal() as session:
for admin in admin_list:
- if not user_crud.get_user_by_tg_id(admin, session):
+ if not await user_crud.get_user_by_tg_id(admin, session):
user = await create_user_id(admin, session)
await user_crud.update(user, {"role": "ADMIN"}, session)
diff --git a/app/models/models.py b/app/models/models.py
index 597b5a0..97160a9 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -69,11 +69,11 @@ class CategoryType(Base):
index=True,
)
- url: Mapped[str] = mapped_column(pgsql_types.VARCHAR(128))
+ url: Mapped[str] = mapped_column(pgsql_types.VARCHAR(128), nullable=True)
media: Mapped[str] = mapped_column(pgsql_types.VARCHAR(128), nullable=True)
- description: Mapped[str] = mapped_column(pgsql_types.TEXT)
+ description: Mapped[str] = mapped_column(pgsql_types.TEXT, nullable=True)
product_category = relationship(
"ProductCategory", back_populates="categories"
From 5166ca384ec0018524c0db2266c1bef8d553a16b Mon Sep 17 00:00:00 2001
From: ikhit
Date: Mon, 14 Oct 2024 09:47:54 +0300
Subject: [PATCH 41/75] little fixes
---
app/admin/admin_managers/category_manager.py | 1 +
app/admin/admin_managers/create_manager.py | 5 +++--
app/admin/admin_managers/question_manager.py | 1 +
app/admin/admin_managers/update_manager.py | 4 ++--
app/core/db.py | 2 +-
5 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/app/admin/admin_managers/category_manager.py b/app/admin/admin_managers/category_manager.py
index 5b5aca0..a4c0cd9 100644
--- a/app/admin/admin_managers/category_manager.py
+++ b/app/admin/admin_managers/category_manager.py
@@ -55,6 +55,7 @@ async def add_obj_name(
Добавить название объекта и перейти в
следующее машинное состояние.
"""
+ await callback.message.delete()
await state.update_data(product_id=product_id)
await callback.message.answer(
"Введите название:",
diff --git a/app/admin/admin_managers/create_manager.py b/app/admin/admin_managers/create_manager.py
index dcc11c7..ab64b87 100644
--- a/app/admin/admin_managers/create_manager.py
+++ b/app/admin/admin_managers/create_manager.py
@@ -87,6 +87,7 @@ async def add_obj_name(
Добавить название объекта и перейти в
следующее машинное состояние.
"""
+ await callback.message.delete()
await callback.message.answer(
"Введите название:",
reply_markup=await get_inline_keyboard(
@@ -154,7 +155,7 @@ async def add_obj_description(self, message: Message, state: FSMContext):
Добавить текст к объекту и перейти в
следующее машинное состояние.
"""
- message_text = "Введите текст:"
+ message_text = "Введите описание:"
await self.prompt_for_input(
message,
message_text,
@@ -170,7 +171,7 @@ async def add_obj_description_callback(
следующее машинное состояние.
"""
await callback.message.answer(
- "Введите текст:",
+ "Введите описание:",
reply_markup=await get_inline_keyboard(
previous_menu=self.back_option
),
diff --git a/app/admin/admin_managers/question_manager.py b/app/admin/admin_managers/question_manager.py
index 630d5d6..ef1dd31 100644
--- a/app/admin/admin_managers/question_manager.py
+++ b/app/admin/admin_managers/question_manager.py
@@ -72,6 +72,7 @@ async def add_question_text(
Добавить текст вопроса и перейти в
следующее машинное состояние.
"""
+ await callback.message.delete()
await state.update_data(
question_type=await self.set_question_type(state)
)
diff --git a/app/admin/admin_managers/update_manager.py b/app/admin/admin_managers/update_manager.py
index 3f7ab73..b4ab4c5 100644
--- a/app/admin/admin_managers/update_manager.py
+++ b/app/admin/admin_managers/update_manager.py
@@ -135,8 +135,8 @@ async def change_obj_content(
"description" in obj_fields and self.obj_to_update.description
):
message_text = (
- f"Текущий текст: \n\n {self.obj_to_update.description} \n\n"
- "Введите новый:"
+ f"Текущее описание: \n\n {self.obj_to_update.description} \n\n"
+ "Введите новое:"
)
await state.set_state(self.states_group.description)
await callback.message.edit_text(
diff --git a/app/core/db.py b/app/core/db.py
index 7ae85cd..c0f8802 100644
--- a/app/core/db.py
+++ b/app/core/db.py
@@ -17,5 +17,5 @@ def __tablename__(cls):
Base = declarative_base(cls=PreBase)
-engine = create_async_engine(settings.database_url, echo=True)
+engine = create_async_engine(settings.database_url) # , echo=True
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession)
From 775bb781dacbae8f623a4714356222a1cfa7686a Mon Sep 17 00:00:00 2001
From: ikhit
Date: Mon, 14 Oct 2024 10:40:04 +0300
Subject: [PATCH 42/75] fixcontact manager validator
---
...6b_fix_shipping_close_add_nullable_true.py} | 10 +++++-----
app/bot/validators.py | 2 +-
app/models/models.py | 4 +---
test.py | 18 ------------------
4 files changed, 7 insertions(+), 27 deletions(-)
rename app/alembic/versions/{ce91f683ad23_add_nullable_true_for_category_type_.py => b6f7d68b3c6b_fix_shipping_close_add_nullable_true.py} (95%)
delete mode 100644 test.py
diff --git a/app/alembic/versions/ce91f683ad23_add_nullable_true_for_category_type_.py b/app/alembic/versions/b6f7d68b3c6b_fix_shipping_close_add_nullable_true.py
similarity index 95%
rename from app/alembic/versions/ce91f683ad23_add_nullable_true_for_category_type_.py
rename to app/alembic/versions/b6f7d68b3c6b_fix_shipping_close_add_nullable_true.py
index 08e07c1..c1cbc6b 100644
--- a/app/alembic/versions/ce91f683ad23_add_nullable_true_for_category_type_.py
+++ b/app/alembic/versions/b6f7d68b3c6b_fix_shipping_close_add_nullable_true.py
@@ -1,8 +1,8 @@
-"""add nullable True for category type content
+"""fix shipping close add nullable true
-Revision ID: ce91f683ad23
+Revision ID: b6f7d68b3c6b
Revises:
-Create Date: 2024-10-13 23:29:19.198485
+Create Date: 2024-10-14 10:31:13.807609
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
-revision: str = 'ce91f683ad23'
+revision: str = 'b6f7d68b3c6b'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -32,7 +32,7 @@ def upgrade() -> None:
sa.Column('need_support', sa.BOOLEAN(), nullable=False),
sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
- sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
+ sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
diff --git a/app/bot/validators.py b/app/bot/validators.py
index 38908fa..a299596 100644
--- a/app/bot/validators.py
+++ b/app/bot/validators.py
@@ -4,7 +4,7 @@
def is_valid_name(name: str) -> bool:
"""Проверяет, что имя содержит только буквы."""
- return bool(match(r"^[A-Za-zА-Яа-яЁё]+$", name))
+ return bool(match(r"^[A-Za-zА-Яа-яЁё ]+$", name))
def is_valid_phone_number(phone_number: str) -> bool:
diff --git a/app/models/models.py b/app/models/models.py
index 97160a9..3e3729d 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -137,9 +137,7 @@ class ContactManager(Base):
)
shipping_date_close: Mapped[datetime] = mapped_column(
- pgsql_types.TIMESTAMP(timezone=True),
- server_default=func.now(),
- nullable=False,
+ pgsql_types.TIMESTAMP(timezone=True), nullable=True
)
feedbacks = relationship(
diff --git a/test.py b/test.py
deleted file mode 100644
index 9a2d1ac..0000000
--- a/test.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import asyncio
-
-from app.crud.info_crud import info_crud
-from app.core.db import AsyncSessionLocal
-
-
-async def set_admin():
- """
- После первого запуска бота и чистой БД добавить себя в список админов.
- Не забыть удалить потом этот файл.
- """
- async with AsyncSessionLocal() as session:
- info = await info_crud.get(1, session)
- await info_crud.remove(info, session)
-
-
-if __name__ == "__main__":
- asyncio.run(set_admin())
From 5bce6555fbce665dc6fc0be87c26497dab8fae23 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Mon, 14 Oct 2024 15:06:47 +0300
Subject: [PATCH 43/75] add callback request links
---
app/admin/admin_settings.py | 11 +-
app/admin/handlers/admin_handlers/__init__.py | 2 +
.../handlers/admin_handlers/admin_special.py | 146 ++++++++++++++++++
...887d8_fix_delete_some_field_in_feedback.py | 30 ++++
...3699d_fix_delete_some_field_in_feedback.py | 30 ++++
...f35e_fix_delete_some_field_in_feedback.py} | 11 +-
app/crud/feedback_crud.py | 114 +++++++-------
app/crud/request_to_manager.py | 51 +++++-
app/models/models.py | 18 +--
9 files changed, 330 insertions(+), 83 deletions(-)
create mode 100644 app/admin/handlers/admin_handlers/admin_special.py
create mode 100644 app/alembic/versions/07c781d887d8_fix_delete_some_field_in_feedback.py
create mode 100644 app/alembic/versions/c493d913699d_fix_delete_some_field_in_feedback.py
rename app/alembic/versions/{b6f7d68b3c6b_fix_shipping_close_add_nullable_true.py => c4b40ca2f35e_fix_delete_some_field_in_feedback.py} (92%)
diff --git a/app/admin/admin_settings.py b/app/admin/admin_settings.py
index 54c715f..fd2d04e 100644
--- a/app/admin/admin_settings.py
+++ b/app/admin/admin_settings.py
@@ -20,6 +20,7 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
"Воспользуйтесь экранной клавиатурой для выбора опции."
)
+
# Кнопки админа
ADMIN_BASE_OPTIONS = {
"create": "Добавить",
@@ -48,9 +49,14 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
"media": "Картинка",
}
ADMIN_CONTENT_BUTTONS = get_buttons(ADMIN_CONTENT_OPTIONS)
-
ADMIN_QUESTION_OPTIONS = {"question": "Вопрос", "answer": "Ответ"}
ADMIN_QUESTION_BUTTONS = get_buttons(ADMIN_QUESTION_OPTIONS)
+ADMIN_SPECIAL_OPTIONS = {
+ "manager_request": "Запросы на обратный звонок",
+ "support_request": "Запросы на техподдержку",
+ "Feedbacks": "Отзывы",
+}
+ADMIN_SPECIAL_BUTTONS = get_buttons(ADMIN_SPECIAL_OPTIONS)
# Кнопки экранной клавиатуры
BASE_BUTTONS = {
"main_menu": "Главное меню",
@@ -66,7 +72,7 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
"products": "Продукты и услуги",
"support": "Техническая поддержка",
"portfolio": "Портфолио",
- "request_callback": "Связаться с менеджером",
+ "admin_special": "Дополнительно",
}
MAIN_MENU_BUTTONS = get_buttons(MAIN_MENU_OPTIONS)
@@ -106,3 +112,4 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
PHONE_NUMBER_REGEX = r"(\+\d{5,25}$|\d{5,25}$)"
USER_CALLBACK_PAGINATION = 5
FEEDBACK_PAGINATION = 5
+DATETIME_FORMAT = '%d-%m-%Y %H:%M'
diff --git a/app/admin/handlers/admin_handlers/__init__.py b/app/admin/handlers/admin_handlers/__init__.py
index 890363a..00f2b2c 100644
--- a/app/admin/handlers/admin_handlers/__init__.py
+++ b/app/admin/handlers/admin_handlers/__init__.py
@@ -6,6 +6,7 @@
from .admin_portfolio_handlers import portfolio_router
from .admin_product_handlers import product_router
from .admin_category_handlers import category_router
+from .admin_special import admin_special_router
admin_router = Router()
admin_router.include_router(admin_main_router)
@@ -14,3 +15,4 @@
admin_router.include_router(portfolio_router)
admin_router.include_router(product_router)
admin_router.include_router(category_router)
+admin_router.include_router(admin_special_router)
diff --git a/app/admin/handlers/admin_handlers/admin_special.py b/app/admin/handlers/admin_handlers/admin_special.py
new file mode 100644
index 0000000..ea5ec1b
--- /dev/null
+++ b/app/admin/handlers/admin_handlers/admin_special.py
@@ -0,0 +1,146 @@
+from aiogram import Router, F
+from aiogram.types import CallbackQuery
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.keyboards.keyboards import (
+ get_inline_keyboard,
+)
+from admin.admin_settings import (
+ ADMIN_SPECIAL_BUTTONS,
+ ADMIN_SPECIAL_OPTIONS,
+ DATETIME_FORMAT,
+ MAIN_MENU_OPTIONS,
+ MAIN_MENU_TEXT,
+)
+from models.models import ContactManager
+from crud.request_to_manager import (
+ close_case,
+ get_all_manager_requests,
+ get_all_support_requests,
+ get_request,
+)
+
+
+class RequestState(StatesGroup):
+ manager_request = State()
+ support_request = State()
+
+
+admin_special_router = Router()
+admin_special_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+
+
+async def get_state_name(state: str) -> str:
+ """Получить название состояния и вернуть его название."""
+ state_name = state.split(":")[1]
+ return ADMIN_SPECIAL_OPTIONS.get(state_name)
+
+
+async def get_requests_list(
+ request_list: list[ContactManager],
+) -> tuple[list[str]]:
+ options = [
+ (
+ f"Заявка номер {request.id} от "
+ f"{request.shipping_date.strftime(DATETIME_FORMAT)}"
+ )
+ for request in request_list
+ ]
+ request_ids = [request.id for request in request_list]
+ return options, request_ids
+
+
+@admin_special_router.callback_query(
+ F.data == MAIN_MENU_OPTIONS.get("admin_special")
+)
+async def get_admin_special_options(callback: CallbackQuery):
+ await callback.message.edit_text(
+ "Дополнительная информация для администрации:",
+ reply_markup=await get_inline_keyboard(
+ ADMIN_SPECIAL_BUTTONS, previous_menu=MAIN_MENU_TEXT
+ ),
+ )
+
+
+@admin_special_router.callback_query(
+ F.data == ADMIN_SPECIAL_OPTIONS.get("manager_request")
+)
+async def get_manager_request_list(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Получить список заявок на обратный звонок от менеджера."""
+ request_list = await get_all_manager_requests(session)
+ options, callbacks = await get_requests_list(request_list)
+ await callback.message.edit_text(
+ ADMIN_SPECIAL_OPTIONS.get("manager_request"),
+ reply_markup=await get_inline_keyboard(
+ options=options,
+ callback=callbacks,
+ previous_menu=MAIN_MENU_OPTIONS.get("admin_special"),
+ ),
+ )
+ await state.set_state(RequestState.manager_request)
+
+
+@admin_special_router.callback_query(
+ F.data == ADMIN_SPECIAL_OPTIONS.get("support_request")
+)
+async def get_support_request_list(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Получить список заявок на техподдержку."""
+ request_list = await get_all_support_requests(session)
+ options, callbacks = await get_requests_list(request_list)
+ await callback.message.edit_text(
+ ADMIN_SPECIAL_OPTIONS.get("support_request"),
+ reply_markup=await get_inline_keyboard(
+ options=options,
+ callback=callbacks,
+ previous_menu=MAIN_MENU_OPTIONS.get("admin_special"),
+ ),
+ )
+ await state.set_state(RequestState.support_request)
+
+
+@admin_special_router.callback_query(RequestState(), F.data.isnumeric())
+async def get_request_data(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Получить данные о заявке."""
+ current_state = await state.get_state()
+ back_option = await get_state_name(current_state)
+ request = await get_request(callback.data, session)
+ message = (
+ f"Заявка от пользователя {request.first_name}\n\n"
+ f"Номер для связи: {request.phone_number}\n\n"
+ f"Дата заявки: {request.shipping_date.strftime(DATETIME_FORMAT)}\n\n"
+ )
+ await callback.message.edit_text(
+ message,
+ reply_markup=await get_inline_keyboard(
+ ["Закрыть"], previous_menu=back_option
+ ),
+ )
+ await state.update_data(request_id=callback.data)
+
+
+@admin_special_router.callback_query(RequestState(), F.data == "Закрыть")
+async def close_request(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Закрыть заявку."""
+ try:
+ current_state = await state.get_state()
+ back_option = await get_state_name(current_state)
+ fsm_data = await state.get_data()
+ request_id = fsm_data.get("request_id")
+ await close_case(request_id, session)
+ await callback.message.edit_text(
+ "Заявка закрыта!",
+ reply_markup=await get_inline_keyboard(previous_menu=back_option),
+ )
+ except Exception as e:
+ await callback.message.answer(f"Произошла ошибка: {e}")
diff --git a/app/alembic/versions/07c781d887d8_fix_delete_some_field_in_feedback.py b/app/alembic/versions/07c781d887d8_fix_delete_some_field_in_feedback.py
new file mode 100644
index 0000000..61318f5
--- /dev/null
+++ b/app/alembic/versions/07c781d887d8_fix_delete_some_field_in_feedback.py
@@ -0,0 +1,30 @@
+"""fix delete some field in feedback
+
+Revision ID: 07c781d887d8
+Revises: c493d913699d
+Create Date: 2024-10-14 13:40:54.256167
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '07c781d887d8'
+down_revision: Union[str, None] = 'c493d913699d'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
diff --git a/app/alembic/versions/c493d913699d_fix_delete_some_field_in_feedback.py b/app/alembic/versions/c493d913699d_fix_delete_some_field_in_feedback.py
new file mode 100644
index 0000000..26af740
--- /dev/null
+++ b/app/alembic/versions/c493d913699d_fix_delete_some_field_in_feedback.py
@@ -0,0 +1,30 @@
+"""fix delete some field in feedback
+
+Revision ID: c493d913699d
+Revises: c4b40ca2f35e
+Create Date: 2024-10-14 13:39:49.912216
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = 'c493d913699d'
+down_revision: Union[str, None] = 'c4b40ca2f35e'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ pass
+ # ### end Alembic commands ###
diff --git a/app/alembic/versions/b6f7d68b3c6b_fix_shipping_close_add_nullable_true.py b/app/alembic/versions/c4b40ca2f35e_fix_delete_some_field_in_feedback.py
similarity index 92%
rename from app/alembic/versions/b6f7d68b3c6b_fix_shipping_close_add_nullable_true.py
rename to app/alembic/versions/c4b40ca2f35e_fix_delete_some_field_in_feedback.py
index c1cbc6b..d055eae 100644
--- a/app/alembic/versions/b6f7d68b3c6b_fix_shipping_close_add_nullable_true.py
+++ b/app/alembic/versions/c4b40ca2f35e_fix_delete_some_field_in_feedback.py
@@ -1,8 +1,8 @@
-"""fix shipping close add nullable true
+"""fix delete some field in feedback
-Revision ID: b6f7d68b3c6b
+Revision ID: c4b40ca2f35e
Revises:
-Create Date: 2024-10-14 10:31:13.807609
+Create Date: 2024-10-14 11:16:32.374280
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
-revision: str = 'b6f7d68b3c6b'
+revision: str = 'c4b40ca2f35e'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -77,12 +77,9 @@ def upgrade() -> None:
op.create_index(op.f('ix_categorytype_product_id'), 'categorytype', ['product_id'], unique=False)
op.create_table('feedback',
sa.Column('user', sa.Integer(), nullable=False),
- sa.Column('contact_manager_id', sa.Integer(), nullable=False),
sa.Column('feedback_text', sa.TEXT(), nullable=False),
sa.Column('feedback_date', postgresql.TIMESTAMP(), nullable=False),
- sa.Column('unread', sa.BOOLEAN(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['contact_manager_id'], ['contactmanager.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
diff --git a/app/crud/feedback_crud.py b/app/crud/feedback_crud.py
index 9fe5c7a..8ac5482 100644
--- a/app/crud/feedback_crud.py
+++ b/app/crud/feedback_crud.py
@@ -1,57 +1,57 @@
-from sqlalchemy import select, desc
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.orm import joinedload
-
-from .base_crud import CRUDBase
-from models.models import Feedback
-
-
-class FeedbackCRUD(CRUDBase):
- async def get_new_feedbacks(
- self,
- session: AsyncSession,
- ):
- """Получить список пользователей ожидающих обратного звонка."""
- users_to_callback = await session.execute(
- select(self.model)
- .where(self.model.unread)
- .order_by(desc(self.model.feedback_date))
- )
- return users_to_callback.scalars().all()
-
- async def mark_as_read(self, feedback: Feedback, session: AsyncSession):
- """Открыть заявку на обратный звонок."""
- setattr(feedback, "unread", False)
- session.add(feedback)
- await session.commit()
- await session.refresh(feedback)
- return feedback
-
- async def get_multi(self, session: AsyncSession):
- """Получить список всех объектов модели из БД."""
- db_objs = await session.execute(
- select(self.model).order_by(desc(self.model.feedback_date))
- )
- return db_objs.scalars().all()
-
- async def bulk_create(
- self,
- objs_in: list,
- session: AsyncSession,
- ):
- db_objs = [self.model(**obj) for obj in objs_in]
- session.add_all(db_objs)
- await session.commit()
- return db_objs
-
- async def get(self, feedback_id: int, session: AsyncSession):
- """Получить объект отзыва вместе с его автором"""
- feedback_with_user = await session.execute(
- select(Feedback)
- .options(joinedload(Feedback.author))
- .where(Feedback.id == feedback_id)
- )
- return feedback_with_user.scalar_one()
-
-
-feedback_crud = FeedbackCRUD(Feedback)
+# from sqlalchemy import select, desc
+# from sqlalchemy.ext.asyncio import AsyncSession
+# from sqlalchemy.orm import joinedload
+
+# from .base_crud import CRUDBase
+# from models.models import Feedback
+
+
+# class FeedbackCRUD(CRUDBase):
+# async def get_new_feedbacks(
+# self,
+# session: AsyncSession,
+# ):
+# """Получить список пользователей ожидающих обратного звонка."""
+# users_to_callback = await session.execute(
+# select(self.model)
+# .where(self.model.unread)
+# .order_by(desc(self.model.feedback_date))
+# )
+# return users_to_callback.scalars().all()
+
+# async def mark_as_read(self, feedback: Feedback, session: AsyncSession):
+# """Открыть заявку на обратный звонок."""
+# setattr(feedback, "unread", False)
+# session.add(feedback)
+# await session.commit()
+# await session.refresh(feedback)
+# return feedback
+
+# async def get_multi(self, session: AsyncSession):
+# """Получить список всех объектов модели из БД."""
+# db_objs = await session.execute(
+# select(self.model).order_by(desc(self.model.feedback_date))
+# )
+# return db_objs.scalars().all()
+
+# async def bulk_create(
+# self,
+# objs_in: list,
+# session: AsyncSession,
+# ):
+# db_objs = [self.model(**obj) for obj in objs_in]
+# session.add_all(db_objs)
+# await session.commit()
+# return db_objs
+
+# async def get(self, feedback_id: int, session: AsyncSession):
+# """Получить объект отзыва вместе с его автором"""
+# feedback_with_user = await session.execute(
+# select(Feedback)
+# .options(joinedload(Feedback.author))
+# .where(Feedback.id == feedback_id)
+# )
+# return feedback_with_user.scalar_one()
+
+
+# feedback_crud = FeedbackCRUD(Feedback)
diff --git a/app/crud/request_to_manager.py b/app/crud/request_to_manager.py
index 6f0c382..38274a7 100644
--- a/app/crud/request_to_manager.py
+++ b/app/crud/request_to_manager.py
@@ -1,11 +1,12 @@
from datetime import datetime
+from sqlalchemy import select, and_
from sqlalchemy.ext.asyncio import AsyncSession
from models.models import ContactManager
async def create_request_to_manager(
- user_data: dict, request_type: str, session: AsyncSession
+ user_data: dict, request_type: str, session: AsyncSession
) -> ContactManager:
"""Создание заявки на связь с менеджером."""
@@ -21,3 +22,51 @@ async def create_request_to_manager(
await session.refresh(data_to_db)
return data_to_db
+
+
+async def get_request(request_id: int, session: AsyncSession):
+ """Получить запрос по id."""
+ request = await session.execute(
+ select(ContactManager).where(ContactManager.id == request_id)
+ )
+ return request.scalars().first()
+
+
+async def get_all_support_requests(session: AsyncSession):
+ """Получить список активных заявок на поддержку."""
+ support_requests = await session.execute(
+ select(ContactManager).where(
+ and_(
+ ContactManager.shipping_date_close.is_(None),
+ ContactManager.need_support.is_(True),
+ )
+ )
+ )
+ return support_requests.scalars().all()
+
+
+async def get_all_manager_requests(session: AsyncSession):
+ """Получить список активных заявок на звонок менеджера."""
+ support_requests = await session.execute(
+ select(ContactManager).where(
+ and_(
+ ContactManager.shipping_date_close.is_(None),
+ ContactManager.need_contact_with_manager.is_(True),
+ )
+ )
+ )
+ return support_requests.scalars().all()
+
+
+async def close_case(request_id: int, session: AsyncSession):
+ """Закрыть заявку."""
+ case_to_close = await get_request(request_id, session)
+ if case_to_close.need_contact_with_manager:
+ setattr(case_to_close, "need_contact_with_manager", False)
+ elif case_to_close.need_support:
+ setattr(case_to_close, "need_support", False)
+ setattr(case_to_close, "shipping_date_close", datetime.now())
+ session.add(case_to_close)
+ await session.commit()
+ await session.refresh(case_to_close)
+ return case_to_close
diff --git a/app/models/models.py b/app/models/models.py
index 3e3729d..c2b0b39 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -39,10 +39,6 @@ class User(Base):
nullable=False,
)
- feedbacks = relationship(
- "Feedback", back_populates="author", cascade="all, delete"
- )
-
class ProductCategory(Base):
"""БД модель продуктов и услуг."""
@@ -140,26 +136,16 @@ class ContactManager(Base):
pgsql_types.TIMESTAMP(timezone=True), nullable=True
)
- feedbacks = relationship(
- "Feedback", back_populates="contact_manager", cascade="all, delete"
- )
-
class Feedback(Base):
+ """БД модель для отзывов."""
+
user: Mapped[int] = mapped_column(
ForeignKey("user.id", ondelete="CASCADE"), nullable=False
)
- contact_manager_id: Mapped[int] = mapped_column(
- ForeignKey("contactmanager.id", ondelete="CASCADE"), nullable=False
- )
feedback_text: Mapped[str] = mapped_column(
pgsql_types.TEXT, nullable=False
)
feedback_date: Mapped[datetime] = mapped_column(
pgsql_types.TIMESTAMP, default=datetime.now
)
- unread: Mapped[bool] = mapped_column(pgsql_types.BOOLEAN, default=True)
- author = relationship("User", back_populates="feedbacks")
- contact_manager = relationship(
- "ContactManager", back_populates="feedbacks"
- )
From 7f461e6c8f02dba20d247404a17435b1e61c1bfe Mon Sep 17 00:00:00 2001
From: ikhit
Date: Mon, 14 Oct 2024 17:50:29 +0300
Subject: [PATCH 44/75] add user promotion menu. not working atm
---
app/admin/admin_settings.py | 15 ++-
app/admin/filters/filters.py | 28 +++-
app/admin/handlers/admin_handlers/__init__.py | 2 +
app/admin/handlers/admin_handlers/admin.py | 6 +-
.../admin_about_company_handlers.py | 4 +-
.../admin_handlers/admin_category_handlers.py | 5 +-
.../admin_handlers/admin_info_handlers.py | 41 +++---
.../admin_portfolio_handlers.py | 6 +-
.../admin_handlers/admin_product_handlers.py | 5 +-
.../admin_handlers/admin_promotion.py | 121 ++++++++++++++++++
.../handlers/admin_handlers/admin_special.py | 11 +-
app/crud/user_crud.py | 48 ++++---
12 files changed, 230 insertions(+), 62 deletions(-)
create mode 100644 app/admin/handlers/admin_handlers/admin_promotion.py
diff --git a/app/admin/admin_settings.py b/app/admin/admin_settings.py
index fd2d04e..bc733fb 100644
--- a/app/admin/admin_settings.py
+++ b/app/admin/admin_settings.py
@@ -54,9 +54,22 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
ADMIN_SPECIAL_OPTIONS = {
"manager_request": "Запросы на обратный звонок",
"support_request": "Запросы на техподдержку",
- "Feedbacks": "Отзывы",
+ "feedbacks": "Отзывы",
}
ADMIN_SPECIAL_BUTTONS = get_buttons(ADMIN_SPECIAL_OPTIONS)
+SUPERUSER_SPECIAL_OPTIONS = {
+ "manager_request": "Запросы на обратный звонок",
+ "support_request": "Запросы на техподдержку",
+ "feedbacks": "Отзывы",
+ "promotion": "Управление персоналом",
+}
+SUPERUSER_SPECIAL_BUTTONS = get_buttons(ADMIN_SPECIAL_OPTIONS)
+SUPERUSER_PROMOTION_OPTIONS = {
+ "manager_list": "Список менеджеров",
+ "promote": "Добавить менеджера",
+ "demote": "Убрать менеджера",
+}
+SUPERUSER_PROMOTION_BUTTONS = get_buttons(SUPERUSER_PROMOTION_OPTIONS)
# Кнопки экранной клавиатуры
BASE_BUTTONS = {
"main_menu": "Главное меню",
diff --git a/app/admin/filters/filters.py b/app/admin/filters/filters.py
index 3f2b670..09088f4 100644
--- a/app/admin/filters/filters.py
+++ b/app/admin/filters/filters.py
@@ -1,7 +1,9 @@
from aiogram.filters import Filter
from aiogram import Bot, types
+from sqlalchemy.ext.asyncio import AsyncSession
-from admin.admin_settings import admin_list
+from crud.user_crud import user_crud
+from models.models import RoleEnum
class ChatTypeFilter(Filter):
@@ -12,9 +14,27 @@ async def __call__(self, message: types.Message) -> bool:
return message.chat.type in self.chat_types
-class IsAdmin(Filter):
+class IsManagerOrAdmin(Filter):
def __init__(self) -> None:
pass
- async def __call__(self, message: types.Message, bot: Bot) -> bool:
- return message.from_user.id in admin_list
+ async def __call__(
+ self, message: types.Message, bot: Bot, session: AsyncSession
+ ) -> bool:
+ user_role = await user_crud.get_role_by_tg_id(
+ message.from_user.id, session
+ )
+ return user_role in {RoleEnum.ADMIN, RoleEnum.MANAGER}
+
+
+class IsAdminOnly(Filter):
+ def __init__(self) -> None:
+ pass
+
+ async def __call__(
+ self, message: types.Message, bot: Bot, session: AsyncSession
+ ) -> bool:
+ user_role = await user_crud.get_role_by_tg_id(
+ message.from_user.id, session
+ )
+ return user_role == RoleEnum.ADMIN
diff --git a/app/admin/handlers/admin_handlers/__init__.py b/app/admin/handlers/admin_handlers/__init__.py
index 00f2b2c..4cc1e97 100644
--- a/app/admin/handlers/admin_handlers/__init__.py
+++ b/app/admin/handlers/admin_handlers/__init__.py
@@ -7,6 +7,7 @@
from .admin_product_handlers import product_router
from .admin_category_handlers import category_router
from .admin_special import admin_special_router
+from .admin_promotion import superuser_router
admin_router = Router()
admin_router.include_router(admin_main_router)
@@ -15,4 +16,5 @@
admin_router.include_router(portfolio_router)
admin_router.include_router(product_router)
admin_router.include_router(category_router)
+admin_router.include_router(superuser_router)
admin_router.include_router(admin_special_router)
diff --git a/app/admin/handlers/admin_handlers/admin.py b/app/admin/handlers/admin_handlers/admin.py
index 95afd2d..059ce97 100644
--- a/app/admin/handlers/admin_handlers/admin.py
+++ b/app/admin/handlers/admin_handlers/admin.py
@@ -4,7 +4,7 @@
from aiogram.fsm.state import State, StatesGroup
-from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.filters.filters import ChatTypeFilter, IsManagerOrAdmin
from admin.keyboards.keyboards import (
get_inline_keyboard,
)
@@ -21,7 +21,9 @@
admin_main_router = Router()
-admin_main_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+admin_main_router.message.filter(
+ ChatTypeFilter(["private"]), IsManagerOrAdmin()
+)
class UserState(StatesGroup):
diff --git a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
index e83a97b..b3520d2 100644
--- a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
@@ -10,7 +10,7 @@
from .admin import SectionState
from bot.exceptions import message_exception_handler
from crud.about_crud import company_info_crud
-from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.filters.filters import ChatTypeFilter, IsManagerOrAdmin
from admin.admin_managers import (
DeleteManager,
DeleteState,
@@ -31,7 +31,7 @@
PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("company_bio")
about_router = Router()
-about_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+about_router.message.filter(ChatTypeFilter(["private"]), IsManagerOrAdmin())
about_create_manager = CreateManager(company_info_crud, PREVIOUS_MENU)
about_delete_manager = DeleteManager(company_info_crud, PREVIOUS_MENU)
diff --git a/app/admin/handlers/admin_handlers/admin_category_handlers.py b/app/admin/handlers/admin_handlers/admin_category_handlers.py
index 32790f4..747b1ed 100644
--- a/app/admin/handlers/admin_handlers/admin_category_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_category_handlers.py
@@ -8,7 +8,7 @@
from sqlalchemy.ext.asyncio import AsyncSession
from admin.handlers.admin_handlers.admin import SectionState
-from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.filters.filters import ChatTypeFilter, IsManagerOrAdmin
from admin.admin_managers import (
CreateCategoryManager,
UpdateCategoryManager,
@@ -32,7 +32,7 @@
logger = logging.getLogger(__name__)
category_router = Router()
-category_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+category_router.message.filter(ChatTypeFilter(["private"]), IsManagerOrAdmin())
PREVIOUS_MENU = "Назад"
@@ -324,4 +324,3 @@ async def update_about_info(
logger.info(
f"Пользователь {message.from_user.id} обновил информацию о категории в БД."
)
-
diff --git a/app/admin/handlers/admin_handlers/admin_info_handlers.py b/app/admin/handlers/admin_handlers/admin_info_handlers.py
index bdd44c9..0298bea 100644
--- a/app/admin/handlers/admin_handlers/admin_info_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_info_handlers.py
@@ -14,7 +14,7 @@
QuestionDeleteManager,
DeleteQuestionStates,
)
-from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.filters.filters import ChatTypeFilter, IsManagerOrAdmin
from admin.handlers.admin_handlers.admin import SectionState
from admin.admin_settings import (
ADMIN_BASE_OPTIONS,
@@ -30,15 +30,14 @@
info_router = Router()
-info_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+info_router.message.filter(ChatTypeFilter(["private"]), IsManagerOrAdmin())
question_create_manager = QuestionCreateManager()
question_update_manager = QuestionUpdateManager()
question_delete_manager = QuestionDeleteManager()
-@message_exception_handler(
- log_error_text='Ошибка при добавлении вопроса'
-)
+
+@message_exception_handler(log_error_text='Ошибка при добавлении вопроса')
@info_router.callback_query(
or_f(SectionState.general_questions, SectionState.problems_with_products),
F.data == ADMIN_BASE_OPTIONS.get("create"),
@@ -70,7 +69,9 @@ async def add_question_answer(
):
"""Добавить ответ на вопрос в БД."""
await question_create_manager.add_question_to_db(message, state, session)
- logger.info(f'Пользователь {message.from_user.id} добавил ответ на вопрос в БД.')
+ logger.info(
+ f'Пользователь {message.from_user.id} добавил ответ на вопрос в БД.'
+ )
@message_exception_handler(
@@ -90,7 +91,9 @@ async def question_to_delete(
next_state=DeleteQuestionStates.select,
session=session,
)
- logger.info(f'Пользователь {callback.from_user.id} выбрал вопрос для удаления.')
+ logger.info(
+ f'Пользователь {callback.from_user.id} выбрал вопрос для удаления.'
+ )
@message_exception_handler(
@@ -105,12 +108,12 @@ async def confirm_delete_question(
):
"""Подтвердить удаление вопроса."""
await question_delete_manager.confirm_delete(callback, state, session)
- logger.info(f'Пользователь {callback.from_user.id} подтвердил удаление вопроса.')
+ logger.info(
+ f'Пользователь {callback.from_user.id} подтвердил удаление вопроса.'
+ )
-@message_exception_handler(
- log_error_text='Ошибка при удалении вопроса из БД'
-)
+@message_exception_handler(log_error_text='Ошибка при удалении вопроса из БД')
@info_router.callback_query(
DeleteQuestionStates.confirm,
and_f(F.data != PROBLEMS_MENU, F.data != GENERAL_QEUSTIONS_MENU),
@@ -140,7 +143,9 @@ async def update_question(
next_state=UpdateQuestionStates.select,
session=session,
)
- logger.info(f'Пользователь {callback.from_user.id} выбрал вопрос для обновления.')
+ logger.info(
+ f'Пользователь {callback.from_user.id} выбрал вопрос для обновления.'
+ )
@message_exception_handler(
@@ -160,7 +165,9 @@ async def update_question_choice(
):
"""Обработать выбор данных для обновления вопроса."""
await question_update_manager.update_data_type(callback, session)
- logger.info(f'Пользователь {callback.from_user.id} выбрал данные для обновления вопроса.')
+ logger.info(
+ f'Пользователь {callback.from_user.id} выбрал данные для обновления вопроса.'
+ )
@message_exception_handler(
@@ -185,7 +192,9 @@ async def update_question_text(callback: CallbackQuery, state: FSMContext):
async def update_question_answer(callback: CallbackQuery, state: FSMContext):
"""Обновить ответ на вопрос."""
await question_update_manager.update_answer(callback, state)
- logger.info(f'Пользователь {callback.from_user.id} обновил ответ на вопрос.')
+ logger.info(
+ f'Пользователь {callback.from_user.id} обновил ответ на вопрос.'
+ )
@message_exception_handler(
@@ -201,4 +210,6 @@ async def update_question_data(
await question_update_manager.update_question_in_db(
message, state, session
)
- logger.info(f'Пользователь {message.from_user.id} обновил данные вопроса в БД.')
+ logger.info(
+ f'Пользователь {message.from_user.id} обновил данные вопроса в БД.'
+ )
diff --git a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
index f2c1c80..2f98fdb 100644
--- a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
@@ -9,7 +9,7 @@
from admin.handlers.admin_handlers.admin import SectionState
from bot.exceptions import message_exception_handler
from crud.portfolio_projects_crud import portfolio_crud
-from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.filters.filters import ChatTypeFilter, IsManagerOrAdmin
from admin.admin_managers import (
DeleteManager,
DeleteState,
@@ -30,7 +30,9 @@
logger = logging.getLogger(__name__)
portfolio_router = Router()
-portfolio_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+portfolio_router.message.filter(
+ ChatTypeFilter(["private"]), IsManagerOrAdmin()
+)
PREVIOUS_MENU = PORTFOLIO_MENU_OPTIONS.get("other_projects")
diff --git a/app/admin/handlers/admin_handlers/admin_product_handlers.py b/app/admin/handlers/admin_handlers/admin_product_handlers.py
index c41f7fc..a63cb13 100644
--- a/app/admin/handlers/admin_handlers/admin_product_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_product_handlers.py
@@ -11,7 +11,7 @@
from .admin import SectionState
from crud.product_crud import product_crud
-from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.filters.filters import ChatTypeFilter, IsManagerOrAdmin
from admin.admin_managers import (
CreateManager,
UpdateManager,
@@ -49,7 +49,7 @@ class ProductDeleteState(StatesGroup):
product_router = Router()
-product_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
+product_router.message.filter(ChatTypeFilter(["private"]), IsManagerOrAdmin())
PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("products")
@@ -217,4 +217,3 @@ async def update_about_info(
logger.info(
f"Пользователь {message.from_user.id} внес изменения продукта в БД."
)
-
diff --git a/app/admin/handlers/admin_handlers/admin_promotion.py b/app/admin/handlers/admin_handlers/admin_promotion.py
new file mode 100644
index 0000000..3ab61a9
--- /dev/null
+++ b/app/admin/handlers/admin_handlers/admin_promotion.py
@@ -0,0 +1,121 @@
+from aiogram import Router, F
+from aiogram.types import CallbackQuery, Message
+from aiogram.fsm.context import FSMContext
+from aiogram.filters import or_f
+from aiogram.fsm.state import State, StatesGroup
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from admin.filters.filters import ChatTypeFilter, IsAdminOnly
+from admin.keyboards.keyboards import (
+ get_inline_keyboard,
+)
+from admin.admin_settings import (
+ MAIN_MENU_OPTIONS,
+ MAIN_MENU_TEXT,
+ SUPERUSER_PROMOTION_BUTTONS,
+ SUPERUSER_PROMOTION_OPTIONS,
+ SUPERUSER_SPECIAL_BUTTONS,
+ SUPERUSER_SPECIAL_OPTIONS,
+)
+from models.models import User, RoleEnum
+from crud.user_crud import user_crud
+
+superuser_router = Router()
+superuser_router.message.filter(
+ ChatTypeFilter(["private"]), IsAdminOnly()
+)
+
+PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("admin_special")
+
+
+class RoleState(StatesGroup):
+ promote = State()
+ demote = State()
+
+
+@superuser_router.callback_query(
+ F.data == MAIN_MENU_OPTIONS.get("admin_special")
+)
+async def get_admin_special_options(callback: CallbackQuery):
+ await callback.message.edit_text(
+ "Дополнительная информация для администрации:",
+ reply_markup=await get_inline_keyboard(
+ SUPERUSER_SPECIAL_BUTTONS, previous_menu=MAIN_MENU_TEXT
+ ),
+ )
+
+
+@superuser_router.callback_query(
+ F.data == SUPERUSER_SPECIAL_OPTIONS.get("promotion")
+)
+async def get_superuser_options(callback: CallbackQuery):
+ """Перейти в меню управления персоналом."""
+ await callback.message.edit_text(
+ SUPERUSER_SPECIAL_OPTIONS.get("promotion"),
+ reply_markup=await get_inline_keyboard(
+ SUPERUSER_PROMOTION_BUTTONS,
+ previous_menu=PREVIOUS_MENU,
+ ),
+ )
+
+
+@superuser_router.callback_query(
+ F.data == SUPERUSER_PROMOTION_OPTIONS.get("manager_list")
+)
+async def get_manager_list(callback: CallbackQuery, session: AsyncSession):
+ """Получить список менеджеров."""
+ manager_list = await user_crud.get_manager_list(session)
+ managers_tg_ids = [manager.tg_id + "\n\n" for manager in manager_list]
+ await callback.message.edit_text(
+ *managers_tg_ids,
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+
+
+@superuser_router.callback_query(
+ or_f(
+ F.data == SUPERUSER_PROMOTION_OPTIONS.get("promote"),
+ F.data == SUPERUSER_PROMOTION_OPTIONS.get("demote"),
+ )
+)
+async def get_user_id_for_action(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Ввести телеграм id пользователя для смены роли."""
+ if callback.data == SUPERUSER_PROMOTION_OPTIONS.get("promote"):
+ await state.set_state(RoleState.promote)
+ elif callback.data == SUPERUSER_PROMOTION_OPTIONS.get("demote"):
+ await state.set_state(RoleState.demote)
+ await callback.message.edit_text(
+ "Введите id телеграм-пользователя:",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+
+
+@superuser_router.message(RoleState(), F.text.isnumeric())
+async def change_user_role(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ """
+ Проверить наличие пользователя в базе.
+ Проверить роль пользователя.
+ Изменить роль пользователя
+ """
+ user: User = await user_crud.get_user_by_tg_id(message.text, session)
+ if not user:
+ message_text = "Такого пользователя в базе нет!"
+ if user.role == RoleEnum.ADMIN:
+ message_text = "Нельзя менять роль админа!"
+ current_state = await state.get_state()
+ if current_state == RoleState.promote.state:
+ message_text = f"Пользователь {message.text} назначен менеджером!"
+ await user_crud.promote_to_manager(user, session)
+ elif current_state == RoleState.demote.state:
+ message_text == (
+ f"Позльователь {message.text} теперь просто пользователь!"
+ )
+ await user_crud.demote_to_user(user, session)
+ await message.answer(
+ message_text,
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
diff --git a/app/admin/handlers/admin_handlers/admin_special.py b/app/admin/handlers/admin_handlers/admin_special.py
index ea5ec1b..c8e2bef 100644
--- a/app/admin/handlers/admin_handlers/admin_special.py
+++ b/app/admin/handlers/admin_handlers/admin_special.py
@@ -4,7 +4,7 @@
from aiogram.fsm.state import State, StatesGroup
from sqlalchemy.ext.asyncio import AsyncSession
-from admin.filters.filters import ChatTypeFilter, IsAdmin
+from admin.filters.filters import ChatTypeFilter, IsManagerOrAdmin
from admin.keyboards.keyboards import (
get_inline_keyboard,
)
@@ -23,16 +23,17 @@
get_request,
)
+admin_special_router = Router()
+admin_special_router.message.filter(
+ ChatTypeFilter(["private"]), IsManagerOrAdmin()
+)
+
class RequestState(StatesGroup):
manager_request = State()
support_request = State()
-admin_special_router = Router()
-admin_special_router.message.filter(ChatTypeFilter(["private"]), IsAdmin())
-
-
async def get_state_name(state: str) -> str:
"""Получить название состояния и вернуть его название."""
state_name = state.split(":")[1]
diff --git a/app/crud/user_crud.py b/app/crud/user_crud.py
index 5bf1ff9..d09cb0e 100644
--- a/app/crud/user_crud.py
+++ b/app/crud/user_crud.py
@@ -1,11 +1,9 @@
-from datetime import datetime
-
from .base_crud import CRUDBase
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
-from models.models import User
+from models.models import User, RoleEnum
class UserCRUD(CRUDBase):
@@ -32,37 +30,37 @@ async def get_user_by_tg_id(self, tg_id: int, session: AsyncSession):
return user.scalars().first()
- async def case_open(self, user: User, session: AsyncSession):
- """Открыть заявку на обратный звонок."""
-
- setattr(user, "callback_request", True)
- setattr(user, "callback_request_date", datetime.now())
+ async def get_role_by_tg_id(
+ self, tg_id: int, session: AsyncSession
+ ) -> User:
+ """Получаем роль пользователя по его tg_id."""
- session.add(user)
- await session.commit()
- await session.refresh(user)
- return user
+ result = await session.execute(
+ select(self.model.role).where(self.model.tg_id == tg_id)
+ )
- async def close_case(self, user: User, session: AsyncSession):
- """Закрыть заявку на обратный звонок."""
+ return result.scalar()
- setattr(user, "callback_request", False)
- setattr(user, "case_closed_date", datetime.now())
+ async def get_manager_list(self, session: AsyncSession) -> list[User]:
+ """Получить список менеджеров."""
+ manager_list = await session.execute(
+ select(self.model).where(self.model.role == RoleEnum.MANAGER)
+ )
+ return manager_list.scalars().all()
+ async def promote_to_manager(self, user: User, session: AsyncSession):
+ """Назначить пользователя менеджером."""
+ setattr(user, "role", RoleEnum.MANAGER)
session.add(user)
await session.commit()
await session.refresh(user)
- return user
- async def bulk_create(
- self,
- objs_in: list,
- session: AsyncSession,
- ):
- db_objs = [self.model(**obj) for obj in objs_in]
- session.add_all(db_objs)
+ async def demote_to_user(self, user: User, session: AsyncSession):
+ """Снять с пользователя роль менеджера."""
+ setattr(user, "role", RoleEnum.USER)
+ session.add(user)
await session.commit()
- return db_objs
+ await session.refresh(user)
user_crud = UserCRUD(User)
From 9942c50d020890ad1ff5e65ea4d8f0234669745b Mon Sep 17 00:00:00 2001
From: ikhit
Date: Mon, 14 Oct 2024 23:14:37 +0300
Subject: [PATCH 45/75] yet another
---
app/admin/admin_settings.py | 2 +-
app/admin/handlers/admin_handlers/admin_promotion.py | 3 ++-
app/models/models.py | 1 +
3 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/app/admin/admin_settings.py b/app/admin/admin_settings.py
index bc733fb..c91af7a 100644
--- a/app/admin/admin_settings.py
+++ b/app/admin/admin_settings.py
@@ -63,7 +63,7 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
"feedbacks": "Отзывы",
"promotion": "Управление персоналом",
}
-SUPERUSER_SPECIAL_BUTTONS = get_buttons(ADMIN_SPECIAL_OPTIONS)
+SUPERUSER_SPECIAL_BUTTONS = get_buttons(SUPERUSER_SPECIAL_OPTIONS)
SUPERUSER_PROMOTION_OPTIONS = {
"manager_list": "Список менеджеров",
"promote": "Добавить менеджера",
diff --git a/app/admin/handlers/admin_handlers/admin_promotion.py b/app/admin/handlers/admin_handlers/admin_promotion.py
index 3ab61a9..8098d4f 100644
--- a/app/admin/handlers/admin_handlers/admin_promotion.py
+++ b/app/admin/handlers/admin_handlers/admin_promotion.py
@@ -66,8 +66,9 @@ async def get_manager_list(callback: CallbackQuery, session: AsyncSession):
"""Получить список менеджеров."""
manager_list = await user_crud.get_manager_list(session)
managers_tg_ids = [manager.tg_id + "\n\n" for manager in manager_list]
+ managers = "".join(managers_tg_ids) if managers_tg_ids else "Список пуст!"
await callback.message.edit_text(
- *managers_tg_ids,
+ managers,
reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
)
diff --git a/app/models/models.py b/app/models/models.py
index c2b0b39..773471e 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -149,3 +149,4 @@ class Feedback(Base):
feedback_date: Mapped[datetime] = mapped_column(
pgsql_types.TIMESTAMP, default=datetime.now
)
+ rating: Mapped[int] = mapped_column(pgsql_types.INTEGER, nullable=False)
From 579835c2ac7570ec39abb5d485793ed10e6eca58 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Tue, 15 Oct 2024 13:20:47 +0300
Subject: [PATCH 46/75] add superuser options
---
app/admin/admin_managers/create_manager.py | 23 +-
app/admin/admin_managers/update_manager.py | 23 +-
app/admin/handlers/admin_handlers/__init__.py | 4 +-
app/admin/handlers/admin_handlers/admin.py | 1 +
.../admin_about_company_handlers.py | 1 +
.../admin_handlers/admin_category_handlers.py | 1 +
.../admin_handlers/admin_info_handlers.py | 1 +
.../admin_portfolio_handlers.py | 61 ++++--
.../admin_handlers/admin_product_handlers.py | 7 +-
.../admin_handlers/admin_promotion.py | 122 -----------
...n_special.py => admin_special_handlers.py} | 12 +-
.../admin_handlers/superuser_handlers.py | 207 ++++++++++++++++++
app/admin/handlers/user.py | 51 +----
app/admin/handlers/validators.py | 11 +
...887d8_fix_delete_some_field_in_feedback.py | 30 ---
...c71b30_add_manager_and_user_connection.py} | 34 +--
...3699d_fix_delete_some_field_in_feedback.py | 30 ---
app/core/db.py | 2 +-
app/crud/about_crud.py | 7 -
app/crud/feedback_crud.py | 72 ++----
app/crud/request_to_manager.py | 28 ++-
app/crud/user_crud.py | 5 +-
app/models/models.py | 15 ++
app/test.py | 23 ++
24 files changed, 435 insertions(+), 336 deletions(-)
delete mode 100644 app/admin/handlers/admin_handlers/admin_promotion.py
rename app/admin/handlers/admin_handlers/{admin_special.py => admin_special_handlers.py} (93%)
create mode 100644 app/admin/handlers/admin_handlers/superuser_handlers.py
delete mode 100644 app/alembic/versions/07c781d887d8_fix_delete_some_field_in_feedback.py
rename app/alembic/versions/{c4b40ca2f35e_fix_delete_some_field_in_feedback.py => 53d69ac71b30_add_manager_and_user_connection.py} (91%)
delete mode 100644 app/alembic/versions/c493d913699d_fix_delete_some_field_in_feedback.py
create mode 100644 app/test.py
diff --git a/app/admin/admin_managers/create_manager.py b/app/admin/admin_managers/create_manager.py
index ab64b87..2cfedb7 100644
--- a/app/admin/admin_managers/create_manager.py
+++ b/app/admin/admin_managers/create_manager.py
@@ -3,15 +3,15 @@
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
-from crud.base_crud import CRUDBase
-
-from .base_manager import (
- BaseAdminManager,
-)
+from admin.handlers.validators import validate_url
from admin.keyboards.keyboards import (
get_inline_keyboard,
)
from admin.admin_settings import ADMIN_CONTENT_BUTTONS
+from .base_manager import (
+ BaseAdminManager,
+)
+from crud.base_crud import CRUDBase
class CreateState(StatesGroup):
@@ -183,7 +183,10 @@ async def add_obj_media(self, message: Message, state: FSMContext):
Добавить картинку к объекту и перейти в
следующее машинное состояние.
"""
- message_text = "Добавьте картинку и текст к ней:"
+ message_text = (
+ "Добавьте картинку и текст к ней. "
+ "Длина текста не должна превышать 2200 символов:"
+ )
await self.prompt_for_input(
message,
message_text,
@@ -213,6 +216,14 @@ async def add_obj_to_db(
try:
current_state = await state.get_state()
if current_state == self.states_group.url.state:
+ if not validate_url(message.text):
+ await message.answer(
+ ("Некорректный URL. Попробуйте добавить заново."),
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ return
await state.update_data(url=message.text)
elif current_state == self.states_group.description.state:
await state.update_data(description=message.text)
diff --git a/app/admin/admin_managers/update_manager.py b/app/admin/admin_managers/update_manager.py
index b4ab4c5..14da17a 100644
--- a/app/admin/admin_managers/update_manager.py
+++ b/app/admin/admin_managers/update_manager.py
@@ -2,9 +2,8 @@
from aiogram.types import CallbackQuery, Message
from aiogram.fsm.state import State, StatesGroup
from sqlalchemy.ext.asyncio import AsyncSession
-from crud.base_crud import CRUDBase
-from crud.portfolio_projects_crud import portfolio_crud
+from admin.handlers.validators import validate_url
from .base_manager import (
BaseAdminManager,
)
@@ -12,6 +11,8 @@
get_inline_keyboard,
)
from admin.admin_settings import ADMIN_UPDATE_BUTTONS
+from crud.base_crud import CRUDBase
+from crud.portfolio_projects_crud import portfolio_crud
class UpdateState(StatesGroup):
@@ -152,7 +153,10 @@ async def change_obj_content(
caption=self.obj_to_update.description,
)
await callback.message.answer(
- "Добавьте новую картинку и описание",
+ (
+ "Добавьте картинку и текст к ней. "
+ "Длина текста не должна превышать 2200 символов:"
+ ),
reply_markup=await get_inline_keyboard(
previous_menu=self.back_option
),
@@ -168,6 +172,14 @@ async def update_obj_in_db(
if current_state == self.states_group.name.state:
await state.update_data(name=message.text)
elif current_state == self.states_group.url.state:
+ if not validate_url(message.text):
+ await message.answer(
+ ("Некорректный URL. Попробуйте добавить заново."),
+ reply_markup=await get_inline_keyboard(
+ previous_menu=self.back_option
+ ),
+ )
+ return
await state.update_data(url=message.text)
elif current_state == self.states_group.description.state:
await state.update_data(description=message.text)
@@ -208,8 +220,9 @@ class UpdatePortfolio:
Вносит изменения в объект в БД и сбрасывает состояние.
"""
- def __init__(self, back_option: str) -> None:
+ def __init__(self, back_option: str, states_group: StatesGroup) -> None:
self.back_option = back_option
+ self.states_group = states_group
async def update_main_portfolio_url(
self, callback: CallbackQuery, state: FSMContext, session: AsyncSession
@@ -220,7 +233,7 @@ async def update_main_portfolio_url(
f"Текущий адрес ссылки: \n\n {self.obj_to_update.url} \n\n"
"Введите новый:"
)
- await state.set_state(self.states_group.portolio)
+ await state.set_state(self.states_group.portfolio)
await callback.message.edit_text(
message_text,
reply_markup=await get_inline_keyboard(
diff --git a/app/admin/handlers/admin_handlers/__init__.py b/app/admin/handlers/admin_handlers/__init__.py
index 4cc1e97..bdd5f15 100644
--- a/app/admin/handlers/admin_handlers/__init__.py
+++ b/app/admin/handlers/admin_handlers/__init__.py
@@ -6,8 +6,8 @@
from .admin_portfolio_handlers import portfolio_router
from .admin_product_handlers import product_router
from .admin_category_handlers import category_router
-from .admin_special import admin_special_router
-from .admin_promotion import superuser_router
+from .admin_special_handlers import admin_special_router
+from .superuser_handlers import superuser_router
admin_router = Router()
admin_router.include_router(admin_main_router)
diff --git a/app/admin/handlers/admin_handlers/admin.py b/app/admin/handlers/admin_handlers/admin.py
index 059ce97..87a614e 100644
--- a/app/admin/handlers/admin_handlers/admin.py
+++ b/app/admin/handlers/admin_handlers/admin.py
@@ -24,6 +24,7 @@
admin_main_router.message.filter(
ChatTypeFilter(["private"]), IsManagerOrAdmin()
)
+admin_main_router.callback_query.filter(IsManagerOrAdmin())
class UserState(StatesGroup):
diff --git a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
index b3520d2..29ecfaf 100644
--- a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
@@ -32,6 +32,7 @@
about_router = Router()
about_router.message.filter(ChatTypeFilter(["private"]), IsManagerOrAdmin())
+about_router.callback_query.filter(IsManagerOrAdmin())
about_create_manager = CreateManager(company_info_crud, PREVIOUS_MENU)
about_delete_manager = DeleteManager(company_info_crud, PREVIOUS_MENU)
diff --git a/app/admin/handlers/admin_handlers/admin_category_handlers.py b/app/admin/handlers/admin_handlers/admin_category_handlers.py
index 747b1ed..28b291f 100644
--- a/app/admin/handlers/admin_handlers/admin_category_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_category_handlers.py
@@ -33,6 +33,7 @@
category_router = Router()
category_router.message.filter(ChatTypeFilter(["private"]), IsManagerOrAdmin())
+category_router.callback_query.filter(IsManagerOrAdmin())
PREVIOUS_MENU = "Назад"
diff --git a/app/admin/handlers/admin_handlers/admin_info_handlers.py b/app/admin/handlers/admin_handlers/admin_info_handlers.py
index 0298bea..c9ad8d3 100644
--- a/app/admin/handlers/admin_handlers/admin_info_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_info_handlers.py
@@ -31,6 +31,7 @@
info_router = Router()
info_router.message.filter(ChatTypeFilter(["private"]), IsManagerOrAdmin())
+info_router.callback_query.filter(IsManagerOrAdmin())
question_create_manager = QuestionCreateManager()
question_update_manager = QuestionUpdateManager()
diff --git a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
index 2f98fdb..af04c54 100644
--- a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
@@ -3,6 +3,7 @@
from aiogram import F, Router
from aiogram.filters import and_f, or_f
from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,11 +13,8 @@
from admin.filters.filters import ChatTypeFilter, IsManagerOrAdmin
from admin.admin_managers import (
DeleteManager,
- DeleteState,
- CreateState,
CreateManager,
UpdateManager,
- UpdateState,
UpdatePortfolio,
)
from admin.admin_settings import (
@@ -33,14 +31,39 @@
portfolio_router.message.filter(
ChatTypeFilter(["private"]), IsManagerOrAdmin()
)
+portfolio_router.callback_query(IsManagerOrAdmin())
PREVIOUS_MENU = PORTFOLIO_MENU_OPTIONS.get("other_projects")
-portfolio_create_manager = CreateManager(portfolio_crud, PREVIOUS_MENU)
-portfolio_delete_manager = DeleteManager(portfolio_crud, PREVIOUS_MENU)
-portfolio_update_manager = UpdateManager(portfolio_crud, PREVIOUS_MENU)
+
+class PortfolioCreateState(StatesGroup):
+ name = State()
+ url = State()
+
+
+class PortfolioDeleteState(StatesGroup):
+ select = State()
+ confirm = State()
+
+
+class PortfolioUpdateState(StatesGroup):
+ select = State()
+ name = State()
+ url = State()
+ portfolio = State()
+
+
+portfolio_create_manager = CreateManager(
+ portfolio_crud, PREVIOUS_MENU, PortfolioCreateState()
+)
+portfolio_delete_manager = DeleteManager(
+ portfolio_crud, PREVIOUS_MENU, PortfolioDeleteState()
+)
+portfolio_update_manager = UpdateManager(
+ portfolio_crud, PREVIOUS_MENU, PortfolioUpdateState
+)
main_portfolio_url_update_manager = UpdatePortfolio(
- MAIN_MENU_OPTIONS.get("portfolio")
+ MAIN_MENU_OPTIONS.get("portfolio"), PortfolioUpdateState()
)
@@ -63,7 +86,7 @@ async def add_portfolio_project_name(
@message_exception_handler(
log_error_text="Ошибка при сохранении имени нового объекта портфолио"
)
-@portfolio_router.message(CreateState.name, F.text)
+@portfolio_router.message(PortfolioCreateState.name, F.text)
async def add_portfolio_project_url(message: Message, state: FSMContext):
"""Сохранить имя нового объекта в состояние."""
await portfolio_create_manager.add_obj_url(message, state)
@@ -75,7 +98,7 @@ async def add_portfolio_project_url(message: Message, state: FSMContext):
@message_exception_handler(
log_error_text="Ошибка при сохранении URL нового объекта в БД"
)
-@portfolio_router.message(CreateState.url, F.text)
+@portfolio_router.message(PortfolioCreateState.url, F.text)
async def create_portfolio_project(
message: Message, state: FSMContext, session: AsyncSession
):
@@ -107,7 +130,9 @@ async def portfolio_project_to_delete(
@message_exception_handler(
log_error_text="Ошибка при подтверждении удаления проекта"
)
-@portfolio_router.callback_query(DeleteState.select, F.data != PREVIOUS_MENU)
+@portfolio_router.callback_query(
+ PortfolioDeleteState.select, F.data != PREVIOUS_MENU
+)
async def confirm_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
@@ -119,7 +144,9 @@ async def confirm_delete(
@message_exception_handler(log_error_text="Ошибка при удалении проекта из БД")
-@portfolio_router.callback_query(DeleteState.confirm, F.data != PREVIOUS_MENU)
+@portfolio_router.callback_query(
+ PortfolioDeleteState.confirm, F.data != PREVIOUS_MENU
+)
async def delete_portfolio_project(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
@@ -150,7 +177,7 @@ async def portfolio_project_to_update(
log_error_text="Ошибка при выборе данных для обновления"
)
@portfolio_router.callback_query(
- UpdateState.select,
+ PortfolioUpdateState.select,
and_f(
F.data != ADMIN_UPDATE_OPTIONS.get("name"),
F.data != ADMIN_UPDATE_OPTIONS.get("content"),
@@ -171,7 +198,7 @@ async def update_portfolio_project_choice(
log_error_text="Ошибка при обновлении имени объекта"
)
@portfolio_router.callback_query(
- UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
+ PortfolioUpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("name")
)
async def portfolio_name_update(
callback: CallbackQuery,
@@ -188,7 +215,7 @@ async def portfolio_name_update(
log_error_text="Ошибка при обновлении содержимого объекта"
)
@portfolio_router.callback_query(
- UpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
+ PortfolioUpdateState.select, F.data == ADMIN_UPDATE_OPTIONS.get("content")
)
async def about_url_update(callback: CallbackQuery, state: FSMContext):
"""Обновить содержимое объекта."""
@@ -199,7 +226,9 @@ async def about_url_update(callback: CallbackQuery, state: FSMContext):
@message_exception_handler(log_error_text="Ошибка при обновлении объекта в БД")
-@portfolio_router.message(or_f(UpdateState.name, UpdateState.url), F.text)
+@portfolio_router.message(
+ or_f(PortfolioUpdateState.name, PortfolioUpdateState.url), F.text
+)
async def update_about_info(
message: Message, state: FSMContext, session: AsyncSession
):
@@ -230,7 +259,7 @@ async def change_portfolio_url(
@message_exception_handler(
log_error_text="Ошибка при обновлении адреса ссылки основного портфолио в БД"
)
-@portfolio_router.message(UpdateState.portolio, F.text)
+@portfolio_router.message(PortfolioUpdateState.portfolio, F.text)
async def update_portfolio_button(
message: Message, state: FSMContext, session: AsyncSession
):
diff --git a/app/admin/handlers/admin_handlers/admin_product_handlers.py b/app/admin/handlers/admin_handlers/admin_product_handlers.py
index a63cb13..96601bd 100644
--- a/app/admin/handlers/admin_handlers/admin_product_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_product_handlers.py
@@ -27,6 +27,10 @@
logger = logging.getLogger(__name__)
+product_router = Router()
+product_router.message.filter(ChatTypeFilter(["private"]), IsManagerOrAdmin())
+product_router.callback_query.filter(IsManagerOrAdmin())
+
class ProductCreateState(StatesGroup):
select = State()
name = State()
@@ -48,9 +52,6 @@ class ProductDeleteState(StatesGroup):
confirm = State()
-product_router = Router()
-product_router.message.filter(ChatTypeFilter(["private"]), IsManagerOrAdmin())
-
PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("products")
product_create_manager = CreateManager(
diff --git a/app/admin/handlers/admin_handlers/admin_promotion.py b/app/admin/handlers/admin_handlers/admin_promotion.py
deleted file mode 100644
index 8098d4f..0000000
--- a/app/admin/handlers/admin_handlers/admin_promotion.py
+++ /dev/null
@@ -1,122 +0,0 @@
-from aiogram import Router, F
-from aiogram.types import CallbackQuery, Message
-from aiogram.fsm.context import FSMContext
-from aiogram.filters import or_f
-from aiogram.fsm.state import State, StatesGroup
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from admin.filters.filters import ChatTypeFilter, IsAdminOnly
-from admin.keyboards.keyboards import (
- get_inline_keyboard,
-)
-from admin.admin_settings import (
- MAIN_MENU_OPTIONS,
- MAIN_MENU_TEXT,
- SUPERUSER_PROMOTION_BUTTONS,
- SUPERUSER_PROMOTION_OPTIONS,
- SUPERUSER_SPECIAL_BUTTONS,
- SUPERUSER_SPECIAL_OPTIONS,
-)
-from models.models import User, RoleEnum
-from crud.user_crud import user_crud
-
-superuser_router = Router()
-superuser_router.message.filter(
- ChatTypeFilter(["private"]), IsAdminOnly()
-)
-
-PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("admin_special")
-
-
-class RoleState(StatesGroup):
- promote = State()
- demote = State()
-
-
-@superuser_router.callback_query(
- F.data == MAIN_MENU_OPTIONS.get("admin_special")
-)
-async def get_admin_special_options(callback: CallbackQuery):
- await callback.message.edit_text(
- "Дополнительная информация для администрации:",
- reply_markup=await get_inline_keyboard(
- SUPERUSER_SPECIAL_BUTTONS, previous_menu=MAIN_MENU_TEXT
- ),
- )
-
-
-@superuser_router.callback_query(
- F.data == SUPERUSER_SPECIAL_OPTIONS.get("promotion")
-)
-async def get_superuser_options(callback: CallbackQuery):
- """Перейти в меню управления персоналом."""
- await callback.message.edit_text(
- SUPERUSER_SPECIAL_OPTIONS.get("promotion"),
- reply_markup=await get_inline_keyboard(
- SUPERUSER_PROMOTION_BUTTONS,
- previous_menu=PREVIOUS_MENU,
- ),
- )
-
-
-@superuser_router.callback_query(
- F.data == SUPERUSER_PROMOTION_OPTIONS.get("manager_list")
-)
-async def get_manager_list(callback: CallbackQuery, session: AsyncSession):
- """Получить список менеджеров."""
- manager_list = await user_crud.get_manager_list(session)
- managers_tg_ids = [manager.tg_id + "\n\n" for manager in manager_list]
- managers = "".join(managers_tg_ids) if managers_tg_ids else "Список пуст!"
- await callback.message.edit_text(
- managers,
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
-
-
-@superuser_router.callback_query(
- or_f(
- F.data == SUPERUSER_PROMOTION_OPTIONS.get("promote"),
- F.data == SUPERUSER_PROMOTION_OPTIONS.get("demote"),
- )
-)
-async def get_user_id_for_action(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
- """Ввести телеграм id пользователя для смены роли."""
- if callback.data == SUPERUSER_PROMOTION_OPTIONS.get("promote"):
- await state.set_state(RoleState.promote)
- elif callback.data == SUPERUSER_PROMOTION_OPTIONS.get("demote"):
- await state.set_state(RoleState.demote)
- await callback.message.edit_text(
- "Введите id телеграм-пользователя:",
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
-
-
-@superuser_router.message(RoleState(), F.text.isnumeric())
-async def change_user_role(
- message: Message, state: FSMContext, session: AsyncSession
-):
- """
- Проверить наличие пользователя в базе.
- Проверить роль пользователя.
- Изменить роль пользователя
- """
- user: User = await user_crud.get_user_by_tg_id(message.text, session)
- if not user:
- message_text = "Такого пользователя в базе нет!"
- if user.role == RoleEnum.ADMIN:
- message_text = "Нельзя менять роль админа!"
- current_state = await state.get_state()
- if current_state == RoleState.promote.state:
- message_text = f"Пользователь {message.text} назначен менеджером!"
- await user_crud.promote_to_manager(user, session)
- elif current_state == RoleState.demote.state:
- message_text == (
- f"Позльователь {message.text} теперь просто пользователь!"
- )
- await user_crud.demote_to_user(user, session)
- await message.answer(
- message_text,
- reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
- )
diff --git a/app/admin/handlers/admin_handlers/admin_special.py b/app/admin/handlers/admin_handlers/admin_special_handlers.py
similarity index 93%
rename from app/admin/handlers/admin_handlers/admin_special.py
rename to app/admin/handlers/admin_handlers/admin_special_handlers.py
index c8e2bef..742e547 100644
--- a/app/admin/handlers/admin_handlers/admin_special.py
+++ b/app/admin/handlers/admin_handlers/admin_special_handlers.py
@@ -1,3 +1,5 @@
+import logging
+
from aiogram import Router, F
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
@@ -23,10 +25,13 @@
get_request,
)
+logger = logging.getLogger(__name__)
+
admin_special_router = Router()
admin_special_router.message.filter(
ChatTypeFilter(["private"]), IsManagerOrAdmin()
)
+admin_special_router.callback_query.filter(IsManagerOrAdmin())
class RequestState(StatesGroup):
@@ -138,10 +143,15 @@ async def close_request(
back_option = await get_state_name(current_state)
fsm_data = await state.get_data()
request_id = fsm_data.get("request_id")
- await close_case(request_id, session)
+ await close_case(callback.from_user.id, request_id, session)
await callback.message.edit_text(
"Заявка закрыта!",
reply_markup=await get_inline_keyboard(previous_menu=back_option),
)
except Exception as e:
await callback.message.answer(f"Произошла ошибка: {e}")
+
+
+@admin_special_router.callback_query(F.data == ADMIN_SPECIAL_OPTIONS.get("feedbacks"))
+async def get_feedbacks(callback: CallbackQuery, session: AsyncSession)
+ ...
diff --git a/app/admin/handlers/admin_handlers/superuser_handlers.py b/app/admin/handlers/admin_handlers/superuser_handlers.py
new file mode 100644
index 0000000..ed23afd
--- /dev/null
+++ b/app/admin/handlers/admin_handlers/superuser_handlers.py
@@ -0,0 +1,207 @@
+import logging
+
+from aiogram import Router, F
+from aiogram.types import CallbackQuery, Message
+from aiogram.fsm.context import FSMContext
+from aiogram.filters import or_f
+from aiogram.fsm.state import State, StatesGroup
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from admin.filters.filters import ChatTypeFilter, IsAdminOnly
+from admin.keyboards.keyboards import (
+ get_inline_keyboard,
+)
+from admin.admin_settings import (
+ DATETIME_FORMAT,
+ MAIN_MENU_OPTIONS,
+ MAIN_MENU_TEXT,
+ SUPERUSER_PROMOTION_BUTTONS,
+ SUPERUSER_PROMOTION_OPTIONS,
+ SUPERUSER_SPECIAL_BUTTONS,
+ SUPERUSER_SPECIAL_OPTIONS,
+)
+from models.models import User, RoleEnum
+from crud.request_to_manager import get_manager_stats
+from crud.user_crud import user_crud
+
+logger = logging.getLogger(__name__)
+
+superuser_router = Router()
+superuser_router.message.filter(ChatTypeFilter(["private"]), IsAdminOnly())
+superuser_router.callback_query.filter(IsAdminOnly())
+
+
+PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("admin_special")
+
+manager = State()
+
+
+class RoleState(StatesGroup):
+ promote = State()
+ demote = State()
+ name = State()
+
+
+async def check_user_tg_id_data(
+ tg_id: int, session: AsyncSession
+) -> tuple[User, str | None]:
+ """
+ Проверить данные пользователя и вернуть пользователя и соообщение,
+ если пользователь админ или такого пользователя нет в базе.
+ """
+ message_text = ""
+ user: User = await user_crud.get_user_by_tg_id(tg_id, session)
+ if not user:
+ message_text = "Такого пользователя в базе нет!"
+ elif user.role == RoleEnum.ADMIN:
+ message_text = "Нельзя менять роль админа!"
+ return user, message_text
+
+
+@superuser_router.callback_query(
+ F.data == MAIN_MENU_OPTIONS.get("admin_special")
+)
+async def get_admin_special_options(
+ callback: CallbackQuery, state: FSMContext
+):
+ """Меню суперпользователя."""
+ await state.clear()
+ await callback.message.edit_text(
+ "Дополнительная информация для администрации:",
+ reply_markup=await get_inline_keyboard(
+ SUPERUSER_SPECIAL_BUTTONS, previous_menu=MAIN_MENU_TEXT
+ ),
+ )
+
+
+@superuser_router.callback_query(
+ F.data == SUPERUSER_SPECIAL_OPTIONS.get("promotion")
+)
+async def get_superuser_options(callback: CallbackQuery):
+ """Перейти в меню управления персоналом."""
+ await callback.message.edit_text(
+ SUPERUSER_SPECIAL_OPTIONS.get("promotion"),
+ reply_markup=await get_inline_keyboard(
+ SUPERUSER_PROMOTION_BUTTONS,
+ previous_menu=PREVIOUS_MENU,
+ ),
+ )
+
+
+@superuser_router.callback_query(
+ F.data == SUPERUSER_PROMOTION_OPTIONS.get("manager_list")
+)
+async def get_manager_list(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Получить список менеджеров."""
+ manager_list = await user_crud.get_manager_list(session)
+ managers_tg_ids = [manager.tg_id for manager in manager_list]
+ managers_names = [manager.name for manager in manager_list]
+ await callback.message.edit_text(
+ SUPERUSER_PROMOTION_OPTIONS.get("manager_list"),
+ reply_markup=await get_inline_keyboard(
+ options=managers_names,
+ callback=managers_tg_ids,
+ previous_menu=PREVIOUS_MENU,
+ ),
+ )
+ await state.set_state(manager)
+
+
+@superuser_router.callback_query(manager, F.data.isnumeric())
+async def get_manager(callback: CallbackQuery, session: AsyncSession):
+ """Получить информацио о менеджере."""
+ manager = await user_crud.get_user_by_tg_id(callback.data, session)
+ cases_count, last_case_closed = await get_manager_stats(
+ callback.data, session
+ )
+ last_case_message = (
+ (
+ f"{last_case_closed.id} от "
+ f"{last_case_closed.shipping_date_close.strftime(DATETIME_FORMAT)}"
+ )
+ if last_case_closed
+ else "закртых заявок пока нет."
+ )
+ message_text = (
+ f"Менеджер {manager.name}\n\n "
+ f"Телеграм id: {manager.tg_id} \n\n"
+ f"Количество закрытых заявок: {cases_count} \n\n"
+ f"Номер последней закрытой заявки: {last_case_message}"
+ )
+ await callback.message.edit_text(
+ message_text,
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+
+
+@superuser_router.callback_query(
+ or_f(
+ F.data == SUPERUSER_PROMOTION_OPTIONS.get("promote"),
+ F.data == SUPERUSER_PROMOTION_OPTIONS.get("demote"),
+ )
+)
+async def get_user_id_for_action(callback: CallbackQuery, state: FSMContext):
+ """Ввести телеграм id пользователя для смены роли."""
+ if callback.data == SUPERUSER_PROMOTION_OPTIONS.get("promote"):
+ await state.set_state(RoleState.promote)
+ elif callback.data == SUPERUSER_PROMOTION_OPTIONS.get("demote"):
+ await state.set_state(RoleState.demote)
+ await callback.message.edit_text(
+ "Введите id телеграм-пользователя:",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+
+
+@superuser_router.message(RoleState.demote, F.text.isnumeric())
+async def demote_to_user(message: Message, session: AsyncSession):
+ """Проверить пользователя и присвоить ему роль USER."""
+ user, error_text = await check_user_tg_id_data(message.text, session)
+ if error_text:
+ message_text = error_text
+ else:
+ message_text = (
+ f"Позльователь {message.text} теперь просто пользователь!"
+ )
+ await user_crud.demote_to_user(user, session)
+ await message.answer(
+ message_text,
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+
+
+@superuser_router.message(RoleState.promote, F.text.isnumeric())
+async def add_name_to_manager(message: Message, state: FSMContext):
+ """Добавить имя для менеджера."""
+ await state.update_data(tg_id=message.text)
+ await state.set_state(RoleState.name)
+ await message.answer(
+ "Введите имя для менеджера",
+ reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
+ )
+
+
+@superuser_router.message(RoleState.name, F.text)
+async def check_user_and_make_him_manager(
+ message: Message, state: FSMContext, session: AsyncSession
+):
+ """Проверить пользователя и присвоить ему роль менеджера."""
+ fsm_data = await state.get_data()
+ user_tg_id = fsm_data.get("tg_id")
+ user, error_message = await check_user_tg_id_data(user_tg_id, session)
+ if error_message:
+ await message.answer(
+ error_message,
+ reply_markup=await get_inline_keyboard(
+ previous_menu=PREVIOUS_MENU
+ ),
+ )
+ else:
+ await user_crud.promote_to_manager(user, message.text, session)
+ await message.answer(
+ "Менеджер добален!",
+ reply_markup=await get_inline_keyboard(
+ previous_menu=PREVIOUS_MENU
+ ),
+ )
diff --git a/app/admin/handlers/user.py b/app/admin/handlers/user.py
index 438e7fc..e0100a6 100644
--- a/app/admin/handlers/user.py
+++ b/app/admin/handlers/user.py
@@ -1,23 +1,17 @@
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
-from aiogram.types import CallbackQuery, Message
-from aiogram.filters import CommandStart, or_f
+from aiogram.types import CallbackQuery
+from aiogram.filters import or_f
from aiogram.fsm.state import State, StatesGroup
from sqlalchemy.ext.asyncio import AsyncSession
-from crud.category_product import category_product_crud
-from admin.filters.filters import ChatTypeFilter
from admin.admin_settings import (
- ADMIN_BASE_BUTTONS,
PORTFOLIO_DEFAULT_DATA,
PORTFOLIO_MENU_TEXT,
PORTFOLIO_OTHER_PROJECTS_TEXT,
PRODUCT_LIST_TEXT,
- admin_list,
BASE_BUTTONS,
- BASE_KEYBOARD_BUTTONS,
MAIN_MENU_OPTIONS,
- GREETINGS,
MAIN_MENU_BUTTONS,
COMPANY_ABOUT,
PORTFOLIO_BUTTONS,
@@ -26,12 +20,12 @@
SUPPORT_MENU_TEXT,
SUPPROT_MENU_BUTTONS,
)
+from admin.filters.filters import ChatTypeFilter
from admin.keyboards.keyboards import (
get_inline_keyboard,
- get_reply_keyboard,
get_delete_message_keyboard,
)
-
+from crud.category_product import category_product_crud
from crud.info_crud import info_crud
from crud.about_crud import company_info_crud
from crud.portfolio_projects_crud import portfolio_crud
@@ -39,7 +33,9 @@
user_router = Router()
-user_router.message.filter(ChatTypeFilter(["private"]))
+user_router.message.filter(
+ ChatTypeFilter(["private"]),
+)
class QuestionAnswer(StatesGroup):
@@ -56,39 +52,6 @@ async def delete_message(callback: CallbackQuery):
await callback.message.delete()
-@user_router.message(CommandStart())
-async def start_cmd(message: Message):
- """Получить основную экранную клавиатуру."""
-
- if message.from_user.id in admin_list:
- await message.answer(
- GREETINGS,
- reply_markup=await get_reply_keyboard(
- ADMIN_BASE_BUTTONS, size=(1, 2)
- ),
- )
- else:
- await message.answer(
- GREETINGS,
- reply_markup=await get_reply_keyboard(
- BASE_KEYBOARD_BUTTONS, size=(1, 2)
- ),
- )
-
-
-@user_router.message(F.text == BASE_BUTTONS.get("main_menu"))
-async def main_menu(message: Message, state: FSMContext):
- """Получить основное меню бота после команды с экранной клавиатуры."""
-
- await message.answer(
- BASE_BUTTONS.get("main_menu"),
- reply_markup=await get_inline_keyboard(MAIN_MENU_BUTTONS),
- )
-
- await message.delete()
- await state.clear()
-
-
@user_router.callback_query(F.data == BASE_BUTTONS.get("main_menu"))
async def main_menu_callback(callback: CallbackQuery, state: FSMContext):
"""Получить основное меню бота через callback_query."""
diff --git a/app/admin/handlers/validators.py b/app/admin/handlers/validators.py
index 0294f1a..09b4d70 100644
--- a/app/admin/handlers/validators.py
+++ b/app/admin/handlers/validators.py
@@ -7,3 +7,14 @@ def phone_number_validator(phone_number: int) -> bool:
"""Вовзращает True если номер телефона соответствует шаблону."""
phone_number = re.sub(r"[-()./ ]", "", phone_number)
return re.match(PHONE_NUMBER_REGEX, phone_number) is not None
+
+
+def validate_url(url: str):
+ """Валидация ссылки."""
+ regex = re.compile(
+ r'^(https://)'
+ r'([A-Za-z0-9.-]+)' # Домен
+ r'(:\d+)?' # Порт (необязательный)
+ r'(/.*)?$' # Путь (необязательный)
+ )
+ return re.match(regex, url) is not None
diff --git a/app/alembic/versions/07c781d887d8_fix_delete_some_field_in_feedback.py b/app/alembic/versions/07c781d887d8_fix_delete_some_field_in_feedback.py
deleted file mode 100644
index 61318f5..0000000
--- a/app/alembic/versions/07c781d887d8_fix_delete_some_field_in_feedback.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""fix delete some field in feedback
-
-Revision ID: 07c781d887d8
-Revises: c493d913699d
-Create Date: 2024-10-14 13:40:54.256167
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision: str = '07c781d887d8'
-down_revision: Union[str, None] = 'c493d913699d'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- pass
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- pass
- # ### end Alembic commands ###
diff --git a/app/alembic/versions/c4b40ca2f35e_fix_delete_some_field_in_feedback.py b/app/alembic/versions/53d69ac71b30_add_manager_and_user_connection.py
similarity index 91%
rename from app/alembic/versions/c4b40ca2f35e_fix_delete_some_field_in_feedback.py
rename to app/alembic/versions/53d69ac71b30_add_manager_and_user_connection.py
index d055eae..782ac41 100644
--- a/app/alembic/versions/c4b40ca2f35e_fix_delete_some_field_in_feedback.py
+++ b/app/alembic/versions/53d69ac71b30_add_manager_and_user_connection.py
@@ -1,8 +1,8 @@
-"""fix delete some field in feedback
+"""add manager and user connection
-Revision ID: c4b40ca2f35e
+Revision ID: 53d69ac71b30
Revises:
-Create Date: 2024-10-14 11:16:32.374280
+Create Date: 2024-10-15 09:50:55.008906
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
-revision: str = 'c4b40ca2f35e'
+revision: str = '53d69ac71b30'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -26,16 +26,6 @@ def upgrade() -> None:
sa.Column('id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
- op.create_table('contactmanager',
- sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
- sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
- sa.Column('need_support', sa.BOOLEAN(), nullable=False),
- sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
- sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
- sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), nullable=True),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.PrimaryKeyConstraint('id')
- )
op.create_table('info',
sa.Column('question_type', postgresql.ENUM('GENERAL_QUESTIONS', 'PROBLEMS_WITH_PRODUCTS', name='question_enum'), nullable=False),
sa.Column('question', sa.TEXT(), nullable=False),
@@ -58,6 +48,7 @@ def upgrade() -> None:
)
op.create_table('user',
sa.Column('tg_id', sa.BIGINT(), nullable=False),
+ sa.Column('name', sa.VARCHAR(length=32), nullable=False),
sa.Column('role', postgresql.ENUM('USER', 'ADMIN', 'MANAGER', name='role_enum'), nullable=False),
sa.Column('join_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
@@ -75,10 +66,23 @@ def upgrade() -> None:
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_categorytype_product_id'), 'categorytype', ['product_id'], unique=False)
+ op.create_table('contactmanager',
+ sa.Column('first_name', sa.VARCHAR(length=32), nullable=False),
+ sa.Column('phone_number', sa.VARCHAR(length=25), nullable=False),
+ sa.Column('need_support', sa.BOOLEAN(), nullable=False),
+ sa.Column('need_contact_with_manager', sa.BOOLEAN(), nullable=False),
+ sa.Column('shipping_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
+ sa.Column('shipping_date_close', postgresql.TIMESTAMP(timezone=True), nullable=True),
+ sa.Column('manager_id', sa.BIGINT(), nullable=True),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['manager_id'], ['user.tg_id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
op.create_table('feedback',
sa.Column('user', sa.Integer(), nullable=False),
sa.Column('feedback_text', sa.TEXT(), nullable=False),
sa.Column('feedback_date', postgresql.TIMESTAMP(), nullable=False),
+ sa.Column('rating', sa.INTEGER(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['user'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
@@ -89,12 +93,12 @@ def upgrade() -> None:
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('feedback')
+ op.drop_table('contactmanager')
op.drop_index(op.f('ix_categorytype_product_id'), table_name='categorytype')
op.drop_table('categorytype')
op.drop_table('user')
op.drop_table('productcategory')
op.drop_table('informationaboutcompany')
op.drop_table('info')
- op.drop_table('contactmanager')
op.drop_table('checkcompanyportfolio')
# ### end Alembic commands ###
diff --git a/app/alembic/versions/c493d913699d_fix_delete_some_field_in_feedback.py b/app/alembic/versions/c493d913699d_fix_delete_some_field_in_feedback.py
deleted file mode 100644
index 26af740..0000000
--- a/app/alembic/versions/c493d913699d_fix_delete_some_field_in_feedback.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""fix delete some field in feedback
-
-Revision ID: c493d913699d
-Revises: c4b40ca2f35e
-Create Date: 2024-10-14 13:39:49.912216
-
-"""
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision: str = 'c493d913699d'
-down_revision: Union[str, None] = 'c4b40ca2f35e'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- pass
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- pass
- # ### end Alembic commands ###
diff --git a/app/core/db.py b/app/core/db.py
index c0f8802..1600b87 100644
--- a/app/core/db.py
+++ b/app/core/db.py
@@ -17,5 +17,5 @@ def __tablename__(cls):
Base = declarative_base(cls=PreBase)
-engine = create_async_engine(settings.database_url) # , echo=True
+engine = create_async_engine(settings.database_url)
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession)
diff --git a/app/crud/about_crud.py b/app/crud/about_crud.py
index da6a9f8..e21ab1b 100644
--- a/app/crud/about_crud.py
+++ b/app/crud/about_crud.py
@@ -18,12 +18,5 @@ async def get_by_about_name(
)
return db_obj.scalars().first()
- async def get_multi(self, session: AsyncSession):
- """Получить список всех объектов модели из БД."""
- db_objs = await session.execute(
- select(self.model).where(self.model.id != 1)
- )
- return db_objs.scalars().all()
-
company_info_crud = AboutCRUD(InformationAboutCompany)
diff --git a/app/crud/feedback_crud.py b/app/crud/feedback_crud.py
index 8ac5482..51d68b3 100644
--- a/app/crud/feedback_crud.py
+++ b/app/crud/feedback_crud.py
@@ -1,57 +1,27 @@
-# from sqlalchemy import select, desc
-# from sqlalchemy.ext.asyncio import AsyncSession
-# from sqlalchemy.orm import joinedload
+from sqlalchemy import select, desc
+from sqlalchemy.ext.asyncio import AsyncSession
-# from .base_crud import CRUDBase
-# from models.models import Feedback
+from .base_crud import CRUDBase
+from models.models import Feedback
-# class FeedbackCRUD(CRUDBase):
-# async def get_new_feedbacks(
-# self,
-# session: AsyncSession,
-# ):
-# """Получить список пользователей ожидающих обратного звонка."""
-# users_to_callback = await session.execute(
-# select(self.model)
-# .where(self.model.unread)
-# .order_by(desc(self.model.feedback_date))
-# )
-# return users_to_callback.scalars().all()
+class FeedbackCRUD(CRUDBase):
+ async def get_multi(self, session: AsyncSession):
+ """Получить список всех объектов модели из БД."""
+ db_objs = await session.execute(
+ select(self.model).order_by(desc(self.model.feedback_date))
+ )
+ return db_objs.scalars().all()
-# async def mark_as_read(self, feedback: Feedback, session: AsyncSession):
-# """Открыть заявку на обратный звонок."""
-# setattr(feedback, "unread", False)
-# session.add(feedback)
-# await session.commit()
-# await session.refresh(feedback)
-# return feedback
+ async def bulk_create(
+ self,
+ objs_in: list,
+ session: AsyncSession,
+ ):
+ db_objs = [self.model(**obj) for obj in objs_in]
+ session.add_all(db_objs)
+ await session.commit()
+ return db_objs
-# async def get_multi(self, session: AsyncSession):
-# """Получить список всех объектов модели из БД."""
-# db_objs = await session.execute(
-# select(self.model).order_by(desc(self.model.feedback_date))
-# )
-# return db_objs.scalars().all()
-# async def bulk_create(
-# self,
-# objs_in: list,
-# session: AsyncSession,
-# ):
-# db_objs = [self.model(**obj) for obj in objs_in]
-# session.add_all(db_objs)
-# await session.commit()
-# return db_objs
-
-# async def get(self, feedback_id: int, session: AsyncSession):
-# """Получить объект отзыва вместе с его автором"""
-# feedback_with_user = await session.execute(
-# select(Feedback)
-# .options(joinedload(Feedback.author))
-# .where(Feedback.id == feedback_id)
-# )
-# return feedback_with_user.scalar_one()
-
-
-# feedback_crud = FeedbackCRUD(Feedback)
+feedback_crud = FeedbackCRUD(Feedback)
diff --git a/app/crud/request_to_manager.py b/app/crud/request_to_manager.py
index 38274a7..5d48b81 100644
--- a/app/crud/request_to_manager.py
+++ b/app/crud/request_to_manager.py
@@ -1,6 +1,6 @@
from datetime import datetime
-from sqlalchemy import select, and_
+from sqlalchemy import select, and_, desc, func
from sqlalchemy.ext.asyncio import AsyncSession
from models.models import ContactManager
@@ -58,7 +58,9 @@ async def get_all_manager_requests(session: AsyncSession):
return support_requests.scalars().all()
-async def close_case(request_id: int, session: AsyncSession):
+async def close_case(
+ manager_id: int, request_id: int, session: AsyncSession
+) -> tuple:
"""Закрыть заявку."""
case_to_close = await get_request(request_id, session)
if case_to_close.need_contact_with_manager:
@@ -66,7 +68,29 @@ async def close_case(request_id: int, session: AsyncSession):
elif case_to_close.need_support:
setattr(case_to_close, "need_support", False)
setattr(case_to_close, "shipping_date_close", datetime.now())
+ setattr(case_to_close, "manager_id", manager_id)
session.add(case_to_close)
await session.commit()
await session.refresh(case_to_close)
return case_to_close
+
+
+async def get_manager_stats(manager_id: int, session: AsyncSession):
+ """Получить статистику по работае менеджера."""
+ closed_requests_count = await session.execute(
+ select(func.count())
+ .select_from(ContactManager)
+ .where(ContactManager.manager_id == manager_id)
+ )
+
+ last_closed_requests = await session.execute(
+ select(ContactManager)
+ .where(ContactManager.manager_id == manager_id)
+ .order_by(desc(ContactManager.shipping_date_close))
+ .limit(1)
+ )
+
+ return (
+ closed_requests_count.scalar_one(),
+ last_closed_requests.scalar_one_or_none(),
+ )
diff --git a/app/crud/user_crud.py b/app/crud/user_crud.py
index d09cb0e..6bca221 100644
--- a/app/crud/user_crud.py
+++ b/app/crud/user_crud.py
@@ -48,9 +48,12 @@ async def get_manager_list(self, session: AsyncSession) -> list[User]:
)
return manager_list.scalars().all()
- async def promote_to_manager(self, user: User, session: AsyncSession):
+ async def promote_to_manager(
+ self, user: User, name: str, session: AsyncSession
+ ):
"""Назначить пользователя менеджером."""
setattr(user, "role", RoleEnum.MANAGER)
+ setattr(user, "name", name)
session.add(user)
await session.commit()
await session.refresh(user)
diff --git a/app/models/models.py b/app/models/models.py
index 773471e..c774b85 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -28,6 +28,10 @@ class User(Base):
pgsql_types.BIGINT, nullable=False, unique=True
)
+ name: Mapped[str] = mapped_column(
+ pgsql_types.VARCHAR(32), default="Аноним"
+ )
+
role: Mapped[RoleEnum] = mapped_column(
pgsql_types.ENUM(RoleEnum, name="role_enum", create_type=False),
default=RoleEnum.USER,
@@ -38,6 +42,9 @@ class User(Base):
server_default=func.now(),
nullable=False,
)
+ closed_requests: Mapped[list["ContactManager"]] = relationship(
+ "ContactManager", back_populates="manager"
+ )
class ProductCategory(Base):
@@ -136,6 +143,14 @@ class ContactManager(Base):
pgsql_types.TIMESTAMP(timezone=True), nullable=True
)
+ manager_id: Mapped[int] = mapped_column(
+ pgsql_types.BIGINT, ForeignKey('user.tg_id'), nullable=True
+ )
+
+ manager: Mapped[User] = relationship(
+ "User", back_populates="closed_requests"
+ )
+
class Feedback(Base):
"""БД модель для отзывов."""
diff --git a/app/test.py b/app/test.py
new file mode 100644
index 0000000..7f8f998
--- /dev/null
+++ b/app/test.py
@@ -0,0 +1,23 @@
+import asyncio
+from datetime import datetime
+
+from core.db import AsyncSessionLocal
+from crud.feedback_crud import feedback_crud
+
+
+
+feedbacks = {
+ "user": 1,
+ "feedback_text": "Типа отзыв",
+ "feedback_date": datetime.now(),
+ "rating": 5
+
+}
+
+async def add_to_db():
+ async with AsyncSessionLocal() as session:
+ await feedback_crud.create(feedbacks, session)
+
+
+if __name__ == "__main__":
+ asyncio.run(add_to_db())
\ No newline at end of file
From 5289f36f7f6af692b768b76b7da29436ea166a9b Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Tue, 15 Oct 2024 14:32:13 +0300
Subject: [PATCH 47/75] autodeploy test
---
README.md | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/README.md b/README.md
index 0df89bc..63ed083 100644
--- a/README.md
+++ b/README.md
@@ -6,16 +6,6 @@
### Директории:
-- app/bot - основная логика работы бота
-- app/admin - админка бота
-- app/exceptions - вызовы возможных ошибок
-- pyproject - файл виртуального окружения
-- poetry - файл с зависимостями
-
-
-### Как работаем:
-- работает каждый в своей ветке, лучше назвать по именам разработчиков, чтоб не путаться, ветки наследуем от dev
-- пушим через pull request в dev
### Ссылка на бота:
From 828ba4a95bb374ae576c68daabf01098874ae36c Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Tue, 15 Oct 2024 14:38:34 +0300
Subject: [PATCH 48/75] autodeploy test
---
.github/workflows/main.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 1f67bb2..9a6766d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -3,6 +3,7 @@ on:
push:
branches:
- master
+ - dev
pull_request:
branches:
- dev
From 066fc1a5bb53b8a66b010556d5c54dfd15e844b9 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Tue, 15 Oct 2024 14:59:00 +0300
Subject: [PATCH 49/75] change server workdir
---
.github/workflows/main.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9a6766d..0a1a081 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -3,7 +3,7 @@ on:
push:
branches:
- master
- - dev
+ - dev # закомментить перед выходом в прод
pull_request:
branches:
- dev
@@ -37,7 +37,7 @@ jobs:
key: ${{ secrets.SSH_KEY }}
passphrase: ${{ secrets.SSH_PASSPHRASE }}
source: "docker-compose.yml"
- target: "scid3"
+ target: "scid_bot"
- name: Executing remote ssh commands to deploy
uses: appleboy/ssh-action@master
with:
From 510c34d1e5c2dd7cb44fb090558a563d263924e1 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Tue, 15 Oct 2024 15:05:05 +0300
Subject: [PATCH 50/75] change server workdir
---
.github/workflows/main.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 0a1a081..5eb27cd 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -46,7 +46,7 @@ jobs:
key: ${{ secrets.SSH_KEY }}
passphrase: ${{ secrets.SSH_PASSPHRASE }}
script: |
- cd scid3
+ cd scid_bot
sudo docker compose -f docker-compose.yml pull
sudo docker compose -f docker-compose.yml down
sudo docker compose -f docker-compose.yml up -d
From 42ccb2557f1ab73fb1e41de43415c5e17ad62518 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Tue, 15 Oct 2024 15:53:03 +0300
Subject: [PATCH 51/75] add make, feedback done
---
.env.example | 4 -
app/admin/filters/filters.py | 1 +
app/admin/handlers/admin_handlers/admin.py | 4 +-
.../admin_about_company_handlers.py | 33 +--
.../admin_handlers/admin_category_handlers.py | 21 +-
.../admin_handlers/admin_info_handlers.py | 29 +--
.../admin_portfolio_handlers.py | 63 ++---
.../admin_handlers/admin_product_handlers.py | 37 +--
app/admin/handlers/user.py | 24 +-
app/admin/keyboards/keyboards.py | 10 +-
app/alembic/env.py | 4 +-
.../versions/fd795bdbba90_update_feedback.py | 24 +-
app/bot/bot_const.py | 85 +++---
app/bot/callbacks.py | 178 +++++++------
app/bot/exceptions.py | 4 +-
app/bot/fsm_contexts/__init__.py | 0
.../{ => fsm_contexts}/feedback_context.py | 50 ++--
.../manager_context.py} | 243 +++++++++---------
app/bot/handlers.py | 24 +-
app/bot/keyborads.py | 91 +++----
app/bot/smtp.py | 9 +-
app/bot/validators.py | 6 +-
app/const.py | 8 +-
app/core/bot_setup.py | 2 +-
app/core/db.py | 8 +-
app/core/init_db.py | 4 +-
app/core/settings.py | 3 +-
app/crud/about_crud.py | 5 +-
app/crud/category_product.py | 8 +-
app/crud/feedback.py | 5 +-
app/crud/projects.py | 16 +-
app/crud/questions.py | 10 +-
app/crud/request_to_manager.py | 6 +-
app/crud/users.py | 12 +-
app/helpers.py | 15 +-
app/loggers/log.py | 46 +++-
app/main.py | 8 +-
app/models/models.py | 26 +-
app/scripts_for_db.py | 20 +-
makefile | 30 +++
poetry.lock | 221 +++++++++++++++-
pyproject.toml | 2 +
42 files changed, 784 insertions(+), 615 deletions(-)
create mode 100644 app/bot/fsm_contexts/__init__.py
rename app/bot/{ => fsm_contexts}/feedback_context.py (54%)
rename app/bot/{fsm_context.py => fsm_contexts/manager_context.py} (60%)
create mode 100644 makefile
diff --git a/.env.example b/.env.example
index eb91225..376acf1 100644
--- a/.env.example
+++ b/.env.example
@@ -1,10 +1,6 @@
-DATABASE_URL=<'DATABASE URL'>
TELEGRAM_TOKEN=<'TELEGRAM TOKEN'>
TELEGRAM_CHAT_IDS=<'TELEGRAM CHAT IDS'>
-BOT_TOKEN=<'BOT TOKEN'>
-TELEGRAM_CHAT_IDS=<'TELEGRAM CHAT IDS'>
-
DATABASE_URL=<'DATABASE URL'>
POSTGRES_USER=<'DB OWNER'>
POSTGRES_PASSWORD=<'PASSWORD'>
diff --git a/app/admin/filters/filters.py b/app/admin/filters/filters.py
index 3b99874..89c7b7d 100644
--- a/app/admin/filters/filters.py
+++ b/app/admin/filters/filters.py
@@ -1,5 +1,6 @@
from aiogram.filters import Filter
from aiogram import Bot, types
+
# from settings import admin_list
from const import admin_list
diff --git a/app/admin/handlers/admin_handlers/admin.py b/app/admin/handlers/admin_handlers/admin.py
index dafa956..0e04305 100644
--- a/app/admin/handlers/admin_handlers/admin.py
+++ b/app/admin/handlers/admin_handlers/admin.py
@@ -182,9 +182,7 @@ async def user_callback_request_data(
@admin_main_router.callback_query(UserState.close_case, F.data)
-async def close_case(
- callback: CallbackQuery, state: FSMContext, session: AsyncSession
-):
+async def close_case(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
"""Закрыть заявку на обратный звонок."""
user = await user_crud.get(callback.data, session)
diff --git a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
index 1b8fd16..ae48200 100644
--- a/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_about_company_handlers.py
@@ -12,6 +12,7 @@
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
+
# from settings import MAIN_MENU_OPTIONS
MAIN_MENU_OPTIONS = {
@@ -59,9 +60,7 @@ async def add_info_name(message: Message, state: FSMContext):
@about_router.message(AddAboutInfo.url, F.text)
-async def add_about_data(
- message: Message, state: FSMContext, session: AsyncSession
-):
+async def add_about_data(message: Message, state: FSMContext, session: AsyncSession):
await state.update_data(url=message.text)
data = await state.get_data()
await company_info_crud.create(data, session)
@@ -80,9 +79,7 @@ async def about_info_to_delete(
info_list = [info.name for info in about_data]
await callback.message.answer(
"Какую информацию вы хотите удалить?",
- reply_markup=await get_inline_keyboard(
- info_list, previous_menu=PREVIOUS_MENU
- ),
+ reply_markup=await get_inline_keyboard(info_list, previous_menu=PREVIOUS_MENU),
)
await state.set_state(DeleteAboutInfo.name)
@@ -91,9 +88,7 @@ async def about_info_to_delete(
async def confirm_delete_info(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- about_data = await company_info_crud.get_by_about_name(
- callback.data, session
- )
+ about_data = await company_info_crud.get_by_about_name(callback.data, session)
await callback.message.edit_text(
f"Вы уверены, что хотите удалить эту информацию?\n\n {about_data.name}",
reply_markup=await get_inline_confirmation_keyboard(
@@ -107,9 +102,7 @@ async def confirm_delete_info(
async def delete_about_info(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- about_data = await company_info_crud.get_by_about_name(
- callback.data, session
- )
+ about_data = await company_info_crud.get_by_about_name(callback.data, session)
await company_info_crud.remove(about_data, session)
await callback.message.edit_text(
"Информация удалена!",
@@ -126,9 +119,7 @@ async def about_info_to_update(
info_list = [info.name for info in about_data]
await callback.message.edit_text(
"Какую информацию вы хотите отредактировать?",
- reply_markup=await get_inline_keyboard(
- info_list, previous_menu=PREVIOUS_MENU
- ),
+ reply_markup=await get_inline_keyboard(info_list, previous_menu=PREVIOUS_MENU),
)
await state.set_state(UpdateAboutInfo.select)
@@ -151,9 +142,7 @@ async def update_info_choise(callback: CallbackQuery, state: FSMContext):
)
-@about_router.callback_query(
- UpdateAboutInfo.select, F.data == "Название ссылки"
-)
+@about_router.callback_query(UpdateAboutInfo.select, F.data == "Название ссылки")
async def about_name_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
@@ -171,9 +160,7 @@ async def about_url_update(
):
about_name_data = await state.get_data()
about_name_text = about_name_data.get("select")
- about_info = await company_info_crud.get_by_about_name(
- about_name_text, session
- )
+ about_info = await company_info_crud.get_by_about_name(about_name_text, session)
await callback.message.answer(
f"Сейчас у ссылки такой адрес:\n\n {about_info.url}\n\n Введите новое название"
)
@@ -181,9 +168,7 @@ async def about_url_update(
@about_router.message(or_f(UpdateAboutInfo.name, UpdateAboutInfo.url), F.text)
-async def update_about_info(
- message: Message, state: FSMContext, session: AsyncSession
-):
+async def update_about_info(message: Message, state: FSMContext, session: AsyncSession):
current_state = await state.get_state()
old_data = await state.get_data()
old_about_data = await company_info_crud.get_by_about_name(
diff --git a/app/admin/handlers/admin_handlers/admin_category_handlers.py b/app/admin/handlers/admin_handlers/admin_category_handlers.py
index 985264e..c4c8c8d 100644
--- a/app/admin/handlers/admin_handlers/admin_category_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_category_handlers.py
@@ -14,6 +14,7 @@
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
+
# from settings import MAIN_MENU_OPTIONS, admin_list
from const import admin_list
@@ -163,9 +164,7 @@ async def add_product_category_data(
)
-@category_router.message(
- or_f(AddCategory.media, UpdateCategory.media), F.photo
-)
+@category_router.message(or_f(AddCategory.media, UpdateCategory.media), F.photo)
async def add_media_description(message: Message, state: FSMContext):
"""Добавить описание к картинке."""
await state.update_data(media=message.photo[-1].file_id)
@@ -204,9 +203,7 @@ async def product_to_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Выбор продукта на удаление."""
- categories = [
- category.name for category in await get_category_list(state, session)
- ]
+ categories = [category.name for category in await get_category_list(state, session)]
await callback.message.edit_text(
"Какой проект вы хотите удалить?",
reply_markup=await get_inline_keyboard(
@@ -250,9 +247,7 @@ async def product_to_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Выбор продукта для редактирования."""
- categories = [
- category.name for category in await get_category_list(state, session)
- ]
+ categories = [category.name for category in await get_category_list(state, session)]
await callback.message.edit_text(
"Какую услугу вы хотите отредактировать?",
reply_markup=await get_inline_keyboard(
@@ -266,9 +261,7 @@ async def product_to_update(
UpdateCategory.select,
and_f(F.data != "Название", F.data != "Содержание"),
)
-async def update_portfolio_project_choise(
- callback: CallbackQuery, state: FSMContext
-):
+async def update_portfolio_project_choise(callback: CallbackQuery, state: FSMContext):
"""Выбор поля для редактирования."""
await state.update_data(select=callback.data)
await callback.message.edit_text(
@@ -332,9 +325,7 @@ async def about_url_update(
F.photo,
),
)
-async def update_about_info(
- message: Message, state: FSMContext, session: AsyncSession
-):
+async def update_about_info(message: Message, state: FSMContext, session: AsyncSession):
"""Внести изменения продукта в БД."""
current_state = await state.get_state()
old_data = await state.get_data()
diff --git a/app/admin/handlers/admin_handlers/admin_info_handlers.py b/app/admin/handlers/admin_handlers/admin_info_handlers.py
index 06e2dde..e78332a 100644
--- a/app/admin/handlers/admin_handlers/admin_info_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_info_handlers.py
@@ -11,6 +11,7 @@
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
+
# from settings import SUPPORT_OPTIONS
from sqlalchemy.ext.asyncio import AsyncSession
from aiogram.fsm.state import State, StatesGroup
@@ -67,9 +68,7 @@ async def get_question_list(question_type: str, session: AsyncSession):
async def add_question(callback: CallbackQuery, state: FSMContext):
current_state = await state.get_state()
await state.set_state(AddQuestion.question_type)
- await state.update_data(
- question_type=await set_question_type(current_state)
- )
+ await state.update_data(question_type=await set_question_type(current_state))
await callback.message.answer("Введите текст нового вопрос")
await state.set_state(AddQuestion.question)
@@ -93,9 +92,7 @@ async def add_question_answer(
await info_crud.create(data, session=session)
await message.answer(
"Вопрос добавлен!",
- reply_markup=await get_inline_keyboard(
- previous_menu=data.get("question_type")
- ),
+ reply_markup=await get_inline_keyboard(previous_menu=data.get("question_type")),
)
await state.clear()
@@ -108,9 +105,7 @@ async def question_to_delete(
):
current_state = await state.get_state()
await state.set_state(DeleteQuestion.question_type)
- await state.update_data(
- question_type=await set_question_type(current_state)
- )
+ await state.update_data(question_type=await set_question_type(current_state))
question_type = (await state.get_data()).get("question_type")
question_list = await get_question_list(question_type, session)
await callback.message.edit_text(
@@ -145,9 +140,7 @@ async def delete_question(
await info_crud.remove(question, session)
await callback.message.edit_text(
"Вопрос удален!",
- reply_markup=await get_inline_keyboard(
- previous_menu=question.question_type
- ),
+ reply_markup=await get_inline_keyboard(previous_menu=question.question_type),
)
@@ -160,9 +153,7 @@ async def update_question(
):
current_state = await state.get_state()
await state.set_state(UpdateQuestion.question_type)
- await state.update_data(
- question_type=await set_question_type(current_state)
- )
+ await state.update_data(question_type=await set_question_type(current_state))
question_type = (await state.get_data()).get("question_type")
question_list = await get_question_list(question_type, session)
await callback.message.edit_text(
@@ -208,17 +199,13 @@ async def update_question_answer(
await state.set_state(UpdateQuestion.answer)
-@info_router.message(
- or_f(UpdateQuestion.confirm, UpdateQuestion.answer), F.text
-)
+@info_router.message(or_f(UpdateQuestion.confirm, UpdateQuestion.answer), F.text)
async def update_question_data(
message: Message, state: FSMContext, session: AsyncSession
):
current_state = await state.get_state()
old_data = await state.get_data()
- question = await info_crud.get_by_question_text(
- old_data.get("question"), session
- )
+ question = await info_crud.get_by_question_text(old_data.get("question"), session)
if current_state == UpdateQuestion.confirm:
await state.update_data(question=message.text)
diff --git a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
index b4f1400..5b4b312 100644
--- a/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_portfolio_handlers.py
@@ -13,6 +13,7 @@
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
+
# from settings import (
# MAIN_MENU_OPTIONS,
# ADMIN_PORTFOLIO_OPTIONS,
@@ -66,18 +67,13 @@ class DeleteProject(AddProject):
async def get_portfolio_project_list(session: AsyncSession):
"""Получить список названий проектов для портфолио."""
projects = [
- project.project_name
- for project in await portfolio_crud.get_multi(session)
+ project.project_name for project in await portfolio_crud.get_multi(session)
]
return projects
-@portfolio_router.callback_query(
- SectionState.other_projects, F.data == "Добавить"
-)
-async def add_portfolio_project_name(
- callback: CallbackQuery, state: FSMContext
-):
+@portfolio_router.callback_query(SectionState.other_projects, F.data == "Добавить")
+async def add_portfolio_project_name(callback: CallbackQuery, state: FSMContext):
await callback.message.answer("Введите название проекта:")
await state.set_state(AddProject.project_name)
@@ -103,9 +99,7 @@ async def create_portfolio_project(
await state.clear()
-@portfolio_router.callback_query(
- SectionState.other_projects, F.data == "Удалить"
-)
+@portfolio_router.callback_query(SectionState.other_projects, F.data == "Удалить")
async def portfolio_project_to_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
@@ -123,9 +117,7 @@ async def portfolio_project_to_delete(
async def confirm_delete(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- portfolio_project = await portfolio_crud.get_by_project_name(
- callback.data, session
- )
+ portfolio_project = await portfolio_crud.get_by_project_name(callback.data, session)
await callback.message.edit_text(
f"Вы уверены, что хотите удалить этот проект?\n\n {portfolio_project.project_name}",
reply_markup=await get_inline_confirmation_keyboard(
@@ -135,15 +127,11 @@ async def confirm_delete(
await state.set_state(DeleteProject.confirm)
-@portfolio_router.callback_query(
- DeleteProject.confirm, F.data != PREVIOUS_MENU
-)
+@portfolio_router.callback_query(DeleteProject.confirm, F.data != PREVIOUS_MENU)
async def delete_protfolio_project(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
- portfolio_project = await portfolio_crud.get_by_project_name(
- callback.data, session
- )
+ portfolio_project = await portfolio_crud.get_by_project_name(callback.data, session)
await portfolio_crud.remove(portfolio_project, session)
await callback.message.edit_text(
"Проект удален!",
@@ -152,9 +140,7 @@ async def delete_protfolio_project(
await state.clear()
-@portfolio_router.callback_query(
- SectionState.other_projects, F.data == "Изменить"
-)
+@portfolio_router.callback_query(SectionState.other_projects, F.data == "Изменить")
async def portfolio_project_to_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
@@ -170,11 +156,11 @@ async def portfolio_project_to_update(
@portfolio_router.callback_query(
UpdateProject.select,
- and_f(F.data != "Название проекта", F.data != "Адрес ссылки", F.data != PREVIOUS_MENU),
+ and_f(
+ F.data != "Название проекта", F.data != "Адрес ссылки", F.data != PREVIOUS_MENU
+ ),
)
-async def update_portfolio_project_choise(
- callback: CallbackQuery, state: FSMContext
-):
+async def update_portfolio_project_choise(callback: CallbackQuery, state: FSMContext):
await state.update_data(select=callback.data)
await callback.message.edit_text(
"Что вы хотите отредактировать?",
@@ -184,11 +170,10 @@ async def update_portfolio_project_choise(
)
-@portfolio_router.callback_query(
- UpdateProject.select, F.data == "Название проекта"
-)
+@portfolio_router.callback_query(UpdateProject.select, F.data == "Название проекта")
async def about_name_update(
- callback: CallbackQuery, state: FSMContext,
+ callback: CallbackQuery,
+ state: FSMContext,
):
about_name = await state.get_data()
about_name_text = about_name.get("select")
@@ -198,29 +183,21 @@ async def about_name_update(
await state.set_state(UpdateProject.project_name)
-@portfolio_router.callback_query(
- UpdateProject.select, F.data == "Адрес ссылки"
-)
+@portfolio_router.callback_query(UpdateProject.select, F.data == "Адрес ссылки")
async def about_url_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
about_name_data = await state.get_data()
about_name_text = about_name_data.get("select")
- about_info = await portfolio_crud.get_by_project_name(
- about_name_text, session
- )
+ about_info = await portfolio_crud.get_by_project_name(about_name_text, session)
await callback.message.answer(
f"Сейчас у ссылки такой адрес:\n\n {about_info.url}\n\n Введите новое название"
)
await state.set_state(UpdateProject.url)
-@portfolio_router.message(
- or_f(UpdateProject.project_name, UpdateProject.url), F.text
-)
-async def update_about_info(
- message: Message, state: FSMContext, session: AsyncSession
-):
+@portfolio_router.message(or_f(UpdateProject.project_name, UpdateProject.url), F.text)
+async def update_about_info(message: Message, state: FSMContext, session: AsyncSession):
current_state = await state.get_state()
old_data = await state.get_data()
old_portfolio_data = await portfolio_crud.get_by_project_name(
diff --git a/app/admin/handlers/admin_handlers/admin_product_handlers.py b/app/admin/handlers/admin_handlers/admin_product_handlers.py
index 2657932..d95a310 100644
--- a/app/admin/handlers/admin_handlers/admin_product_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_product_handlers.py
@@ -13,6 +13,7 @@
get_inline_confirmation_keyboard,
get_inline_keyboard,
)
+
# from settings import (
# MAIN_MENU_OPTIONS,
# )
@@ -80,9 +81,7 @@ async def add_product_description(message: Message, state: FSMContext):
@product_router.message(AddProduct.response, F.text)
-async def creeate_product(
- message: Message, state: FSMContext, session: AsyncSession
-):
+async def creeate_product(message: Message, state: FSMContext, session: AsyncSession):
"""Создать продкет в БД."""
await state.update_data(response=message.text)
@@ -110,9 +109,7 @@ async def add_product_categoty(
last_product = await product_crud.get_last_added_product(session)
await state.update_data(product_id=last_product.id)
- await callback.message.answer(
- "Введите название для дополнительной информации"
- )
+ await callback.message.answer("Введите название для дополнительной информации")
await state.set_state(AddProductInfo.name)
@@ -135,9 +132,7 @@ async def add_product_category_name(message: Message, state: FSMContext):
or_f(AddProductInfo.name, AddProductInfo.description),
or_f(F.data == "Ссылка", F.data == "Текст", F.data == "Картинка"),
)
-async def add_product_category_data(
- callback: CallbackQuery, state: FSMContext
-):
+async def add_product_category_data(callback: CallbackQuery, state: FSMContext):
"""Добавить информацию в основной вариант."""
if callback.data == "Ссылка":
@@ -219,9 +214,7 @@ async def confirm_delete(
):
"""Подтверждение удаления."""
- portfolio_project = await product_crud.get_by_product_name(
- callback.data, session
- )
+ portfolio_project = await product_crud.get_by_product_name(callback.data, session)
await callback.message.edit_text(
f"Вы уверены, что хотите удалить "
f"этот проект?\n\n {portfolio_project.title}",
@@ -239,9 +232,7 @@ async def delete_product(
):
"""Удалить продукт из БД."""
- portfolio_project = await product_crud.get_by_product_name(
- callback.data, session
- )
+ portfolio_project = await product_crud.get_by_product_name(callback.data, session)
await product_crud.remove(portfolio_project, session)
await callback.message.edit_text(
@@ -277,9 +268,7 @@ async def product_to_update(
F.data != PREVIOUS_MENU,
),
)
-async def update_portfolio_project_choise(
- callback: CallbackQuery, state: FSMContext
-):
+async def update_portfolio_project_choise(callback: CallbackQuery, state: FSMContext):
"""Выбор поля для редактирования."""
await state.update_data(select=callback.data)
@@ -291,9 +280,7 @@ async def update_portfolio_project_choise(
)
-@product_router.callback_query(
- UpdateProduct.select, F.data == "Название проекта"
-)
+@product_router.callback_query(UpdateProduct.select, F.data == "Название проекта")
async def about_name_update(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
@@ -326,12 +313,8 @@ async def about_url_update(
await state.set_state(UpdateProduct.response)
-@product_router.message(
- or_f(UpdateProduct.title, UpdateProduct.response), F.text
-)
-async def update_about_info(
- message: Message, state: FSMContext, session: AsyncSession
-):
+@product_router.message(or_f(UpdateProduct.title, UpdateProduct.response), F.text)
+async def update_about_info(message: Message, state: FSMContext, session: AsyncSession):
"""Внести изменения продукта в БД."""
current_state = await state.get_state()
diff --git a/app/admin/handlers/user.py b/app/admin/handlers/user.py
index fa5e482..2477ac0 100644
--- a/app/admin/handlers/user.py
+++ b/app/admin/handlers/user.py
@@ -63,16 +63,12 @@ async def start_cmd(message: Message):
if message.from_user.id in admin_list:
await message.answer(
GREETINGS,
- reply_markup=await get_reply_keyboard(
- ADMIN_BASE_BUTTONS, size=(1, 2)
- ),
+ reply_markup=await get_reply_keyboard(ADMIN_BASE_BUTTONS, size=(1, 2)),
)
else:
await message.answer(
GREETINGS,
- reply_markup=await get_reply_keyboard(
- BASE_KEYBOARD_BUTTONS, size=(1, 2)
- ),
+ reply_markup=await get_reply_keyboard(BASE_KEYBOARD_BUTTONS, size=(1, 2)),
)
@@ -118,9 +114,7 @@ async def portfolio_info(callback: CallbackQuery, session: AsyncSession):
@user_router.callback_query(F.data == MAIN_MENU_OPTIONS.get("company_bio"))
-async def main_info(
- callback: CallbackQuery, session: AsyncSession, state: FSMContext
-):
+async def main_info(callback: CallbackQuery, session: AsyncSession, state: FSMContext):
"""Получить список ссылок на информацию о компании."""
await state.clear()
@@ -195,9 +189,7 @@ async def info_faq(
await state.set_state(QuestionAnswer.question)
-@user_router.callback_query(
- F.data == PORTFOLIO_MENU_OPTIONS.get("other_projects")
-)
+@user_router.callback_query(F.data == PORTFOLIO_MENU_OPTIONS.get("other_projects"))
async def portfolio_other_projects(
callback: CallbackQuery, session: AsyncSession, state: FSMContext
):
@@ -227,9 +219,7 @@ async def get_products_list(
):
"""Получить список продуктов."""
- products = [
- product.title for product in await product_crud.get_multi(session)
- ]
+ products = [product.title for product in await product_crud.get_multi(session)]
await state.clear()
@@ -300,9 +290,7 @@ async def get_product_info(
@user_router.callback_query(QuestionAnswer.question, F.data)
-async def faq_answer(
- callback: CallbackQuery, session: AsyncSession, state: FSMContext
-):
+async def faq_answer(callback: CallbackQuery, session: AsyncSession, state: FSMContext):
"""Получить ответ на вопрос из раздела Техподдержка."""
question_list = [
diff --git a/app/admin/keyboards/keyboards.py b/app/admin/keyboards/keyboards.py
index 7d389b0..ee99edc 100644
--- a/app/admin/keyboards/keyboards.py
+++ b/app/admin/keyboards/keyboards.py
@@ -35,11 +35,7 @@ async def get_inline_keyboard(
InlineKeyboardButton(
text=option,
callback_data=str(callback[index]),
- url=(
- urls[index]
- if urls and index in range(len(urls))
- else None
- ),
+ url=(urls[index] if urls and index in range(len(urls)) else None),
)
)
@@ -204,9 +200,7 @@ async def get_delete_message_keyboard() -> InlineKeyboardMarkup:
"""Создать копку для удаления сообщения."""
keyboard = InlineKeyboardBuilder()
- keyboard.add(
- InlineKeyboardButton(text="Понятно! :)", callback_data="delete")
- )
+ keyboard.add(InlineKeyboardButton(text="Понятно! :)", callback_data="delete"))
return keyboard.adjust(1).as_markup(resize_keyboard=True)
diff --git a/app/alembic/env.py b/app/alembic/env.py
index 35b65ef..68f55e3 100644
--- a/app/alembic/env.py
+++ b/app/alembic/env.py
@@ -10,11 +10,11 @@
from core.base import Base
-load_dotenv('.env')
+load_dotenv(".env")
config = context.config
-config.set_main_option('sqlalchemy.url', os.environ['DATABASE_URL'])
+config.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"])
if config.config_file_name is not None:
fileConfig(config.config_file_name)
diff --git a/app/alembic/versions/fd795bdbba90_update_feedback.py b/app/alembic/versions/fd795bdbba90_update_feedback.py
index cde669b..c6f71cc 100644
--- a/app/alembic/versions/fd795bdbba90_update_feedback.py
+++ b/app/alembic/versions/fd795bdbba90_update_feedback.py
@@ -5,6 +5,7 @@
Create Date: 2024-10-11 19:56:11.060934
"""
+
from typing import Sequence, Union
from alembic import op
@@ -12,7 +13,7 @@
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
-revision: str = 'fd795bdbba90'
+revision: str = "fd795bdbba90"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -20,20 +21,21 @@
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
- op.create_table('feedback',
- sa.Column('user', sa.Integer(), nullable=False),
- sa.Column('rating', sa.INTEGER(), nullable=False),
- sa.Column('feedback_text', sa.TEXT(), nullable=False),
- sa.Column('feedback_date', postgresql.TIMESTAMP(), nullable=False),
- sa.Column('unread', sa.BOOLEAN(), nullable=False),
- sa.Column('id', sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(['user'], ['user.id'], ondelete='CASCADE'),
- sa.PrimaryKeyConstraint('id')
+ op.create_table(
+ "feedback",
+ sa.Column("user", sa.Integer(), nullable=False),
+ sa.Column("rating", sa.INTEGER(), nullable=False),
+ sa.Column("feedback_text", sa.TEXT(), nullable=False),
+ sa.Column("feedback_date", postgresql.TIMESTAMP(), nullable=False),
+ sa.Column("unread", sa.BOOLEAN(), nullable=False),
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(["user"], ["user.id"], ondelete="CASCADE"),
+ sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('feedback')
+ op.drop_table("feedback")
# ### end Alembic commands ###
diff --git a/app/bot/bot_const.py b/app/bot/bot_const.py
index 439c9d5..7321765 100644
--- a/app/bot/bot_const.py
+++ b/app/bot/bot_const.py
@@ -1,46 +1,45 @@
from aiogram.fsm.state import StatesGroup, State
-ADMIN_POSITIVE_ANSWER: str = 'Добро пожаловать в админку!'
+ADMIN_POSITIVE_ANSWER: str = "Добро пожаловать в админку!"
-ADMIN_NEGATIVE_ANSWER: str = 'У вас нет прав администратора!'
+ADMIN_NEGATIVE_ANSWER: str = "У вас нет прав администратора!"
START_MESSAGE: str = (
- 'Здравстуйте! Я ваш виртуальный помощник. '
- 'Как я могу помочь вам сегодня? '
+ "Здравстуйте! Я ваш виртуальный помощник. "
+ "Как я могу помочь вам сегодня? "
)
MESSAGE_FOR_SHOW_PROJECTS: str = (
- 'Вот некоторые из наших проектов. '
- 'Выберите, чтобы узнать больше о каждом из них: '
+ "Вот некоторые из наших проектов. "
+ "Выберите, чтобы узнать больше о каждом из них: "
)
MESSAGE_FOR_PREVIOUS_CHOICE: str = (
- 'Вы вернулись в оснвное меню. '
- 'Как я могу помочь вам дальше? '
+ "Вы вернулись в оснвное меню. Как я могу помочь вам дальше? "
)
-MESSAGE_FOR_GET_QUESTIONS: str = 'Какой вид технической поддержки вам нужен? '
+MESSAGE_FOR_GET_QUESTIONS: str = "Какой вид технической поддержки вам нужен? "
-QUESTION_NOT_FOUND: str = 'Вопрос не найден.'
+QUESTION_NOT_FOUND: str = "Вопрос не найден."
-MESSAGE_FOR_BACK_TO_PRODUCTS: str = 'Вы вернулись к списку продуктов и услуг: '
+MESSAGE_FOR_BACK_TO_PRODUCTS: str = "Вы вернулись к списку продуктов и услуг: "
MESSAGE_FOR_VIEW_PORTFOLIO: str = (
- 'Вот ссылка на наше портфолио [url]. '
- 'Хотите узнать больше о конкретны проектах или услугах? '
+ "Вот ссылка на наше портфолио [url]. "
+ "Хотите узнать больше о конкретны проектах или услугах? "
)
MESSAGE_FOR_COMPANY_INFO: str = (
- 'Вот несколько вариантов информации о нашей компании.'
- 'Что именно вас интересует? '
+ "Вот несколько вариантов информации о нашей компании."
+ "Что именно вас интересует? "
)
-MESSAGE_FOR_GET_SUPPORT: str = 'Какой вид технической поддержки вам нужен?'
+MESSAGE_FOR_GET_SUPPORT: str = "Какой вид технической поддержки вам нужен?"
MESSAGE_FOR_PRODUCTS_SERVICES: str = (
- 'Мы предлагаем следующие продукты и услуги. '
- 'Какой из них вас интересует? '
+ "Мы предлагаем следующие продукты и услуги. "
+ "Какой из них вас интересует? "
)
@@ -52,54 +51,54 @@ class Form(StatesGroup):
QUESTIONS: dict[Form, str] = {
- Form.first_name: 'Введите ваше имя:',
+ Form.first_name: "Введите ваше имя:",
Form.phone_number: (
- 'Введите ваш номер телефона (в формате +7XXXXXXXXXX '
- 'или 8XXXXXXXXXX):'
- )
+ "Введите ваш номер телефона (в формате +7XXXXXXXXXX "
+ "или 8XXXXXXXXXX):"
+ ),
}
def succses_answer(user_data: dict) -> str:
return (
- f'Спасибо! Наш менеджер свяжется '
- f'с вами в ближайшее время.\n '
- f'Отправленная форма:\n '
- f'Имя: {user_data['first_name']}\n '
- f'Номер телефона: {user_data['phone_number']} '
+ f"Спасибо! Наш менеджер свяжется "
+ f"с вами в ближайшее время.\n "
+ f"Отправленная форма:\n "
+ f"Имя: {user_data['first_name']}\n "
+ f"Номер телефона: {user_data['phone_number']} "
)
INPUT_NUMBER_PHONE: str = (
- 'Номер телефона должен быть в формате +7XXXXXXXXXX '
- 'или 8XXXXXXXXXX. Попробуйте снова. '
+ "Номер телефона должен быть в формате +7XXXXXXXXXX "
+ "или 8XXXXXXXXXX. Попробуйте снова. "
)
-INPUT_NAME: str = (
- 'Имя должно содержать только буквы. Попробуйте снова.'
-)
+INPUT_NAME: str = "Имя должно содержать только буквы. Попробуйте снова."
START_INPUT_USER_DATA: str = (
- 'Пожалуйста, оставьте ваше имя и контактный номер, '
- 'и наш менеджер свяжется с вами. '
+ "Пожалуйста, оставьте ваше имя и контактный номер, "
+ "и наш менеджер свяжется с вами. "
+ "Оставляя контактные данные, вы соглашаетесь на их обработку "
+ "в соответствии с нашей политикой конфиденциальности."
)
MESSAGE_FOR_NOT_SUPPORTED_CONTENT_TYPE = (
- 'Бот не может обрабатывать этот тип контента. У программистов лапки.'
+ "Бот не может обрабатывать этот тип контента. У программистов лапки."
)
MESSAGE_FOR_GET_FEEDBACK = (
- 'Вы давно не взаимодействовали с ботом. Не хотите оставить отзыв?'
+ "Вы давно не взаимодействовали с ботом. Не хотите оставить отзыв?"
)
MESSAGE_FOR_GET_FEEDBACK_YES = (
- 'Спасибо за обращение! Если у вас будут вопросы, '
- 'не стейсняйтесь обращаться снова. Хорошего дня! '
+ "Спасибо за обращение! Если у вас будут вопросы, "
+ "не стейсняйтесь обращаться снова. Хорошего дня! "
)
MESSAGE_FOR_GET_FEEDBACK_NO = (
- 'Если у вас будут вопросы, '
- 'не стейсняйтесь обращаться снова. Хорошего дня! '
+ "Если у вас будут вопросы, "
+ "не стейсняйтесь обращаться снова. Хорошего дня! "
)
@@ -111,8 +110,8 @@ class FeedbackForm(StatesGroup):
FEEDBACK_QUESTIONS: dict[FeedbackForm, str] = {
- FeedbackForm.rating: 'Оцените работу бота от 1 до 10:',
+ FeedbackForm.rating: "Оцените работу бота от 1 до 10:",
FeedbackForm.feedback_text: (
- 'Пожалуйста, напишите, что вам понравилось или что можно улучшить:'
- )
+ "Пожалуйста, напишите, что вам понравилось или что можно улучшить:"
+ ),
}
diff --git a/app/bot/callbacks.py b/app/bot/callbacks.py
index 15808ff..c0d77c6 100644
--- a/app/bot/callbacks.py
+++ b/app/bot/callbacks.py
@@ -4,14 +4,20 @@
from aiogram.types import CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession
+from helpers import get_user_id, start_inactivity_timer
+from core.bot_setup import bot
from bot.exceptions import message_exception_handler
from bot.keyborads import (
- list_of_projects_keyboard, main_keyboard,
+ list_of_projects_keyboard,
+ main_keyboard,
faq_or_problems_with_products_inline_keyboard,
- category_type_inline_keyboard, inline_products_and_services,
- company_information_keyboard, company_portfolio_choice,
- support_keyboard, back_to_main_menu
+ category_type_inline_keyboard,
+ inline_products_and_services,
+ company_information_keyboard,
+ company_portfolio_choice,
+ support_keyboard,
+ back_to_main_menu,
)
from crud.questions import get_question_by_id
from crud.projects import get_title_by_id, response_text_by_id
@@ -19,17 +25,14 @@
from loggers.log import setup_logging
-setup_logging()
-
router = Router()
+setup_logging()
logger = logging.getLogger(__name__)
-@message_exception_handler(
- log_error_text='Ошибка при выводе списка проектов для'
-)
-@router.callback_query(F.data == 'show_projects')
+@message_exception_handler(log_error_text="Ошибка при выводе списка проектов.")
+@router.callback_query(F.data == "show_projects")
async def show_projects(
callback: CallbackQuery, session: AsyncSession
) -> None:
@@ -37,39 +40,40 @@ async def show_projects(
await callback.answer()
+ user_id = get_user_id(callback)
+
await callback.message.edit_text(
bc.MESSAGE_FOR_SHOW_PROJECTS,
- reply_markup=await list_of_projects_keyboard(session)
+ reply_markup=await list_of_projects_keyboard(session),
)
- logger.info(
- f'Пользователь {callback.from_user.id} запросил список проектов'
- )
+ logger.info(f"Пользователь {user_id} запросил список проектов")
+
+ await start_inactivity_timer(user_id, bot)
@message_exception_handler(
- log_error_text='Ошибка при возврате в основное меню для'
+ log_error_text="Ошибка при возврате в основное меню."
)
-@router.callback_query(F.data == 'back_to_main_menu')
+@router.callback_query(F.data == "back_to_main_menu")
async def previous_choice(callback: CallbackQuery) -> None:
"""Возвращает в основное меню."""
await callback.answer()
+ user_id = get_user_id(callback)
+
await callback.message.edit_text(
- bc.MESSAGE_FOR_PREVIOUS_CHOICE,
- reply_markup=main_keyboard
+ bc.MESSAGE_FOR_PREVIOUS_CHOICE, reply_markup=main_keyboard
)
- logger.info(
- f'Пользователь {callback.from_user.id} вернулся в основное меню'
- )
+ logger.info(f"Пользователь {user_id} вернулся в основное меню")
+ await start_inactivity_timer(user_id, bot)
-@message_exception_handler(
- log_error_text='Ошибка при получении вопросов для'
-)
-@router.callback_query(F.data.in_(('get_faq', 'get_problems_with_products')))
+
+@message_exception_handler(log_error_text="Ошибка при получении вопросов.")
+@router.callback_query(F.data.in_(("get_faq", "get_problems_with_products")))
async def get_questions(
callback: CallbackQuery, session: AsyncSession
) -> None:
@@ -77,28 +81,31 @@ async def get_questions(
await callback.answer()
+ user_id = get_user_id(callback)
+
question_type = (
- 'GENERAL_QUESTIONS' if callback.data == 'get_faq'
- else 'PROBLEMS_WITH_PRODUCTS'
+ "GENERAL_QUESTIONS" if callback.data == "get_faq"
+ else "PROBLEMS_WITH_PRODUCTS"
)
await callback.message.edit_text(
bc.MESSAGE_FOR_GET_QUESTIONS,
reply_markup=await faq_or_problems_with_products_inline_keyboard(
question_type, session
- )
+ ),
)
logger.info(
- f'Пользователь {callback.from_user.id} '
- f'запросил {question_type.lower()}.'
+ f"Пользователь {user_id} " f"запросил {question_type.lower()}."
)
+ await start_inactivity_timer(user_id, bot)
+
@message_exception_handler(
- log_error_text='Ошибка при получении ответа на вопрос.'
+ log_error_text="Ошибка при получении ответа на вопрос."
)
-@router.callback_query(F.data.startswith('answer:'))
+@router.callback_query(F.data.startswith("answer:"))
async def get_faq_answer(
callback: CallbackQuery, session: AsyncSession
) -> None:
@@ -106,30 +113,32 @@ async def get_faq_answer(
await callback.answer()
- question = await get_question_by_id(
- callback.data.split(':')[1], session
- )
+ user_id = get_user_id(callback)
+
+ question = await get_question_by_id(callback.data.split(":")[1], session)
if question:
await callback.message.edit_text(
- text=f'{question.answer}',
+ text=f"{question.answer}",
reply_markup=InlineKeyboardBuilder().add(
back_to_main_menu
- ).as_markup()
+ ).as_markup(),
)
else:
await callback.message.edit_text(bc.QUESTION_NOT_FOUND)
logger.info(
- f'Пользователь {callback.from_user.id} запросил '
- f'ответ на вопрос {callback.data.split(':')[1]} '
+ f"Пользователь {user_id} запросил "
+ f"ответ на вопрос {callback.data.split(':')[1]} "
)
+ await start_inactivity_timer(user_id, bot)
+
@message_exception_handler(
- log_error_text='Ошибка при возврате к выбору продуктов.'
+ log_error_text="Ошибка при возврате к выбору продуктов."
)
-@router.callback_query(F.data == 'back_to_previous_menu')
+@router.callback_query(F.data == "back_to_previous_menu")
async def back_to_products(
callback: CallbackQuery, session: AsyncSession
) -> None:
@@ -137,20 +146,22 @@ async def back_to_products(
await callback.answer()
+ user_id = get_user_id(callback)
+
await callback.message.edit_text(
text=bc.MESSAGE_FOR_BACK_TO_PRODUCTS,
- reply_markup=await inline_products_and_services(session)
+ reply_markup=await inline_products_and_services(session),
)
- logger.info(
- f'Пользователь {callback.from_user.id} вернулся к выбору продуктов.'
- )
+ logger.info(f"Пользователь {user_id} вернулся к выбору продуктов.")
+
+ await start_inactivity_timer(user_id, bot)
@message_exception_handler(
- log_error_text='Ошибка при запросе ответа для выбранной категории.'
+ log_error_text="Ошибка при запросе ответа для выбранной категории."
)
-@router.callback_query(F.data.startswith('category_'))
+@router.callback_query(F.data.startswith("category_"))
async def get_response_by_title(
callback: CallbackQuery, session: AsyncSession
) -> None:
@@ -158,78 +169,84 @@ async def get_response_by_title(
await callback.answer()
- category_id = int(callback.data.split('_')[1])
+ user_id = get_user_id(callback)
+
+ category_id = int(callback.data.split("_")[1])
await callback.message.edit_text(
text=await response_text_by_id(category_id, session),
reply_markup=await category_type_inline_keyboard(
await get_title_by_id(category_id, session), session
- )
+ ),
)
- logger.info(
- f'Пользователь {callback.from_user.id} выбрал категорию {category_id}.'
- )
+ logger.info(f"Пользователь {user_id} выбрал категорию {category_id}.")
+
+ await start_inactivity_timer(user_id, bot)
@message_exception_handler(
- log_error_text='Ошибка при показе портфолио.'
+ log_error_text="Ошибка при показе портфолио."
)
-@router.callback_query(F.data == 'view_portfolio')
+@router.callback_query(F.data == "view_portfolio")
async def view_portfolio(callback: CallbackQuery) -> None:
"""Показ портфолио компании."""
await callback.answer()
+ user_id = get_user_id(callback)
+
await callback.message.edit_text(
- bc.MESSAGE_FOR_VIEW_PORTFOLIO,
- reply_markup=company_portfolio_choice
+ bc.MESSAGE_FOR_VIEW_PORTFOLIO, reply_markup=company_portfolio_choice
)
- logger.info(f'Пользователь {callback.from_user.id} запросил портфолио.')
+ logger.info(f"Пользователь {user_id} запросил портфолио.")
+
+ await start_inactivity_timer(user_id, bot)
@message_exception_handler(
- log_error_text='Ошибка при запросе информации о компании.'
+ log_error_text="Ошибка при запросе информации о компании."
)
-@router.callback_query(F.data == 'company_info')
+@router.callback_query(F.data == "company_info")
async def company_info(callback: CallbackQuery) -> None:
"""Информация о компании."""
await callback.answer()
+ user_id = get_user_id(callback)
+
await callback.message.edit_text(
- bc.MESSAGE_FOR_COMPANY_INFO,
- reply_markup=company_information_keyboard
+ bc.MESSAGE_FOR_COMPANY_INFO, reply_markup=company_information_keyboard
)
- logger.info(
- f'Пользователь {callback.from_user.id} '
- f'запросил информацию о компании. '
- )
+ logger.info(f"Пользователь {user_id} " f"запросил информацию о компании. ")
+ await start_inactivity_timer(user_id, bot)
-@message_exception_handler(
- log_error_text='Ошибка при запросе техподдержки.'
-)
-@router.callback_query(F.data == 'tech_support')
+
+@message_exception_handler(log_error_text="Ошибка при запросе техподдержки.")
+@router.callback_query(F.data == "tech_support")
async def get_support(callback: CallbackQuery) -> None:
"""Выводит виды тех. поддержки."""
await callback.answer()
+ user_id = get_user_id(callback)
+
await callback.message.edit_text(
- bc.MESSAGE_FOR_GET_SUPPORT,
- reply_markup=support_keyboard
+ bc.MESSAGE_FOR_GET_SUPPORT, reply_markup=support_keyboard
)
- logger.info(f'Пользователь {callback.from_user.id} запросил техподдержку.')
+ logger.info(f"Пользователь {user_id} запросил техподдержку.")
+
+ await start_inactivity_timer(user_id, bot)
@message_exception_handler(
- log_error_text='Ошибка при запросе информации о продуктах и услугах.'
+ log_error_text="Ошибка при запросе информации о продуктах и услугах."
)
-@router.callback_query(F.data == 'products_services')
+@router.callback_query(F.data == "products_services")
async def products_services(
callback: CallbackQuery, session: AsyncSession
) -> None:
@@ -237,23 +254,28 @@ async def products_services(
await callback.answer()
+ user_id = get_user_id(callback)
+
await callback.message.edit_text(
bc.MESSAGE_FOR_PRODUCTS_SERVICES,
- reply_markup=await inline_products_and_services(session)
+ reply_markup=await inline_products_and_services(session),
)
logger.info(
- f'Пользователь {callback.from_user.id} запросил '
- f'информацию о продуктах и услугах. '
+ f"Пользователь {user_id} запросил "
+ f"информацию о продуктах и услугах. "
)
+ await start_inactivity_timer(user_id, bot)
+
@message_exception_handler(
log_error_text='Ошибка при ответе "нет" для составления фидбека.'
)
-@router.callback_query(F.data == 'get_feedback_no')
+@router.callback_query(F.data == "get_feedback_no")
async def get_feedback_no(callback: CallbackQuery) -> None:
"""Ответ на выбор 'Нет'."""
await callback.answer()
+
await callback.message.answer(bc.MESSAGE_FOR_GET_FEEDBACK_NO)
diff --git a/app/bot/exceptions.py b/app/bot/exceptions.py
index 525d413..f0bbd39 100644
--- a/app/bot/exceptions.py
+++ b/app/bot/exceptions.py
@@ -10,7 +10,9 @@
def message_exception_handler(
log_error_text: str,
- message_error_text: str = 'Произошла ошибка. Пожалуйста, попробуйте позже.'
+ message_error_text: str = (
+ "Произошла ошибка. Пожалуйста, попробуйте позже."
+ ),
) -> Callable:
"""Обработчик ошибок."""
diff --git a/app/bot/fsm_contexts/__init__.py b/app/bot/fsm_contexts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/bot/feedback_context.py b/app/bot/fsm_contexts/feedback_context.py
similarity index 54%
rename from app/bot/feedback_context.py
rename to app/bot/fsm_contexts/feedback_context.py
index f8fb850..1222dc7 100644
--- a/app/bot/feedback_context.py
+++ b/app/bot/fsm_contexts/feedback_context.py
@@ -7,10 +7,12 @@
import bot.bot_const as bc
from bot.exceptions import message_exception_handler
-from helpers import ask_next_question
+from helpers import ask_next_question, start_inactivity_timer, get_user_id
from loggers.log import setup_logging
from bot.validators import is_valid_rating
from crud.feedback import create_feedback
+from crud.users import get_id_by_tg_id
+from core.bot_setup import bot
router = Router()
@@ -19,31 +21,36 @@
logger = logging.getLogger(__name__)
-@message_exception_handler(
- log_error_text="Ошибка при составлении фидбека."
-)
-@router.callback_query(F.data == 'get_feedback_yes')
-async def get_feedback_yes(callback: CallbackQuery, state: FSMContext) -> None:
+@message_exception_handler(log_error_text="Ошибка при составлении фидбека.")
+@router.callback_query(F.data == "get_feedback_yes")
+async def get_feedback_yes(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+) -> None:
"""Начало сбора обратной связи."""
- await state.update_data()
+ user_id = get_user_id(callback)
+
+ await state.update_data(user=await get_id_by_tg_id(
+ callback.from_user.id, session))
await ask_next_question(
callback.message, state, bc.FeedbackForm.rating, bc.FEEDBACK_QUESTIONS
)
- logger.info(f'Пользователь {callback.from_user.id} начал процесс.')
+ logger.info(f"Пользователь {callback.from_user.id} начал процесс.")
+ await start_inactivity_timer(user_id, bot)
-@message_exception_handler(
- log_error_text="Ошибка при обработке оценки."
-)
+
+@message_exception_handler(log_error_text="Ошибка при обработке оценки.")
@router.message(bc.FeedbackForm.rating)
async def process_rating(message: Message, state: FSMContext) -> None:
"""Обрабатывает ввод оценки."""
rating = message.text
+ user_id = get_user_id(message)
+
if not is_valid_rating(rating):
await message.answer("Пожалуйста, введите число от 1 до 10.")
return
@@ -54,12 +61,12 @@ async def process_rating(message: Message, state: FSMContext) -> None:
message, state, bc.FeedbackForm.feedback_text, bc.FEEDBACK_QUESTIONS
)
- logger.info(f'Пользователь {message.from_user.id} ввел оценку.')
+ logger.info(f"Пользователь {message.from_user.id} ввел оценку.")
+ await start_inactivity_timer(user_id, bot)
-@message_exception_handler(
- log_error_text="Ошибка при обработке текста."
-)
+
+@message_exception_handler(log_error_text="Ошибка при обработке текста.")
@router.message(bc.FeedbackForm.feedback_text)
async def process_description(
message: Message, state: FSMContext, session: AsyncSession
@@ -68,16 +75,21 @@ async def process_description(
await state.update_data(feedback_text=message.text)
- logger.info(f'Пользователь {message.from_user.id} ввел текст.')
+ user_id = get_user_id(message)
+
+ logger.info(f"Пользователь {message.from_user.id} ввел текст.")
feedback_data = await state.get_data()
+
await create_feedback(feedback_data, session)
- logger.info(f'Запись создана в БД с ID: {feedback_data.id}.')
+ logger.info(f'Запись создана в БД с ID: {feedback_data.get("user")}.')
await message.answer(
- f'Спасибо за вашу оценку: {feedback_data['rating']}\n'
- f'Ваш комментарий: {feedback_data['feedback_text']}'
+ f"Спасибо за вашу оценку: {feedback_data['rating']}\n"
+ f"Ваш комментарий: {feedback_data['feedback_text']}"
)
+ await start_inactivity_timer(user_id, bot)
+
await state.clear()
diff --git a/app/bot/fsm_context.py b/app/bot/fsm_contexts/manager_context.py
similarity index 60%
rename from app/bot/fsm_context.py
rename to app/bot/fsm_contexts/manager_context.py
index 86640d6..bd328ed 100644
--- a/app/bot/fsm_context.py
+++ b/app/bot/fsm_contexts/manager_context.py
@@ -1,116 +1,127 @@
-import logging
-# import os
-# import asyncio
-
-from aiogram import F, Router
-from aiogram.fsm.context import FSMContext
-from aiogram.types import Message, CallbackQuery
-from aiogram.utils.keyboard import InlineKeyboardBuilder
-from sqlalchemy.ext.asyncio import AsyncSession
-
-import bot.bot_const as bc
-from bot.exceptions import message_exception_handler
-from bot.keyborads import back_to_main_menu
-# from bot.smtp import send_mail
-from bot.validators import (
- is_valid_name, is_valid_phone_number, format_phone_number
-)
-from crud.request_to_manager import create_request_to_manager
-from helpers import ask_next_question
-from loggers.log import setup_logging
-
-
-setup_logging()
-
-# CLIENT_EMAIL = os.getenv("EMAIL")
-
-
-router = Router()
-logger = logging.getLogger(__name__)
-
-
-@message_exception_handler(
- log_error_text='Ошибка при выводе формы.'
-)
-@router.callback_query(F.data.in_(('contact_manager', 'callback_request')))
-async def contact_with_manager(
- callback: CallbackQuery, state: FSMContext
-) -> None:
- """Выводит форму для связи с менеджером или запрос на обратный звонок."""
-
- await state.update_data(request_type=callback.data)
-
- await callback.message.edit_text(bc.START_INPUT_USER_DATA)
-
- await ask_next_question(
- callback.message, state, bc.Form.first_name, bc.QUESTIONS
- )
-
- logger.info(f'Пользователь {callback.from_user.id} начал процесс.')
-
-
-@message_exception_handler(
- log_error_text='Ошибка при обработке имени пользователя.'
-)
-@router.message(bc.Form.first_name)
-async def process_first_name(message: Message, state: FSMContext) -> None:
- """Состояние: ввод имени."""
-
- if not is_valid_name(message.text):
- await message.answer(bc.INPUT_NAME)
- return
-
- await state.update_data(first_name=message.text)
-
- logger.info(
- f'Пользователь {message.from_user.id} ввёл имя: {message.text}.'
- )
-
- await ask_next_question(message, state, bc.Form.phone_number, bc.QUESTIONS)
-
-
-@message_exception_handler(
- log_error_text='Ошибка при обработке номера телефона пользователя.'
-)
-@router.message(bc.Form.phone_number)
-async def process_phone_number(
- message: Message, state: FSMContext, session: AsyncSession
-) -> None:
- """Состояние: ввод номера телефона."""
-
- if not is_valid_phone_number(message.text):
- await message.answer(bc.INPUT_NUMBER_PHONE)
- return
-
- formatted_phone_number = format_phone_number(message.text)
-
- await state.update_data(phone_number=formatted_phone_number)
-
- logger.info(
- f'Пользователь {message.from_user.id} ввёл телефон: '
- f'{formatted_phone_number}.'
- )
-
- user_data = await state.get_data()
- request_type = user_data.pop('request_type')
-
- new_request = await create_request_to_manager(
- user_data, request_type, session
- )
-
- logger.info(f'Запись создана в БД с ID: {new_request.id}.')
-
- # mail = send_mail('Заявка на обратную связь', CLIENT_EMAIL, user_data)
- # asyncio.gather(asyncio.create_task(mail))
-
- # logger.info("Отправлено сообщение на почту менеджеру для связи "
- # f"с пользователем {message.from_user.id}")
-
- await message.answer(
- bc.succses_answer(user_data),
- reply_markup=InlineKeyboardBuilder().add(
- back_to_main_menu
- ).as_markup()
- )
-
- await state.clear()
+import logging
+import asyncio
+
+from aiogram import F, Router
+from aiogram.fsm.context import FSMContext
+from aiogram.types import Message, CallbackQuery
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+from sqlalchemy.ext.asyncio import AsyncSession
+
+import bot.bot_const as bc
+from bot.exceptions import message_exception_handler
+from bot.keyborads import back_to_main_menu
+from bot.smtp import send_mail
+from bot.validators import (
+ is_valid_name, is_valid_phone_number, format_phone_number
+)
+from crud.request_to_manager import create_request_to_manager
+from helpers import ask_next_question, get_user_id, start_inactivity_timer
+from loggers.log import setup_logging
+from core.bot_setup import bot
+from core.settings import settings
+
+
+router = Router()
+
+
+setup_logging()
+logger = logging.getLogger(__name__)
+
+
+@message_exception_handler(log_error_text="Ошибка при выводе формы.")
+@router.callback_query(F.data.in_(("contact_manager", "callback_request")))
+async def contact_with_manager(
+ callback: CallbackQuery, state: FSMContext
+) -> None:
+ """Выводит форму для связи с менеджером или запрос на обратный звонок."""
+
+ user_id = get_user_id(callback)
+
+ await state.update_data(request_type=callback.data)
+
+ await callback.message.edit_text(bc.START_INPUT_USER_DATA)
+
+ await ask_next_question(
+ callback.message, state, bc.Form.first_name, bc.QUESTIONS
+ )
+
+ logger.info(f"Пользователь {user_id} начал процесс.")
+
+ await start_inactivity_timer(user_id, bot)
+
+
+@message_exception_handler(
+ log_error_text="Ошибка при обработке имени пользователя."
+)
+@router.message(bc.Form.first_name)
+async def process_first_name(message: Message, state: FSMContext) -> None:
+ """Состояние: ввод имени."""
+
+ user_id = get_user_id(message)
+
+ if not is_valid_name(message.text):
+ await message.answer(bc.INPUT_NAME)
+ return
+
+ await state.update_data(first_name=message.text)
+
+ logger.info(
+ f"Пользователь {message.from_user.id} ввёл имя: {message.text}."
+ )
+
+ await ask_next_question(message, state, bc.Form.phone_number, bc.QUESTIONS)
+
+ await start_inactivity_timer(user_id, bot)
+
+
+@message_exception_handler(
+ log_error_text="Ошибка при обработке номера телефона пользователя."
+)
+@router.message(bc.Form.phone_number)
+async def process_phone_number(
+ message: Message, state: FSMContext, session: AsyncSession
+) -> None:
+ """Состояние: ввод номера телефона."""
+
+ user_id = get_user_id(message)
+
+ if not is_valid_phone_number(message.text):
+ await message.answer(bc.INPUT_NUMBER_PHONE)
+ return
+
+ formatted_phone_number = format_phone_number(message.text)
+
+ await state.update_data(phone_number=formatted_phone_number)
+
+ logger.info(
+ f"Пользователь {user_id} ввёл телефон: "
+ f"{formatted_phone_number}."
+ )
+
+ user_data = await state.get_data()
+ request_type = user_data.pop("request_type")
+
+ new_request = await create_request_to_manager(
+ user_data, request_type, session
+ )
+
+ logger.info(f"Запись создана в БД с ID: {new_request.id}.")
+
+ mail = send_mail("Заявка на обратную связь", settings.email, user_data)
+ asyncio.gather(asyncio.create_task(mail))
+
+ logger.info(
+ "Отправлено сообщение на почту менеджеру для связи "
+ f"с пользователем {user_id}"
+ )
+
+ await message.answer(
+ bc.succses_answer(user_data),
+ reply_markup=InlineKeyboardBuilder().add(
+ back_to_main_menu
+ ).as_markup(),
+ )
+
+ await start_inactivity_timer(user_id, bot)
+
+ await state.clear()
diff --git a/app/bot/handlers.py b/app/bot/handlers.py
index e6126c1..d9d2db0 100644
--- a/app/bot/handlers.py
+++ b/app/bot/handlers.py
@@ -20,16 +20,17 @@
from loggers.log import setup_logging
-setup_logging()
router = Router()
+
+setup_logging()
logger = logging.getLogger(__name__)
@message_exception_handler(
- log_error_text='Ошибка при обработке команды /admin.'
+ log_error_text="Ошибка при обработке команды /admin."
)
-@router.message(Command('admin'))
+@router.message(Command("admin"))
async def cmd_admin(message: Message, session: AsyncSession) -> None:
"""Вход в админку."""
@@ -39,18 +40,16 @@ async def cmd_admin(message: Message, session: AsyncSession) -> None:
if role in (RoleEnum.ADMIN, RoleEnum.MANAGER):
await message.answer(
ADMIN_POSITIVE_ANSWER,
- reply_markup=await get_inline_keyboard(MAIN_MENU_BUTTONS)
+ reply_markup=await get_inline_keyboard(MAIN_MENU_BUTTONS),
)
else:
await message.answer(ADMIN_NEGATIVE_ANSWER)
- logger.info(
- f'Пользователь {user_id} вызвал команду /admin.'
- )
+ logger.info(f"Пользователь {user_id} вызвал команду /admin.")
@message_exception_handler(
- log_error_text='Ошибка при обработке команды /start.'
+ log_error_text="Ошибка при обработке команды /start."
)
@router.message(CommandStart())
async def cmd_start(message: Message, session: AsyncSession) -> None:
@@ -61,14 +60,9 @@ async def cmd_start(message: Message, session: AsyncSession) -> None:
if not await is_user_in_db(user_id, session):
await create_user_id(user_id, session)
- await message.answer(
- START_MESSAGE,
- reply_markup=main_keyboard
- )
+ await message.answer(START_MESSAGE, reply_markup=main_keyboard)
- logger.info(
- f'Пользователь {user_id} вызвал команду /start.'
- )
+ logger.info(f"Пользователь {user_id} вызвал команду /start.")
await start_inactivity_timer(user_id, bot)
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index efa9451..a8344d0 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -8,45 +8,38 @@
back_to_main_menu = InlineKeyboardButton(
- text='Вернуться к основным вариантам.',
- callback_data='back_to_main_menu'
+ text="Вернуться к основным вариантам.", callback_data="back_to_main_menu"
)
back_to_previous_menu = InlineKeyboardButton(
- text='Назад к продуктам.',
- callback_data='back_to_previous_menu'
+ text="Назад к продуктам.", callback_data="back_to_previous_menu"
)
main_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
- text='Посмотреть портфолио.',
- callback_data='view_portfolio'
+ text="Посмотреть портфолио.", callback_data="view_portfolio"
)
],
[
InlineKeyboardButton(
- text='Получить информацию о компании.',
- callback_data='company_info'
+ text="Получить информацию о компании.", callback_data="company_info"
)
],
[
InlineKeyboardButton(
- text='Узнать о продуктах и услугах.',
- callback_data='products_services'
+ text="Узнать о продуктах и услугах.", callback_data="products_services"
)
],
[
InlineKeyboardButton(
- text='Получить техническую поддержку.',
- callback_data='tech_support'
+ text="Получить техническую поддержку.", callback_data="tech_support"
)
],
[
InlineKeyboardButton(
- text='Связаться с менеджером.',
- callback_data='contact_manager'
+ text="Связаться с менеджером.", callback_data="contact_manager"
)
],
]
@@ -56,17 +49,16 @@
inline_keyboard=[
[
InlineKeyboardButton(
- text='Презентация компании.',
- url='https://www.visme.co/ru/powerpoint-online/'
+ text="Презентация компании.",
+ url="https://www.visme.co/ru/powerpoint-online/",
)
],
[
InlineKeyboardButton(
- text='Карточка компании.',
- url='https://github.com/Rxyalxrd'
+ text="Карточка компании.", url="https://github.com/Rxyalxrd"
)
],
- [back_to_main_menu]
+ [back_to_main_menu],
]
)
@@ -79,10 +71,9 @@ async def inline_products_and_services(session: AsyncSession):
objects_in_db = await get_all_prtfolio_projects(ProductCategory, session)
for obj in objects_in_db:
- keyboard.add(InlineKeyboardButton(
- text=obj.title,
- callback_data=f'category_{obj.id}'
- ))
+ keyboard.add(
+ InlineKeyboardButton(text=obj.title, callback_data=f"category_{obj.id}")
+ )
keyboard.add(back_to_main_menu)
@@ -93,11 +84,10 @@ async def inline_products_and_services(session: AsyncSession):
inline_keyboard=[
[
InlineKeyboardButton(
- text='Перейти к проектам.',
- callback_data='show_projects'
+ text="Перейти к проектам.", callback_data="show_projects"
)
],
- [back_to_main_menu]
+ [back_to_main_menu],
]
)
@@ -110,12 +100,7 @@ async def list_of_projects_keyboard(session: AsyncSession):
keyboard = InlineKeyboardBuilder()
for project in projects:
- keyboard.add(
- InlineKeyboardButton(
- text=project.project_name,
- url=project.url
- )
- )
+ keyboard.add(InlineKeyboardButton(text=project.project_name, url=project.url))
keyboard.add(back_to_main_menu)
@@ -124,32 +109,24 @@ async def list_of_projects_keyboard(session: AsyncSession):
support_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
+ [InlineKeyboardButton(text="F.A.Q", callback_data="get_faq")],
[
InlineKeyboardButton(
- text='F.A.Q',
- callback_data='get_faq'
+ text="Проблемы с продуктами", callback_data="get_problems_with_products"
)
],
[
InlineKeyboardButton(
- text='Проблемы с продуктами',
- callback_data='get_problems_with_products'
+ text="Запрос на обратный звонок", callback_data="callback_request"
)
],
- [
- InlineKeyboardButton(
- text='Запрос на обратный звонок',
- callback_data='callback_request'
- )
- ],
- [back_to_main_menu]
+ [back_to_main_menu],
]
)
async def faq_or_problems_with_products_inline_keyboard(
- question_type: str,
- session: AsyncSession
+ question_type: str, session: AsyncSession
) -> InlineKeyboardMarkup:
"""Инлайн-клавиатуры для f.a.q вопросов или проблем с продуктами."""
@@ -159,8 +136,7 @@ async def faq_or_problems_with_products_inline_keyboard(
for question in questions:
keyboard.add(
InlineKeyboardButton(
- text=question.question,
- callback_data=f"answer:{question.id}"
+ text=question.question, callback_data=f"answer:{question.id}"
)
)
@@ -170,8 +146,7 @@ async def faq_or_problems_with_products_inline_keyboard(
async def category_type_inline_keyboard(
- product_name: str,
- session: AsyncSession
+ product_name: str, session: AsyncSession
) -> InlineKeyboardMarkup:
"""Инлайн клавиатура для типов в категориях."""
@@ -181,10 +156,7 @@ async def category_type_inline_keyboard(
for category_type in category_types:
keyboard.add(
- InlineKeyboardButton(
- text=category_type.name,
- url=category_type.url
- )
+ InlineKeyboardButton(text=category_type.name, url=category_type.url)
)
keyboard.add(back_to_previous_menu)
@@ -192,7 +164,12 @@ async def category_type_inline_keyboard(
return keyboard.adjust(1).as_markup()
-get_feedback_keyboard = InlineKeyboardBuilder().add(
- InlineKeyboardButton(text='Да', callback_data='get_feedback_yes'),
- InlineKeyboardButton(text='Нет', callback_data='get_feedback_no')
-).adjust(1).as_markup()
+get_feedback_keyboard = (
+ InlineKeyboardBuilder()
+ .add(
+ InlineKeyboardButton(text="Да", callback_data="get_feedback_yes"),
+ InlineKeyboardButton(text="Нет", callback_data="get_feedback_no"),
+ )
+ .adjust(1)
+ .as_markup()
+)
diff --git a/app/bot/smtp.py b/app/bot/smtp.py
index bd49788..1bfe320 100644
--- a/app/bot/smtp.py
+++ b/app/bot/smtp.py
@@ -12,15 +12,16 @@
async def send_mail(subject, to, user_data):
- text = (f'Пользователь {user_data["first_name"]} '
- f'заказал звонок по номеру {user_data["phone_number"]}')
+ text = (
+ f'Пользователь {user_data["first_name"]} '
+ f'заказал звонок по номеру {user_data["phone_number"]}'
+ )
message = MIMEMultipart()
message["From"] = BASE_EMAIL
message["To"] = to
message["Subject"] = subject
- message.attach(
- MIMEText(f"{text}", "html", "utf-8"))
+ message.attach(MIMEText(f"{text}", "html", "utf-8"))
smtp_client = SMTP(hostname="smtp.yandex.ru", port=465, use_tls=True)
async with smtp_client:
diff --git a/app/bot/validators.py b/app/bot/validators.py
index 84ff0b3..f17e7f3 100644
--- a/app/bot/validators.py
+++ b/app/bot/validators.py
@@ -3,7 +3,7 @@
def is_valid_name(name: str) -> bool:
"""Проверяет, что имя содержит только буквы."""
-
+ # добавить чтобы был пробел
return bool(match(r"^[A-Za-zА-Яа-яЁё]+$", name))
@@ -21,8 +21,8 @@ def is_valid_phone_number(phone_number: str) -> bool:
def format_phone_number(phone_number: str) -> str:
"""Преобразует номер телефона, начинающийся с 8, в формат +7."""
- if phone_number.startswith('8'):
- return '+7' + phone_number[1:]
+ if phone_number.startswith("8"):
+ return "+7" + phone_number[1:]
return phone_number
diff --git a/app/const.py b/app/const.py
index 88b3bb2..63b820a 100644
--- a/app/const.py
+++ b/app/const.py
@@ -68,7 +68,9 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
SUPPROT_MENU_BUTTONS = get_buttons(SUPPORT_OPTIONS)
# Информация о компании - кнопки и текст
-COMPANY_ABOUT = "Вот несколько вариантов информации о нашей компании. Что именно вас интересует?"
+COMPANY_ABOUT = (
+ "Вот несколько вариантов информации о нашей компании. Что именно вас интересует?"
+)
# Портфолио - кнопки и текст
@@ -83,9 +85,7 @@ def get_buttons(menu: dict[str, str]) -> list[str]:
PORTFOLIO_DEFAULT_DATA = {"name": "Портфолио", "url": "https://scid.ru/cases"}
# Продукты
-PRODUCT_LIST_TEXT = (
- "Мы предлагаем следющие продукты и услуги. Что Вас интересует?"
-)
+PRODUCT_LIST_TEXT = "Мы предлагаем следющие продукты и услуги. Что Вас интересует?"
PRODUCT_LIST = []
# Константы проекта
diff --git a/app/core/bot_setup.py b/app/core/bot_setup.py
index 5af2271..a02f8db 100644
--- a/app/core/bot_setup.py
+++ b/app/core/bot_setup.py
@@ -15,6 +15,6 @@ def check_token() -> None:
if settings.bot_token is None:
logger.error("Токен бота не найден.")
- raise ValueError('Отсутствуют необходимые токены.')
+ raise ValueError("Отсутствуют необходимые токены.")
else:
logger.info("Токен бота успешно загружен.")
diff --git a/app/core/db.py b/app/core/db.py
index 9bc9233..07fc697 100644
--- a/app/core/db.py
+++ b/app/core/db.py
@@ -1,10 +1,6 @@
from sqlalchemy import Integer
-from sqlalchemy.ext.asyncio import (
- AsyncSession, create_async_engine, async_sessionmaker
-)
-from sqlalchemy.orm import (
- declarative_base, declared_attr, Mapped, mapped_column
-)
+from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
+from sqlalchemy.orm import declarative_base, declared_attr, Mapped, mapped_column
from .settings import settings
diff --git a/app/core/init_db.py b/app/core/init_db.py
index e1dc466..79bdbc2 100644
--- a/app/core/init_db.py
+++ b/app/core/init_db.py
@@ -10,6 +10,4 @@ async def add_portfolio():
if not await company_info_crud.get_by_about_name(
PORTFOLIO_DEFAULT_DATA.get("name"), async_session
):
- await company_info_crud.create(
- PORTFOLIO_DEFAULT_DATA, async_session
- )
+ await company_info_crud.create(PORTFOLIO_DEFAULT_DATA, async_session)
diff --git a/app/core/settings.py b/app/core/settings.py
index e6425ab..90b91f4 100644
--- a/app/core/settings.py
+++ b/app/core/settings.py
@@ -7,6 +7,7 @@ class Settings(BaseSettings):
Взятие данных из .env и их валидация.
"""
+
database_url: str
bot_token: str
telegram_chat_ids: str
@@ -19,7 +20,7 @@ class Settings(BaseSettings):
# db_port: str
class Config:
- env_file = '.env'
+ env_file = ".env"
settings = Settings()
diff --git a/app/crud/about_crud.py b/app/crud/about_crud.py
index 84fae91..adaa0b0 100644
--- a/app/crud/about_crud.py
+++ b/app/crud/about_crud.py
@@ -4,6 +4,7 @@
from sqlalchemy.ext.asyncio import AsyncSession
from models.models import InformationAboutCompany
+
# from core.settings import PORTFOLIO_DEFAULT_DATA
@@ -33,9 +34,7 @@ async def get_portfolio(self, session: AsyncSession):
async def get_multi(self, session: AsyncSession):
"""Получить список всех объектов модели из БД."""
- db_objs = await session.execute(
- select(self.model).where(self.model.id != 1)
- )
+ db_objs = await session.execute(select(self.model).where(self.model.id != 1))
return db_objs.scalars().all()
diff --git a/app/crud/category_product.py b/app/crud/category_product.py
index 12565d4..3fb5dad 100644
--- a/app/crud/category_product.py
+++ b/app/crud/category_product.py
@@ -6,9 +6,7 @@
class CategoryTypeCRUD(CRUDBase):
- async def get_category_by_product_id(
- self, product_id: int, session: AsyncSession
- ):
+ async def get_category_by_product_id(self, product_id: int, session: AsyncSession):
"""Получить список всех вариантов продукта."""
product_categories = await session.execute(
select(self.model).where(self.model.product_id == product_id)
@@ -27,9 +25,7 @@ async def get_active_field(
)
return active_field.scalars().first()
- async def get_multi_for_product(
- self, product_id: int, session: AsyncSession
- ):
+ async def get_multi_for_product(self, product_id: int, session: AsyncSession):
categories_for_product = await session.execute(
select(self.model).where(self.model.product_id == product_id)
)
diff --git a/app/crud/feedback.py b/app/crud/feedback.py
index be87f35..386ab4c 100644
--- a/app/crud/feedback.py
+++ b/app/crud/feedback.py
@@ -8,10 +8,7 @@
async def create_feedback(user_data: dict, session: AsyncSession) -> Feedback:
"""Создание записи с отзывом."""
- data_to_db = Feedback(
- **user_data,
- feedback_date=datetime.utcnow()
- )
+ data_to_db = Feedback(**user_data, feedback_date=datetime.utcnow())
session.add(data_to_db)
await session.commit()
diff --git a/app/crud/projects.py b/app/crud/projects.py
index 4b7e8f6..b5ad7d3 100644
--- a/app/crud/projects.py
+++ b/app/crud/projects.py
@@ -1,14 +1,10 @@
from sqlalchemy.ext.asyncio import AsyncSession
-from models.models import (
- CheckCompanyPortfolio, ProductCategory,
- CategoryType
-)
+from models.models import CheckCompanyPortfolio, ProductCategory, CategoryType
from sqlalchemy import select
async def get_all_prtfolio_projects(
- object_model: CheckCompanyPortfolio | ProductCategory,
- session: AsyncSession
+ object_model: CheckCompanyPortfolio | ProductCategory, session: AsyncSession
) -> list[CheckCompanyPortfolio | ProductCategory]:
"""Получение всех проектов-портфолио или продуктов и услуг."""
@@ -34,9 +30,7 @@ async def get_categories_by_name(
result = await session.execute(
select(CategoryType)
- .join(
- ProductCategory, ProductCategory.id == CategoryType.product_id
- )
+ .join(ProductCategory, ProductCategory.id == CategoryType.product_id)
.where(ProductCategory.title == product_name)
)
@@ -47,9 +41,7 @@ async def get_title_by_id(category_id: int, session: AsyncSession) -> str:
"""Получает название категории по ID из базы данных."""
result = await session.execute(
- select(ProductCategory.title).where(
- ProductCategory.id == category_id
- )
+ select(ProductCategory.title).where(ProductCategory.id == category_id)
)
category_name = result.scalar()
diff --git a/app/crud/questions.py b/app/crud/questions.py
index d43b7e5..2639834 100644
--- a/app/crud/questions.py
+++ b/app/crud/questions.py
@@ -5,7 +5,7 @@
async def get_question_by_title(
- question_type: str, session: AsyncSession
+ question_type: str, session: AsyncSession
) -> list[Info]:
"""Получаем все вопросы по категории."""
@@ -16,13 +16,9 @@ async def get_question_by_title(
return result.scalars().all()
-async def get_question_by_id(
- question_id: int, session: AsyncSession
-) -> Info | None:
+async def get_question_by_id(question_id: int, session: AsyncSession) -> Info | None:
"""Получить вопрос по его ID."""
- result = await session.execute(
- select(Info).where(Info.id == int(question_id))
- )
+ result = await session.execute(select(Info).where(Info.id == int(question_id)))
return result.scalar_one_or_none()
diff --git a/app/crud/request_to_manager.py b/app/crud/request_to_manager.py
index ae65f71..e755ec6 100644
--- a/app/crud/request_to_manager.py
+++ b/app/crud/request_to_manager.py
@@ -6,15 +6,15 @@
async def create_request_to_manager(
- user_data: dict, request_type: str, session: AsyncSession
+ user_data: dict, request_type: str, session: AsyncSession
) -> ContactManager:
"""Создание заявки на связь с менеджером."""
data_to_db = ContactManager(
**user_data,
shipping_date=datetime.utcnow(),
- need_support=(request_type == 'callback_request'),
- need_contact_with_manager=(request_type == 'contact_manager')
+ need_support=(request_type == "callback_request"),
+ need_contact_with_manager=(request_type == "contact_manager")
)
session.add(data_to_db)
diff --git a/app/crud/users.py b/app/crud/users.py
index 8066ba0..cfb3ddc 100644
--- a/app/crud/users.py
+++ b/app/crud/users.py
@@ -29,8 +29,14 @@ async def is_user_in_db(tg_id: int, session: AsyncSession) -> bool:
async def get_role_by_tg_id(tg_id: int, session: AsyncSession) -> User:
"""Получаем роль пользователя по его tg_id."""
- result = await session.execute(
- select(User.role).where(User.tg_id == tg_id)
- )
+ result = await session.execute(select(User.role).where(User.tg_id == tg_id))
+
+ return result.scalar()
+
+
+async def get_id_by_tg_id(tg_id: int, session: AsyncSession) -> User:
+ """Получение id по tg_id."""
+
+ result = await session.execute(select(User.id).where(User.tg_id == tg_id))
return result.scalar()
diff --git a/app/helpers.py b/app/helpers.py
index 8f5b1f7..e445823 100644
--- a/app/helpers.py
+++ b/app/helpers.py
@@ -1,7 +1,7 @@
import logging
import asyncio
-from aiogram.types import Message
+from aiogram.types import Message, CallbackQuery
from aiogram import Bot
from aiogram.fsm.state import State
from aiogram.fsm.context import FSMContext
@@ -17,7 +17,7 @@
logger = logging.getLogger(__name__)
-def get_user_id(message: Message) -> int:
+def get_user_id(message: Message | CallbackQuery) -> int:
"""Получает ID пользователя из сообщения."""
return message.from_user.id
@@ -27,7 +27,7 @@ def get_user_id(message: Message) -> int:
async def start_inactivity_timer(
- user_id: int, bot: Bot, timeout: int = 10
+ user_id: int, bot: Bot, timeout: int = 10
) -> None:
"""
Запускает таймер.
@@ -53,8 +53,7 @@ async def inactivity_timer(user_id: int, bot: Bot, timeout: int):
if user_id in user_timers:
await bot.send_message(
- user_id,
- bc.MESSAGE_FOR_GET_FEEDBACK,
+ user_id, bc.MESSAGE_FOR_GET_FEEDBACK,
reply_markup=get_feedback_keyboard
)
del user_timers[user_id]
@@ -64,17 +63,17 @@ async def inactivity_timer(user_id: int, bot: Bot, timeout: int):
@message_exception_handler(
- log_error_text='Ошибка при переходе к следующему вопросу.'
+ log_error_text="Ошибка при переходе к следующему вопросу."
)
async def ask_next_question(
message: Message,
state: FSMContext,
next_state: State,
- questions: dict[FeedbackForm | Form, str]
+ questions: dict[FeedbackForm | Form, str],
) -> None:
"""Переход к следующему вопросу."""
await state.set_state(next_state.state)
await message.answer(questions[next_state])
- logger.info(f'Переход к следующему вопросу: {next_state}.')
+ logger.info(f"Переход к следующему вопросу: {next_state}.")
diff --git a/app/loggers/log.py b/app/loggers/log.py
index 09d67d3..0cfee7f 100644
--- a/app/loggers/log.py
+++ b/app/loggers/log.py
@@ -2,7 +2,7 @@
from pathlib import Path
-log_dir = Path(__file__).parent / 'log_levels'
+log_dir = Path(__file__).parent / "log_levels"
log_dir.mkdir(exist_ok=True)
@@ -24,29 +24,25 @@ def setup_logging():
if logger.hasHandlers():
logger.handlers.clear()
- info_handler = logging.FileHandler(log_dir / 'info.log', encoding='utf-8')
+ info_handler = logging.FileHandler(log_dir / "info.log", encoding="utf-8")
info_handler.setLevel(logging.INFO)
info_handler.addFilter(LevelFilter(logging.INFO))
info_handler.setFormatter(
- logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
+ logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
)
- error_handler = logging.FileHandler(
- log_dir / 'error.log', encoding='utf-8'
- )
+ error_handler = logging.FileHandler(log_dir / "error.log", encoding="utf-8")
error_handler.setLevel(logging.ERROR)
error_handler.addFilter(LevelFilter(logging.ERROR))
error_handler.setFormatter(
- logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
+ logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
)
- debug_handler = logging.FileHandler(
- log_dir / 'debug.log', encoding='utf-8'
- )
+ debug_handler = logging.FileHandler(log_dir / "debug.log", encoding="utf-8")
debug_handler.setLevel(logging.DEBUG)
debug_handler.addFilter(LevelFilter(logging.DEBUG))
debug_handler.setFormatter(
- logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
+ logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
)
logger.setLevel(logging.DEBUG)
@@ -56,7 +52,29 @@ def setup_logging():
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
- console_handler.setFormatter(logging.Formatter(
- '%(asctime)s - %(levelname)s - %(message)s')
+ console_handler.setFormatter(
+ logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
+ )
+ logger.addHandler(console_handler) # вывод в консоль
+
+ alembic_logger = logging.getLogger("alembic")
+ alembic_logger.setLevel(logging.INFO)
+
+ alembic_info_handler = logging.FileHandler(
+ log_dir / "alembic_info.log", encoding="utf-8"
)
- logger.addHandler(console_handler)
+ alembic_info_handler.setLevel(logging.INFO)
+ alembic_info_handler.setFormatter(
+ logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
+ )
+ alembic_logger.addHandler(alembic_info_handler)
+
+ alembic_error_handler = logging.FileHandler(
+ log_dir / "alembic_error.log", encoding="utf-8"
+ )
+ alembic_error_handler.setLevel(logging.ERROR)
+ alembic_error_handler.setFormatter(
+ logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
+ )
+
+ # alembic_logger.addHandler(alembic_error_handler) вывод в консоль
diff --git a/app/main.py b/app/main.py
index 0cef2d9..bb73b65 100644
--- a/app/main.py
+++ b/app/main.py
@@ -7,8 +7,8 @@
from core.bot_setup import bot, dispatcher, check_token
from bot.handlers import router as message_router
from bot.callbacks import router as callback_router
-from bot.fsm_context import router as fsm_context_router
-from bot.feedback_context import router as feedback_context
+from bot.fsm_contexts.manager_context import router as fsm_context_router
+from bot.fsm_contexts.feedback_context import router as feedback_context
from core.init_db import add_portfolio
from admin.handlers.admin_handlers import admin_router
from admin.handlers.user import user_router
@@ -37,9 +37,7 @@ async def main() -> None:
try:
logger.info("Запуск бота...")
- dispatcher.update.middleware(
- DataBaseSession(session_pool=AsyncSessionLocal)
- )
+ dispatcher.update.middleware(DataBaseSession(session_pool=AsyncSessionLocal))
await add_portfolio()
await dispatcher.start_polling(bot, skip_updates=True)
diff --git a/app/models/models.py b/app/models/models.py
index 2c3061f..e9d4bfb 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -6,18 +6,18 @@
from sqlalchemy.sql import func
import sqlalchemy.dialects.postgresql as pgsql_types
-from app.core.db import Base
+from core.db import Base
class RoleEnum(str, Enum):
- USER = 'U'
- ADMIN = 'A'
- MANAGER = 'M'
+ USER = "U"
+ ADMIN = "A"
+ MANAGER = "M"
class QuestionEnum(str, Enum):
- GENERAL_QUESTIONS = 'Общие вопросы'
- PROBLEMS_WITH_PRODUCTS = 'Проблемы с продуктами'
+ GENERAL_QUESTIONS = "Общие вопросы"
+ PROBLEMS_WITH_PRODUCTS = "Проблемы с продуктами"
class User(Base):
@@ -59,7 +59,7 @@ class CategoryType(Base):
name: Mapped[str] = mapped_column(pgsql_types.VARCHAR(150), nullable=False)
product_id: Mapped[int] = mapped_column(
- ForeignKey('productcategory.id', ondelete='CASCADE'),
+ ForeignKey("productcategory.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
@@ -96,7 +96,7 @@ class Info(Base):
question_type: Mapped[QuestionEnum] = mapped_column(
pgsql_types.ENUM(
- QuestionEnum, name='question_enum', create_type=False
+ QuestionEnum, name="question_enum", create_type=False
),
nullable=False,
)
@@ -139,16 +139,18 @@ class ContactManager(Base):
class Feedback(Base):
+ """Бд модель обратной связи."""
+
user: Mapped[int] = mapped_column(
ForeignKey("user.id", ondelete="CASCADE"), nullable=False
)
- rating: Mapped[int] = mapped_column(
- pgsql_types.INTEGER, nullable=False
- )
+
+ rating: Mapped[int] = mapped_column(pgsql_types.INTEGER, nullable=False)
+
feedback_text: Mapped[str] = mapped_column(
pgsql_types.TEXT, nullable=False
)
+
feedback_date: Mapped[datetime] = mapped_column(
pgsql_types.TIMESTAMP, default=datetime.now
)
- unread: Mapped[bool] = mapped_column(pgsql_types.BOOLEAN, default=True)
diff --git a/app/scripts_for_db.py b/app/scripts_for_db.py
index b40eb89..61d3921 100644
--- a/app/scripts_for_db.py
+++ b/app/scripts_for_db.py
@@ -1,13 +1,13 @@
-'''
+"""
Скрипт тестовых данных для ветки информация о компании:
INSERT INTO InformationAboutCompany (name, url)
VALUES
('Презентация компании', 'https://scid.ru/'),
('Карточка компании', 'https://scid.ru/contacts');
-'''
+"""
-'''
+"""
Скрипт для ветки узнать о продуктах и услугах:
INSERT INTO ProductCategory (title, response) VALUES
@@ -17,9 +17,9 @@
('Консультация по КИОСК365', 'Текст для консультации по КИОСК365'),
('"НБП ЕЖА"', 'Текст для НБП ЕЖА'),
('Хостинг', 'Текст для хостинга');
-'''
+"""
-'''
+"""
Скрипт для тестовых данных в ветку получить техю поддержку
INSERT INTO Info (question_type, question, answer) VALUES
@@ -27,9 +27,9 @@
('GENERAL_QUESTIONS', 'Как я могу связаться с поддержкой?', 'Вы можете связаться с поддержкой через нашу форму обратной связи или по телефону.'),
('PROBLEMS_WITH_PRODUCTS', 'Что делать, если продукт неисправен?', 'Если продукт неисправен, пожалуйста, свяжитесь с поддержкой, и мы организуем замену или возврат.'),
('PROBLEMS_WITH_PRODUCTS', 'Почему мой продукт не включается?', 'Убедитесь, что устройство заряжено, и проверьте кнопку включения.');
-'''
+"""
-'''
+"""
Скрипт тестовых данных для ветки узнать о продуктах и услугах
INSERT INTO categorytype (name, product_id, url, media)
@@ -40,9 +40,9 @@
('Программы лояльности', 2, 'https://www.airlinesoftware.com/loyalty-solutions', 'adasdad'),
('Порталы для госучереждений', 2, 'https://www.egov.kz', 'adadasda'),
('Личные кабинеты', 2, 'https://my.gov.ru', 'ghfhfgh');
-'''
+"""
-'''
+"""
Скрипт тестовыхх данных для ветки посмотреть портфолио
INSERT INTO checkcompanyportfolio (project_name, url) VALUES
@@ -51,4 +51,4 @@
('Project Gamma', 'https://example.com/project-gamma'),
('Project Delta', 'https://example.com/project-delta'),
('Project Epsilon', 'https://example.com/project-epsilon');
-'''
+"""
diff --git a/makefile b/makefile
new file mode 100644
index 0000000..138e42b
--- /dev/null
+++ b/makefile
@@ -0,0 +1,30 @@
+PYTHON = python
+APP = app/main.py
+
+
+install:
+ poetry install
+
+install-package:
+ poetry add $(PACKAGE)
+
+update:
+ poetry update
+
+run:
+ poetry run $(PYTHON) $(APP)
+
+clean:
+ find . -name "*.pyc" -delete
+
+format:
+ poetry run black .
+
+migrate:
+ alembic upgrade head
+
+downgrade:
+ alembic downgrade -1
+
+makemigration:
+ alembic revision --autogenerate -m "$(msg)"
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index d3d2e07..a9c1ee3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -240,6 +240,25 @@ files = [
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
+[[package]]
+name = "arrow"
+version = "1.3.0"
+description = "Better dates & times for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"},
+ {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"},
+]
+
+[package.dependencies]
+python-dateutil = ">=2.7.0"
+types-python-dateutil = ">=2.8.10"
+
+[package.extras]
+doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"]
+test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"]
+
[[package]]
name = "asyncpg"
version = "0.29.0"
@@ -313,6 +332,50 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
+[[package]]
+name = "black"
+version = "24.10.0"
+description = "The uncompromising code formatter."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
+ {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
+ {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
+ {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
+ {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
+ {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
+ {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
+ {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
+ {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
+ {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
+ {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
+ {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
+ {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
+ {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
+ {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
+ {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
+ {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"},
+ {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"},
+ {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"},
+ {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"},
+ {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
+ {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
+]
+
+[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.10)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
[[package]]
name = "certifi"
version = "2024.8.30"
@@ -324,6 +387,31 @@ files = [
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
]
+[[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"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
[[package]]
name = "frozenlist"
version = "1.4.1"
@@ -510,6 +598,38 @@ files = [
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+[[package]]
+name = "jinja2"
+version = "3.1.4"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
+ {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "jinja2-time"
+version = "0.2.0"
+description = "Jinja2 Extension for Dates and Times"
+optional = false
+python-versions = "*"
+files = [
+ {file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"},
+ {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"},
+]
+
+[package.dependencies]
+arrow = "*"
+jinja2 = "*"
+
[[package]]
name = "magic-filter"
version = "1.0.12"
@@ -524,6 +644,20 @@ files = [
[package.extras]
dev = ["black (>=22.8.0,<22.9.0)", "flake8 (>=5.0.4,<5.1.0)", "isort (>=5.11.5,<5.12.0)", "mypy (>=1.4.1,<1.5.0)", "pre-commit (>=2.20.0,<2.21.0)", "pytest (>=7.1.3,<7.2.0)", "pytest-cov (>=3.0.0,<3.1.0)", "pytest-html (>=3.1.1,<3.2.0)", "types-setuptools (>=65.3.0,<65.4.0)"]
+[[package]]
+name = "make"
+version = "0.1.6.post2"
+description = "Create project layout from jinja2 templates."
+optional = false
+python-versions = "*"
+files = [
+ {file = "make-0.1.6.post2-py3-none-any.whl", hash = "sha256:307991f0d24668b7785a9abade301ba6c2d004460e90c59baf19b47c16b8ed39"},
+]
+
+[package.dependencies]
+Jinja2 = "*"
+jinja2-time = "*"
+
[[package]]
name = "mako"
version = "1.3.5"
@@ -713,6 +847,55 @@ files = [
{file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"},
]
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "packaging"
+version = "24.1"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
+ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.6"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
+ {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
+]
+
+[package.extras]
+docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
+type = ["mypy (>=1.11.2)"]
+
[[package]]
name = "pydantic"
version = "2.9.2"
@@ -857,6 +1040,20 @@ azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0
toml = ["tomli (>=2.0.1)"]
yaml = ["pyyaml (>=6.0.1)"]
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+ {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
[[package]]
name = "python-dotenv"
version = "1.0.1"
@@ -871,6 +1068,17 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
[[package]]
name = "sqlalchemy"
version = "2.0.35"
@@ -958,6 +1166,17 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3_binary"]
+[[package]]
+name = "types-python-dateutil"
+version = "2.9.0.20241003"
+description = "Typing stubs for python-dateutil"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"},
+ {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"},
+]
+
[[package]]
name = "typing-extensions"
version = "4.12.2"
@@ -1077,4 +1296,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
-content-hash = "6894b39621ead657410e8a9e3ee706ec3f958f130d7857bc3f3ddce1fd831a8a"
+content-hash = "f2797bb2cd7752c7ad4f6fb5bfbf2141649c845fe157a8d7204f7cf070b24c3f"
diff --git a/pyproject.toml b/pyproject.toml
index b402792..275bcd1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,6 +15,8 @@ sqlalchemy = "^2.0.35"
pydantic-settings = "^2.5.2"
asyncpg = "^0.29.0"
aiosmtplib = "^3.0.2"
+make = "^0.1.6.post2"
+black = "^24.10.0"
[tool.poetry.group.dev.dependencies]
aiosqlite = "^0.20.0"
From 7a572c34806f92490c9022e633dc3c8280b7be29 Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Tue, 15 Oct 2024 15:55:48 +0300
Subject: [PATCH 52/75] remove #
---
app/core/settings.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/app/core/settings.py b/app/core/settings.py
index 90b91f4..5bc003e 100644
--- a/app/core/settings.py
+++ b/app/core/settings.py
@@ -11,13 +11,13 @@ class Settings(BaseSettings):
database_url: str
bot_token: str
telegram_chat_ids: str
- # email: str
- # email_password: str
- # postgres_user: str
- # postgres_password: str
- # postgres_db: str
- # db_host: str
- # db_port: str
+ email: str
+ email_password: str
+ postgres_user: str
+ postgres_password: str
+ postgres_db: str
+ db_host: str
+ db_port: str
class Config:
env_file = ".env"
From affb4472fbcac412f4372aaa55bbce17920c0978 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Tue, 15 Oct 2024 16:03:03 +0300
Subject: [PATCH 53/75] done most part
---
app/admin/admin_managers/base_manager.py | 1 +
app/admin/admin_managers/create_manager.py | 1 +
app/admin/admin_managers/delete_manager.py | 1 +
app/admin/admin_managers/question_manager.py | 19 +++
app/admin/admin_managers/update_manager.py | 1 +
.../admin_handlers/admin_special_handlers.py | 133 ++++++++++++++----
.../admin_handlers/superuser_handlers.py | 53 ++++++-
app/admin/keyboards/keyboards.py | 27 +---
...ection.py => e8b13ba333a5_first_commit.py} | 8 +-
9 files changed, 187 insertions(+), 57 deletions(-)
rename app/alembic/versions/{53d69ac71b30_add_manager_and_user_connection.py => e8b13ba333a5_first_commit.py} (97%)
diff --git a/app/admin/admin_managers/base_manager.py b/app/admin/admin_managers/base_manager.py
index db83bb8..c773d89 100644
--- a/app/admin/admin_managers/base_manager.py
+++ b/app/admin/admin_managers/base_manager.py
@@ -16,6 +16,7 @@ class BaseAdminManager(ABC):
Attributes:
model_crud (CRUDBase): Объект для выполнения операций CRUD с моделью.
back_option (str): Данные для возврата в меню.
+ staes_group (StaesGroup): Набор машинных состояний
"""
def __init__(
diff --git a/app/admin/admin_managers/create_manager.py b/app/admin/admin_managers/create_manager.py
index 2cfedb7..e32b76e 100644
--- a/app/admin/admin_managers/create_manager.py
+++ b/app/admin/admin_managers/create_manager.py
@@ -35,6 +35,7 @@ class CreateManager(BaseAdminManager):
Attributes:
model_crud (CRUDBase): Объект для выполнения операций CRUD с моделью.
back_option (str): Данные для возврата в меню.
+ staes_group (StaesGroup): Набор машинных состояний
Methods:
select_data_type(callback: CallbackQuery, state: FSMContext):
diff --git a/app/admin/admin_managers/delete_manager.py b/app/admin/admin_managers/delete_manager.py
index 6da9a76..74abaa4 100644
--- a/app/admin/admin_managers/delete_manager.py
+++ b/app/admin/admin_managers/delete_manager.py
@@ -31,6 +31,7 @@ class DeleteManager(BaseAdminManager):
Attributes:
model_crud (CRUDBase): Объект для выполнения операций CRUD с моделью.
back_option (str): Данные для возврата в меню.
+ staes_group (StaesGroup): Набор машинных состояний
Methods:
diff --git a/app/admin/admin_managers/question_manager.py b/app/admin/admin_managers/question_manager.py
index ef1dd31..97cef68 100644
--- a/app/admin/admin_managers/question_manager.py
+++ b/app/admin/admin_managers/question_manager.py
@@ -29,6 +29,10 @@ class DeleteQuestionStates(StatesGroup):
class QuestionBaseManager(ABC):
+ """
+ Базовый класс для управления вопросами.
+ Определяет основные методы для работы с вопросами и их типами.
+ """
async def set_question_type(self, state: FSMContext):
state = await state.get_state()
@@ -38,6 +42,10 @@ async def set_question_type(self, state: FSMContext):
class QuestionUpdateDeleteBase(QuestionBaseManager, ABC):
+ """
+ Базовый класс для управления обновлением и удалением вопросов.
+ """
+
async def get_question_list(self, session: AsyncSession):
return [
question.question
@@ -65,6 +73,10 @@ async def select_question(
class QuestionCreateManager(QuestionBaseManager):
+ """
+ Менеджер для создания вопросов.
+ """
+
async def add_question_text(
self, callback: CallbackQuery, state: FSMContext
):
@@ -121,6 +133,10 @@ async def add_question_to_db(
class QuestionUpdateManager(QuestionUpdateDeleteBase):
+ """
+ Менеджер для обновления вопросов.
+ """
+
async def update_data_type(
self, callback: CallbackQuery, session: AsyncSession
):
@@ -172,6 +188,9 @@ async def update_question_in_db(
class QuestionDeleteManager(QuestionUpdateDeleteBase):
+ """
+ Менеджер для удаления вопросов.
+ """
async def confirm_delete(
self,
diff --git a/app/admin/admin_managers/update_manager.py b/app/admin/admin_managers/update_manager.py
index 14da17a..34f4557 100644
--- a/app/admin/admin_managers/update_manager.py
+++ b/app/admin/admin_managers/update_manager.py
@@ -39,6 +39,7 @@ class UpdateManager(BaseAdminManager):
Attributes:
model_crud (CRUDBase): Объект для выполнения операций CRUD с моделью.
back_option (str): Данные для возврата в меню.
+ staes_group (StaesGroup): Набор машинных состояний
Methods:
get_all_model_names(session: AsyncSession) -> list[str]:
diff --git a/app/admin/handlers/admin_handlers/admin_special_handlers.py b/app/admin/handlers/admin_handlers/admin_special_handlers.py
index 742e547..4dcb15d 100644
--- a/app/admin/handlers/admin_handlers/admin_special_handlers.py
+++ b/app/admin/handlers/admin_handlers/admin_special_handlers.py
@@ -7,9 +7,7 @@
from sqlalchemy.ext.asyncio import AsyncSession
from admin.filters.filters import ChatTypeFilter, IsManagerOrAdmin
-from admin.keyboards.keyboards import (
- get_inline_keyboard,
-)
+from admin.keyboards.keyboards import get_inline_keyboard
from admin.admin_settings import (
ADMIN_SPECIAL_BUTTONS,
ADMIN_SPECIAL_OPTIONS,
@@ -17,13 +15,15 @@
MAIN_MENU_OPTIONS,
MAIN_MENU_TEXT,
)
-from models.models import ContactManager
+from bot.exceptions import message_exception_handler
+from models.models import ContactManager, Feedback
from crud.request_to_manager import (
close_case,
get_all_manager_requests,
get_all_support_requests,
get_request,
)
+from crud.feedback_crud import feedback_crud
logger = logging.getLogger(__name__)
@@ -34,6 +34,13 @@
admin_special_router.callback_query.filter(IsManagerOrAdmin())
+PREVIOUS_MENU = MAIN_MENU_OPTIONS.get("admin_special")
+
+
+class FeedbackState(StatesGroup):
+ feedback = State()
+
+
class RequestState(StatesGroup):
manager_request = State()
support_request = State()
@@ -45,7 +52,7 @@ async def get_state_name(state: str) -> str:
return ADMIN_SPECIAL_OPTIONS.get(state_name)
-async def get_requests_list(
+async def get_requests_data(
request_list: list[ContactManager],
) -> tuple[list[str]]:
options = [
@@ -59,6 +66,23 @@ async def get_requests_list(
return options, request_ids
+async def get_feedbacks_data(
+ feedback_list: list[Feedback],
+) -> tuple[list[str]]:
+ options = [
+ (
+ f"Отзыв {feedback.id} от "
+ f"{feedback.feedback_date.strftime(DATETIME_FORMAT)}"
+ )
+ for feedback in feedback_list
+ ]
+ request_ids = [feedback.id for feedback in feedback_list]
+ return options, request_ids
+
+
+@message_exception_handler(
+ log_error_text="Ошибка при открытии админского меню"
+)
@admin_special_router.callback_query(
F.data == MAIN_MENU_OPTIONS.get("admin_special")
)
@@ -69,8 +93,12 @@ async def get_admin_special_options(callback: CallbackQuery):
ADMIN_SPECIAL_BUTTONS, previous_menu=MAIN_MENU_TEXT
),
)
+ logger.info(f"Пользователь {callback.from_user.id} открыл админское меню.")
+@message_exception_handler(
+ log_error_text="Ошибка при получении списка менеджерских заявок"
+)
@admin_special_router.callback_query(
F.data == ADMIN_SPECIAL_OPTIONS.get("manager_request")
)
@@ -79,18 +107,24 @@ async def get_manager_request_list(
):
"""Получить список заявок на обратный звонок от менеджера."""
request_list = await get_all_manager_requests(session)
- options, callbacks = await get_requests_list(request_list)
+ options, callbacks = await get_requests_data(request_list)
await callback.message.edit_text(
ADMIN_SPECIAL_OPTIONS.get("manager_request"),
reply_markup=await get_inline_keyboard(
options=options,
callback=callbacks,
- previous_menu=MAIN_MENU_OPTIONS.get("admin_special"),
+ previous_menu=PREVIOUS_MENU,
),
)
await state.set_state(RequestState.manager_request)
+ logger.info(
+ f"Пользователь {callback.from_user.id} запросил список менеджерских заявок."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при получении списка заявок на техподдержку"
+)
@admin_special_router.callback_query(
F.data == ADMIN_SPECIAL_OPTIONS.get("support_request")
)
@@ -99,18 +133,24 @@ async def get_support_request_list(
):
"""Получить список заявок на техподдержку."""
request_list = await get_all_support_requests(session)
- options, callbacks = await get_requests_list(request_list)
+ options, callbacks = await get_requests_data(request_list)
await callback.message.edit_text(
ADMIN_SPECIAL_OPTIONS.get("support_request"),
reply_markup=await get_inline_keyboard(
options=options,
callback=callbacks,
- previous_menu=MAIN_MENU_OPTIONS.get("admin_special"),
+ previous_menu=PREVIOUS_MENU,
),
)
await state.set_state(RequestState.support_request)
+ logger.info(
+ f"Пользователь {callback.from_user.id} запросил список заявок на техподдержку."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при получении данных о заявке"
+)
@admin_special_router.callback_query(RequestState(), F.data.isnumeric())
async def get_request_data(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
@@ -131,27 +171,72 @@ async def get_request_data(
),
)
await state.update_data(request_id=callback.data)
+ logger.info(
+ f"Пользователь {callback.from_user.id} запросил данные по заявке {request.id}."
+ )
+@message_exception_handler(log_error_text="Ошибка при закрытии заявки")
@admin_special_router.callback_query(RequestState(), F.data == "Закрыть")
async def close_request(
callback: CallbackQuery, state: FSMContext, session: AsyncSession
):
"""Закрыть заявку."""
- try:
- current_state = await state.get_state()
- back_option = await get_state_name(current_state)
- fsm_data = await state.get_data()
- request_id = fsm_data.get("request_id")
- await close_case(callback.from_user.id, request_id, session)
- await callback.message.edit_text(
- "Заявка закрыта!",
- reply_markup=await get_inline_keyboard(previous_menu=back_option),
- )
- except Exception as e:
- await callback.message.answer(f"Произошла ошибка: {e}")
+ current_state = await state.get_state()
+ back_option = await get_state_name(current_state)
+ fsm_data = await state.get_data()
+ request_id = fsm_data.get("request_id")
+ await close_case(callback.from_user.id, request_id, session)
+ await callback.message.edit_text(
+ "Заявка закрыта!",
+ reply_markup=await get_inline_keyboard(previous_menu=back_option),
+ )
+ logger.info(
+ f"Пользователь {callback.from_user.id} закрыл заявку {request_id}."
+ )
+
+
+@message_exception_handler(log_error_text="Ошибка при получении отзывов")
+@admin_special_router.callback_query(
+ F.data == ADMIN_SPECIAL_OPTIONS.get("feedbacks")
+)
+async def get_feedbacks(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Получить все отзывы."""
+ feedbacks = await feedback_crud.get_multi(session)
+ options, callbacks = await get_feedbacks_data(feedbacks)
+ await callback.message.edit_text(
+ ADMIN_SPECIAL_OPTIONS.get("feedbacks"),
+ reply_markup=await get_inline_keyboard(
+ options=options, callback=callbacks, previous_menu=PREVIOUS_MENU
+ ),
+ )
+ await state.set_state(FeedbackState.feedback)
+ logger.info(f"Пользователь {callback.from_user.id} запросил все отзывы.")
-@admin_special_router.callback_query(F.data == ADMIN_SPECIAL_OPTIONS.get("feedbacks"))
-async def get_feedbacks(callback: CallbackQuery, session: AsyncSession)
- ...
+@message_exception_handler(
+ log_error_text="Ошибка при получении содержания отзыва"
+)
+@admin_special_router.callback_query(
+ FeedbackState.feedback, F.data.isnumeric()
+)
+async def get_feedback_content(
+ callback: CallbackQuery, state: FSMContext, session: AsyncSession
+):
+ """Получить содержание отзыва."""
+ feedback = await feedback_crud.get(callback.data, session)
+ message_text = (
+ f"{feedback.feedback_text} \n\n Оценка: {feedback.rating} \n\n"
+ f" Дата отзыва: {feedback.feedback_date.strftime(DATETIME_FORMAT)}"
+ )
+ await callback.message.edit_text(
+ message_text,
+ reply_markup=await get_inline_keyboard(
+ previous_menu=ADMIN_SPECIAL_OPTIONS.get("feedbacks")
+ ),
+ )
+ logger.info(
+ f"Пользователь {callback.from_user.id} запросил содержание отзыва {feedback.id}."
+ )
diff --git a/app/admin/handlers/admin_handlers/superuser_handlers.py b/app/admin/handlers/admin_handlers/superuser_handlers.py
index ed23afd..7d0f401 100644
--- a/app/admin/handlers/admin_handlers/superuser_handlers.py
+++ b/app/admin/handlers/admin_handlers/superuser_handlers.py
@@ -20,6 +20,7 @@
SUPERUSER_SPECIAL_BUTTONS,
SUPERUSER_SPECIAL_OPTIONS,
)
+from bot.exceptions import message_exception_handler
from models.models import User, RoleEnum
from crud.request_to_manager import get_manager_stats
from crud.user_crud import user_crud
@@ -58,6 +59,9 @@ async def check_user_tg_id_data(
return user, message_text
+@message_exception_handler(
+ log_error_text="Ошибка при открытии меню суперпользователя"
+)
@superuser_router.callback_query(
F.data == MAIN_MENU_OPTIONS.get("admin_special")
)
@@ -72,8 +76,14 @@ async def get_admin_special_options(
SUPERUSER_SPECIAL_BUTTONS, previous_menu=MAIN_MENU_TEXT
),
)
+ logger.info(
+ f"Пользователь {callback.from_user.id} открыл меню суперпользователя."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при переходе в меню управления персоналом"
+)
@superuser_router.callback_query(
F.data == SUPERUSER_SPECIAL_OPTIONS.get("promotion")
)
@@ -86,8 +96,14 @@ async def get_superuser_options(callback: CallbackQuery):
previous_menu=PREVIOUS_MENU,
),
)
+ logger.info(
+ f"Пользователь {callback.from_user.id} открыл меню управления персоналом."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при получении списка менеджеров"
+)
@superuser_router.callback_query(
F.data == SUPERUSER_PROMOTION_OPTIONS.get("manager_list")
)
@@ -107,11 +123,17 @@ async def get_manager_list(
),
)
await state.set_state(manager)
+ logger.info(
+ f"Пользователь {callback.from_user.id} запросил список менеджеров."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при получении информации о менеджере"
+)
@superuser_router.callback_query(manager, F.data.isnumeric())
async def get_manager(callback: CallbackQuery, session: AsyncSession):
- """Получить информацио о менеджере."""
+ """Получить информацию о менеджере."""
manager = await user_crud.get_user_by_tg_id(callback.data, session)
cases_count, last_case_closed = await get_manager_stats(
callback.data, session
@@ -122,7 +144,7 @@ async def get_manager(callback: CallbackQuery, session: AsyncSession):
f"{last_case_closed.shipping_date_close.strftime(DATETIME_FORMAT)}"
)
if last_case_closed
- else "закртых заявок пока нет."
+ else "закрытых заявок пока нет."
)
message_text = (
f"Менеджер {manager.name}\n\n "
@@ -134,8 +156,14 @@ async def get_manager(callback: CallbackQuery, session: AsyncSession):
message_text,
reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
)
+ logger.info(
+ f"Пользователь {callback.from_user.id} запросил информацию о менеджере {manager.name}."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при вводе Telegram ID для смены роли"
+)
@superuser_router.callback_query(
or_f(
F.data == SUPERUSER_PROMOTION_OPTIONS.get("promote"),
@@ -152,8 +180,14 @@ async def get_user_id_for_action(callback: CallbackQuery, state: FSMContext):
"Введите id телеграм-пользователя:",
reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
)
+ logger.info(
+ f"Пользователь {callback.from_user.id} выбрал действие для смены роли."
+ )
+@message_exception_handler(
+ log_error_text="Ошибка при понижении роли пользователя"
+)
@superuser_router.message(RoleState.demote, F.text.isnumeric())
async def demote_to_user(message: Message, session: AsyncSession):
"""Проверить пользователя и присвоить ему роль USER."""
@@ -162,15 +196,19 @@ async def demote_to_user(message: Message, session: AsyncSession):
message_text = error_text
else:
message_text = (
- f"Позльователь {message.text} теперь просто пользователь!"
+ f"Пользователь {message.text} теперь просто пользователь!"
)
await user_crud.demote_to_user(user, session)
await message.answer(
message_text,
reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
)
+ logger.info(f"Пользователь {message.text} понижен до роли USER.")
+@message_exception_handler(
+ log_error_text="Ошибка при добавлении имени для менеджера"
+)
@superuser_router.message(RoleState.promote, F.text.isnumeric())
async def add_name_to_manager(message: Message, state: FSMContext):
"""Добавить имя для менеджера."""
@@ -180,8 +218,12 @@ async def add_name_to_manager(message: Message, state: FSMContext):
"Введите имя для менеджера",
reply_markup=await get_inline_keyboard(previous_menu=PREVIOUS_MENU),
)
+ logger.info(
+ f"Пользователь {message.text} готовится к назначению роли менеджера."
+ )
+@message_exception_handler(log_error_text="Ошибка при назначении менеджера")
@superuser_router.message(RoleState.name, F.text)
async def check_user_and_make_him_manager(
message: Message, state: FSMContext, session: AsyncSession
@@ -200,8 +242,11 @@ async def check_user_and_make_him_manager(
else:
await user_crud.promote_to_manager(user, message.text, session)
await message.answer(
- "Менеджер добален!",
+ "Менеджер добавлен!",
reply_markup=await get_inline_keyboard(
previous_menu=PREVIOUS_MENU
),
)
+ logger.info(
+ f"Пользователь {user_tg_id} назначен менеджером с именем {message.text}."
+ )
diff --git a/app/admin/keyboards/keyboards.py b/app/admin/keyboards/keyboards.py
index ebf66a2..01803e9 100644
--- a/app/admin/keyboards/keyboards.py
+++ b/app/admin/keyboards/keyboards.py
@@ -1,10 +1,5 @@
-from aiogram.types import (
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- KeyboardButton,
-)
-from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
+from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
+from aiogram.utils.keyboard import InlineKeyboardBuilder
class InlineKeyboardManager:
@@ -99,24 +94,6 @@ async def get_inline_keyboard(
).create_keyboard()
-async def get_reply_keyboard(
- options: list[str] | str | None = None,
- size: tuple[int] = (1,),
-) -> ReplyKeyboardMarkup:
- """Создать экранную клавиатуру."""
-
- keyboard = ReplyKeyboardBuilder()
-
- if options:
- if isinstance(options, list):
- for option in options:
- keyboard.add(KeyboardButton(text=option, callback_data=option))
- else:
- keyboard.add(KeyboardButton(text=options))
-
- return keyboard.adjust(*size).as_markup()
-
-
async def get_delete_message_keyboard() -> InlineKeyboardMarkup:
"""Создать копку для удаления сообщения."""
diff --git a/app/alembic/versions/53d69ac71b30_add_manager_and_user_connection.py b/app/alembic/versions/e8b13ba333a5_first_commit.py
similarity index 97%
rename from app/alembic/versions/53d69ac71b30_add_manager_and_user_connection.py
rename to app/alembic/versions/e8b13ba333a5_first_commit.py
index 782ac41..b336f4d 100644
--- a/app/alembic/versions/53d69ac71b30_add_manager_and_user_connection.py
+++ b/app/alembic/versions/e8b13ba333a5_first_commit.py
@@ -1,8 +1,8 @@
-"""add manager and user connection
+"""first commit
-Revision ID: 53d69ac71b30
+Revision ID: e8b13ba333a5
Revises:
-Create Date: 2024-10-15 09:50:55.008906
+Create Date: 2024-10-15 13:59:59.402203
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
-revision: str = '53d69ac71b30'
+revision: str = 'e8b13ba333a5'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
From 439a1996d8a3476ec7c8a646e16b3657b1cfab39 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Tue, 15 Oct 2024 16:17:00 +0300
Subject: [PATCH 54/75] comment yml
---
.github/workflows/main.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5eb27cd..cc6b8bf 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -4,9 +4,9 @@ on:
branches:
- master
- dev # закомментить перед выходом в прод
- pull_request:
- branches:
- - dev
+ # pull_request:
+ # branches:
+ # - dev
jobs:
linter:
runs-on: ubuntu-latest
From 8a9df320a21836f6cf9da60d5cb5a1d770cfe573 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Tue, 15 Oct 2024 18:40:03 +0300
Subject: [PATCH 55/75] test autodeploy
---
.github/workflows/main.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5eb27cd..3555da3 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -4,9 +4,9 @@ on:
branches:
- master
- dev # закомментить перед выходом в прод
- pull_request:
- branches:
- - dev
+# pull_request:
+# branches:
+# - dev
jobs:
linter:
runs-on: ubuntu-latest
From 3950a270d46020f30a727114b2079285e704ee2d Mon Sep 17 00:00:00 2001
From: Vladimir Mironov
Date: Tue, 15 Oct 2024 21:04:33 +0300
Subject: [PATCH 56/75] 12
---
.github/workflows/main.yml | 81 ++++++++++++++++++++++++++++++++++++++
1 file changed, 81 insertions(+)
create mode 100644 .github/workflows/main.yml
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..81be121
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,81 @@
+name: SCID 3 Bot Workflow
+on:
+ push:
+ branches:
+ - master
+ - dev # закомментить перед выходом в прод
+# pull_request:
+# branches:
+# - dev
+jobs:
+ linter:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.12'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install flake8
+ - name: Test with flake8
+ run: python -m flake8 app/
+ build_and_push_to_docker_hub:
+ name: Push Docker image to DockerHub
+ runs-on: ubuntu-latest
+ needs: linter
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v3
+ - name: Set up Docker Build
+ uses: docker/setup-buildx-action@v2
+ - name: Login to Docker
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+ - name: Push to DockerHub
+ uses: docker/build-push-action@v4
+ with:
+ push: true
+ tags: greenvibe/scid_bot_3:latest
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build_and_push_to_docker_hub
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v3
+ - name: Copy docker-compose.yml via ssh
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.HOST }}
+ username: ${{ secrets.USER }}
+ key: ${{ secrets.SSH_KEY }}
+ passphrase: ${{ secrets.SSH_PASSPHRASE }}
+ source: "docker-compose.yml"
+ target: "scid_bot"
+ - name: Executing remote ssh commands to deploy
+ uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.HOST }}
+ username: ${{ secrets.USER }}
+ key: ${{ secrets.SSH_KEY }}
+ passphrase: ${{ secrets.SSH_PASSPHRASE }}
+ script: |
+ cd scid_bot
+ sudo docker compose -f docker-compose.yml pull
+ sudo docker compose -f docker-compose.yml down
+ sudo docker compose -f docker-compose.yml up -d
+ send_message:
+ runs-on: ubuntu-latest
+ needs: deploy
+ steps:
+ - name: Send message
+ uses: appleboy/telegram-action@master
+ with:
+ to: ${{ secrets.TELEGRAM_TO }}
+ token: ${{ secrets.TELEGRAM_TOKEN }}
+ message: Деплой успешно выполнен!
From 9c0359a554b5f53d8245c35b509ac487bf766bfb Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Tue, 15 Oct 2024 23:53:35 +0300
Subject: [PATCH 57/75] fix build in maim.yml
---
.github/workflows/main.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 81be121..d545ccf 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -40,6 +40,7 @@ jobs:
- name: Push to DockerHub
uses: docker/build-push-action@v4
with:
+ context: .
push: true
tags: greenvibe/scid_bot_3:latest
deploy:
From a2d2e3623438ce65af5a8c8e35e6e7ad6ba62d1c Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <142910967+Rxyalxrd@users.noreply.github.com>
Date: Wed, 16 Oct 2024 01:01:19 +0300
Subject: [PATCH 58/75] Update README.md
---
README.md | 203 +++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 187 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index 0df89bc..65410e9 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,193 @@
-### Установка:
-- клонируем директорию с гитхаба
-- устанавливаем poetry из любой директории: pip install poetry --user
-- переходим в папку проекта и устанавливаем зависимости: poetry install
-- создаём файл .env и добавляем строку BOT_TOKEN='<токен бота>'
+# Чат-бот для предоставления информации, помощи в обслуживании клиентов, проведения маркетинговых кампаний.
+**SCID** — это цифровое агентство полного цикла, основанное в 2008 году. Оно занимается созданием IT-решений, которые помогают компаниям оптимизировать бизнес-процессы и улучшить эффективность. Среди ключевых услуг агентства — разработка сайтов, мобильных приложений, CRM-систем и систем бизнес-аналитики (BI).
-### Директории:
-- app/bot - основная логика работы бота
-- app/admin - админка бота
-- app/exceptions - вызовы возможных ошибок
-- pyproject - файл виртуального окружения
-- poetry - файл с зависимостями
+**Основные направления деятельности SCID включают:**
+ 1. Разработку программного обеспечения, включая системы для управления бизнесом;
+ 2. Создание сайтов и мобильных приложений для различных отраслей, включая медицину, образование и ритейл;
+ 3. Интеграцию с корпоративными системами и разработку сложных CRM-систем;
+ 4. Внедрение бизнес-аналитики (BI) для компаний с помощью современных цифровых инструментов.
+ 5. SCID также работает с крупнейшими представителями бизнеса и государственных структур, успешно реализовав более 250 проектов для разных отраслей, что делает их экспертом в создании индивидуальных IT-решений для компаний всех размеров.
-### Как работаем:
-- работает каждый в своей ветке, лучше назвать по именам разработчиков, чтоб не путаться, ветки наследуем от dev
-- пушим через pull request в dev
+Благодаря многолетнему опыту и глубокому пониманию специфики цифровых технологий, агентство создает проекты, ориентированные на решение конкретных задач клиентов, будь то оптимизация логистики, внедрение систем управления ресурсами или улучшение пользовательского опыта.
+Для более детальной информации можно посетить официальный сайт компании: [SCID](https://scid.ru/).
-### Ссылка на бота:
-https://t.me/csid_inform_bot
+---
+
+## Стек технологий
+
+В данном проекте используется современный стек технологий, который обеспечивает эффективную разработку, масштабируемость и удобство в использовании. Ниже представлены основные библиотеки и инструменты, использованные в проекте:
+
+- **Python**: Основной язык программирования проекта, известный своей простотой и читаемостью, что способствует быстрой разработке.
+
+- **Aiogram**: Асинхронная библиотека для создания Telegram-ботов, обеспечивающая простоту интеграции и работы с `API Telegram`.
+
+- **Pydantic**: Библиотека для валидации данных и работы с настройками на основе аннотаций типов Python, что улучшает надежность кода.
+
+- **Python-dotenv**: Позволяет загружать переменные окружения из файла `.env`, упрощая управление конфигурацией приложения.
+
+- **Alembic**: Инструмент для управления миграциями базы данных, совместимый с `SQLAlchemy`, что облегчает версионирование схемы базы данных.
+
+- **SQLAlchemy**: `ORM (Object Relational Mapping)` библиотека для работы с реляционными базами данных, предоставляющая высокоуровневый интерфейс для взаимодействия с базами данных.
+
+- **Pydantic-settings**: Расширение `Pydantic` для управления настройками приложений с поддержкой файлов конфигурации и переменных окружения.
+
+- **Asyncpg**: Асинхронный драйвер для `PostgreSQL`, который обеспечивает высокую производительность и эффективное взаимодействие с базой данных.
+
+- **Aiosmtplib**: Асинхронная библиотека для отправки электронной почты через `SMTP`, позволяющая интегрировать почтовые функции в приложение.
+
+- **Make**: Инструмент для автоматизации задач, упрощающий процессы установки зависимостей и запуска различных команд.
+
+- **Black**: Форматировщик кода, который помогает поддерживать код в чистом и читаемом состоянии, следуя единым стандартам оформления.
+
+Этот стек технологий был выбран для обеспечения надежности, производительности и удобства разработки, что позволяет сосредоточиться на реализации бизнес-логики приложения.
+
+---
+
+## .env
+
+В проекте используются переменные окружения для конфигурации подключения к различным сервисам, таким как Telegram, база данных, и почтовый сервер. Вам необходимо создать файл `.env` на основе примера `.env.example` и заполнить его актуальными данными.
+
+### Шаги для настройки
+
+1. **Скопируйте файл `.env.example` в `.env`:**
+
+```bash
+cp .env.example .env
+```
+
+### Заполните файл `.env` своими данными
+
+2. **Откройте файл `.env` в любом текстовом редакторе и замените значения в угловых скобках на актуальные данные.**
+
+### Описание переменных окружения
+
+- **`TELEGRAM_TOKEN`**: Токен вашего Telegram-бота. Получите его у [BotFather](https://t.me/BotFather).
+
+- **`TELEGRAM_CHAT_IDS`**: Список ID чатов Telegram, которые получат статус администратора. Эти ID можно получить после настройки бота.
+
+- **`DATABASE_URL`**: Полный URL для подключения к базе данных PostgreSQL. Пример:
+ ```bash
+ postgresql://:@:/
+
+ POSTGRES_USER: Имя пользователя, который является владельцем базы данных.
+
+- **`POSTGRES_PASSWORD`**: Пароль для подключения к базе данных.
+
+- **`POSTGRES_DB`**: Название базы данных, к которой происходит подключение.
+
+- **`DB_HOST`**: Хост базы данных (например, localhost или IP-адрес сервера).
+
+- **`DB_PORT`**: Порт для подключения к базе данных (обычно 5432 для PostgreSQL).
+
+- **`EMAIL`**: Адрес электронной почты, который будет использоваться для отправки сообщений менеджеру.
+
+- **`EMAIL_PASSWORD`**: Пароль для доступа к электронной почте, с которой будут отправляться сообщения.
+
+3. **Пример заполненного файла `.env`:**
+```bash
+ TELEGRAM_TOKEN=123456789:ABCdefGhijklMNOpqrstuvwxyz
+ TELEGRAM_CHAT_IDS=123456789,987654321
+
+ DATABASE_URL=postgresql://dbuser:password@localhost:5432/mydatabase
+ POSTGRES_USER=dbuser
+ POSTGRES_PASSWORD=password
+ POSTGRES_DB=mydatabase
+ DB_HOST=localhost
+ DB_PORT=5432
+
+ EMAIL=manager@example.com
+ EMAIL_PASSWORD=your-email-password
+ ```
+---
+
+## Как использовать Makefile
+
+Makefile предоставляет команды для упрощения управления зависимостями, миграциями базы данных и запуском приложения. Вот описание каждой команды:
+
+### 1. Установка зависимостей
+Команда устанавливает зависимости, указанные в `pyproject.toml`, с помощью `Poetry`.
+
+**Использование:**
+```bash
+make install
+```
+
+### 2. Установка нового пакета
+Команда добавляет новый пакет в проект с помощью `Poetry`. Нужно передать переменную `PACKAGE`, указывающую имя пакета.
+
+**Использование:**
+```bash
+make install-package PACKAGE=<имя_пакета>
+```
+
+### 3. Обновление зависимостей
+Команда обновляет все зависимости до последних версий с помощью `Poetry`.
+
+**Использование:**
+```bash
+make update
+```
+
+### 4. Запуск приложения
+Команда запускает основное приложение, используя переменные `PYTHON` (интерпретатор) и `APP` (главный файл приложения).
+
+**Использование:**
+```bash
+make run
+```
+
+### 5. Очистка .pyc файлов
+Команда удаляет все файлы .pyc, которые создаются при компиляции Python. Это помогает очистить проект от временных файлов.
+
+**Использование:**
+```bash
+make clean
+```
+
+### 6. Форматирование кода
+Команда форматирует весь код проекта по стандарту black.
+
+**Использование:**
+```bash
+make format
+```
+
+### 7. Применение миграций
+Команда применяет все доступные миграции к базе данных с помощью Alembic.
+
+**Использование:**
+```bash
+make migrate
+```
+
+### 8. Откат миграций
+Команда откатывает последнюю примененную миграцию базы данных с помощью Alembic.
+
+**Использование:**
+```bash
+make downgrade
+```
+
+### 9. Создание миграции
+Команда создает новую миграцию на основе изменений в моделях базы данных. Необходимо указать описание изменений в переменной msg.
+
+**Использование:**
+```bash
+make makemigration msg="описание изменений"
+```
+
+### 10. Docker Compose команда
+Команда выполняет следующие действия для работы с Docker Compose:
+
+- Проставляет текущее состояние миграций: alembic stamp head.
+- Генерирует новую миграцию: alembic revision --autogenerate.
+- Применяет миграции: alembic upgrade head.
+- Запускает основное приложение.
+
+**Использование:**
+```bash
+make docker_compose_command
+```
From 119448d5583504f5cf03c45370eee8ef7815630c Mon Sep 17 00:00:00 2001
From: Green Vibe <105653543+GreenVibesOnly@users.noreply.github.com>
Date: Wed, 16 Oct 2024 02:38:07 +0300
Subject: [PATCH 59/75] Rename Dockerfile
---
DockerFile => Dockerfile | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename DockerFile => Dockerfile (100%)
diff --git a/DockerFile b/Dockerfile
similarity index 100%
rename from DockerFile
rename to Dockerfile
From 5ee6c4929f1b8411f85969d125c5b6b3e8fdf211 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Wed, 16 Oct 2024 10:21:32 +0300
Subject: [PATCH 60/75] fix alembic.ini
---
app/admin/handlers/admin_handlers/superuser_handlers.py | 4 ++--
app/alembic.ini | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/admin/handlers/admin_handlers/superuser_handlers.py b/app/admin/handlers/admin_handlers/superuser_handlers.py
index 7d0f401..4d8151e 100644
--- a/app/admin/handlers/admin_handlers/superuser_handlers.py
+++ b/app/admin/handlers/admin_handlers/superuser_handlers.py
@@ -140,7 +140,7 @@ async def get_manager(callback: CallbackQuery, session: AsyncSession):
)
last_case_message = (
(
- f"{last_case_closed.id} от "
+ f"{last_case_closed.id} "
f"{last_case_closed.shipping_date_close.strftime(DATETIME_FORMAT)}"
)
if last_case_closed
@@ -150,7 +150,7 @@ async def get_manager(callback: CallbackQuery, session: AsyncSession):
f"Менеджер {manager.name}\n\n "
f"Телеграм id: {manager.tg_id} \n\n"
f"Количество закрытых заявок: {cases_count} \n\n"
- f"Номер последней закрытой заявки: {last_case_message}"
+ f"Номер и дата последней закрытой заявки: {last_case_message}"
)
await callback.message.edit_text(
message_text,
diff --git a/app/alembic.ini b/app/alembic.ini
index d8440a8..d4ab1d7 100644
--- a/app/alembic.ini
+++ b/app/alembic.ini
@@ -3,7 +3,7 @@
[alembic]
# path to migration scripts.
# Use forward slashes (/) also on windows to provide an os agnostic path
-script_location = app/alembic
+script_location = alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
@@ -11,7 +11,7 @@ script_location = app/alembic
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
-prepend_sys_path = app
+prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
From 638b8293803f8b5fba80515b9bd641cea4897281 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Wed, 16 Oct 2024 11:10:57 +0300
Subject: [PATCH 61/75] fix models err
---
app/models/models.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/models/models.py b/app/models/models.py
index 205e0e0..fec8d54 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -107,7 +107,6 @@ class Info(Base):
question_type: Mapped[QuestionEnum] = mapped_column(
pgsql_types.ENUM(
QuestionEnum, name="question_enum", create_type=False
- QuestionEnum, name="question_enum", create_type=False
),
nullable=False,
)
From 1209e69765c5ff1286f0966f6f1bba5720763c9e Mon Sep 17 00:00:00 2001
From: ikhit
Date: Wed, 16 Oct 2024 11:28:19 +0300
Subject: [PATCH 62/75] fix alembic
---
app/alembic.ini | 4 +-
...commit.py => 17c3c5e67c14_first_commit.py} | 7 ++--
.../versions/fd795bdbba90_update_feedback.py | 41 -------------------
app/main.py | 2 +-
app/models/models.py | 5 +--
5 files changed, 8 insertions(+), 51 deletions(-)
rename app/alembic/versions/{e8b13ba333a5_first_commit.py => 17c3c5e67c14_first_commit.py} (95%)
delete mode 100644 app/alembic/versions/fd795bdbba90_update_feedback.py
diff --git a/app/alembic.ini b/app/alembic.ini
index ada65d0..578c2ae 100644
--- a/app/alembic.ini
+++ b/app/alembic.ini
@@ -83,7 +83,7 @@ sqlalchemy.url = driver://user:pass@localhost/dbname
keys = root,sqlalchemy,alembic
[handlers]
-keys = generic
+keys = console
[formatters]
keys = generic
@@ -111,4 +111,4 @@ formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
-datefmt = %H:%M:%S
+datefmt = %H:%M:%S
\ No newline at end of file
diff --git a/app/alembic/versions/e8b13ba333a5_first_commit.py b/app/alembic/versions/17c3c5e67c14_first_commit.py
similarity index 95%
rename from app/alembic/versions/e8b13ba333a5_first_commit.py
rename to app/alembic/versions/17c3c5e67c14_first_commit.py
index b336f4d..671fb60 100644
--- a/app/alembic/versions/e8b13ba333a5_first_commit.py
+++ b/app/alembic/versions/17c3c5e67c14_first_commit.py
@@ -1,8 +1,8 @@
"""first commit
-Revision ID: e8b13ba333a5
+Revision ID: 17c3c5e67c14
Revises:
-Create Date: 2024-10-15 13:59:59.402203
+Create Date: 2024-10-16 11:27:25.240035
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
-revision: str = 'e8b13ba333a5'
+revision: str = '17c3c5e67c14'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -63,6 +63,7 @@ def upgrade() -> None:
sa.Column('description', sa.TEXT(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['product_id'], ['productcategory.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_categorytype_product_id'), 'categorytype', ['product_id'], unique=False)
diff --git a/app/alembic/versions/fd795bdbba90_update_feedback.py b/app/alembic/versions/fd795bdbba90_update_feedback.py
deleted file mode 100644
index c6f71cc..0000000
--- a/app/alembic/versions/fd795bdbba90_update_feedback.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""Update Feedback
-
-Revision ID: fd795bdbba90
-Revises:
-Create Date: 2024-10-11 19:56:11.060934
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = "fd795bdbba90"
-down_revision: Union[str, None] = None
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table(
- "feedback",
- sa.Column("user", sa.Integer(), nullable=False),
- sa.Column("rating", sa.INTEGER(), nullable=False),
- sa.Column("feedback_text", sa.TEXT(), nullable=False),
- sa.Column("feedback_date", postgresql.TIMESTAMP(), nullable=False),
- sa.Column("unread", sa.BOOLEAN(), nullable=False),
- sa.Column("id", sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(["user"], ["user.id"], ondelete="CASCADE"),
- sa.PrimaryKeyConstraint("id"),
- )
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table("feedback")
- # ### end Alembic commands ###
diff --git a/app/main.py b/app/main.py
index fbae720..e869d9e 100644
--- a/app/main.py
+++ b/app/main.py
@@ -9,7 +9,7 @@
from bot.callbacks import router as callback_router
from bot.fsm_contexts.manager_context import router as fsm_context_router
from bot.fsm_contexts.feedback_context import router as feedback_context
-from core.init_db import add_portfolio
+from core.init_db import add_portfolio, set_admin
from admin.handlers.admin_handlers import admin_router
from admin.handlers.user import user_router
from loggers.log import setup_logging
diff --git a/app/models/models.py b/app/models/models.py
index fec8d54..83ac44c 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -18,10 +18,7 @@ class RoleEnum(str, Enum):
class QuestionEnum(str, Enum):
GENERAL_QUESTIONS = "Общие вопросы"
PROBLEMS_WITH_PRODUCTS = "Проблемы с продуктами"
- GENERAL_QUESTIONS = "Общие вопросы"
- PROBLEMS_WITH_PRODUCTS = "Проблемы с продуктами"
-
-
+
class User(Base):
"""БД модель пользователя."""
From a480eeb2c7e0eb681e78946f577f8c7e13abbf9f Mon Sep 17 00:00:00 2001
From: ikhit
Date: Wed, 16 Oct 2024 12:11:50 +0300
Subject: [PATCH 63/75] merge check complete
---
app/bot/fsm_contexts/feedback_context.py | 4 ++--
app/crud/feedback.py | 17 -----------------
app/crud/feedback_crud.py | 10 ----------
app/main.py | 8 +++++---
app/test.py | 23 -----------------------
5 files changed, 7 insertions(+), 55 deletions(-)
delete mode 100644 app/crud/feedback.py
delete mode 100644 app/test.py
diff --git a/app/bot/fsm_contexts/feedback_context.py b/app/bot/fsm_contexts/feedback_context.py
index 1222dc7..fcb5d0d 100644
--- a/app/bot/fsm_contexts/feedback_context.py
+++ b/app/bot/fsm_contexts/feedback_context.py
@@ -10,9 +10,9 @@
from helpers import ask_next_question, start_inactivity_timer, get_user_id
from loggers.log import setup_logging
from bot.validators import is_valid_rating
-from crud.feedback import create_feedback
from crud.users import get_id_by_tg_id
from core.bot_setup import bot
+from crud.feedback_crud import feedback_crud
router = Router()
@@ -81,7 +81,7 @@ async def process_description(
feedback_data = await state.get_data()
- await create_feedback(feedback_data, session)
+ await feedback_crud.create(feedback_data, session)
logger.info(f'Запись создана в БД с ID: {feedback_data.get("user")}.')
diff --git a/app/crud/feedback.py b/app/crud/feedback.py
deleted file mode 100644
index 386ab4c..0000000
--- a/app/crud/feedback.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from datetime import datetime
-
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from models.models import Feedback
-
-
-async def create_feedback(user_data: dict, session: AsyncSession) -> Feedback:
- """Создание записи с отзывом."""
-
- data_to_db = Feedback(**user_data, feedback_date=datetime.utcnow())
-
- session.add(data_to_db)
- await session.commit()
- await session.refresh(data_to_db)
-
- return data_to_db
diff --git a/app/crud/feedback_crud.py b/app/crud/feedback_crud.py
index 51d68b3..104844e 100644
--- a/app/crud/feedback_crud.py
+++ b/app/crud/feedback_crud.py
@@ -13,15 +13,5 @@ async def get_multi(self, session: AsyncSession):
)
return db_objs.scalars().all()
- async def bulk_create(
- self,
- objs_in: list,
- session: AsyncSession,
- ):
- db_objs = [self.model(**obj) for obj in objs_in]
- session.add_all(db_objs)
- await session.commit()
- return db_objs
-
feedback_crud = FeedbackCRUD(Feedback)
diff --git a/app/main.py b/app/main.py
index e869d9e..32e26b1 100644
--- a/app/main.py
+++ b/app/main.py
@@ -30,14 +30,16 @@ async def main() -> None:
dispatcher.include_router(fsm_context_router)
dispatcher.include_router(feedback_context)
- dispatcher.include_router(message_router)
- dispatcher.include_router(callback_router)
dispatcher.include_router(admin_router)
dispatcher.include_router(user_router)
+ dispatcher.include_router(message_router)
+ dispatcher.include_router(callback_router)
try:
logger.info("Запуск бота...")
- dispatcher.update.middleware(DataBaseSession(session_pool=AsyncSessionLocal))
+ dispatcher.update.middleware(
+ DataBaseSession(session_pool=AsyncSessionLocal)
+ )
await add_portfolio()
await set_admin()
await dispatcher.start_polling(bot)
diff --git a/app/test.py b/app/test.py
deleted file mode 100644
index 7f8f998..0000000
--- a/app/test.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import asyncio
-from datetime import datetime
-
-from core.db import AsyncSessionLocal
-from crud.feedback_crud import feedback_crud
-
-
-
-feedbacks = {
- "user": 1,
- "feedback_text": "Типа отзыв",
- "feedback_date": datetime.now(),
- "rating": 5
-
-}
-
-async def add_to_db():
- async with AsyncSessionLocal() as session:
- await feedback_crud.create(feedbacks, session)
-
-
-if __name__ == "__main__":
- asyncio.run(add_to_db())
\ No newline at end of file
From 2dc232b5b1330fd6c7a65dbd1c0a32bf9cae1039 Mon Sep 17 00:00:00 2001
From: ikhit
Date: Wed, 16 Oct 2024 12:13:28 +0300
Subject: [PATCH 64/75] remove commets from app/core/settings.py
---
app/core/settings.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/app/core/settings.py b/app/core/settings.py
index 90b91f4..5bc003e 100644
--- a/app/core/settings.py
+++ b/app/core/settings.py
@@ -11,13 +11,13 @@ class Settings(BaseSettings):
database_url: str
bot_token: str
telegram_chat_ids: str
- # email: str
- # email_password: str
- # postgres_user: str
- # postgres_password: str
- # postgres_db: str
- # db_host: str
- # db_port: str
+ email: str
+ email_password: str
+ postgres_user: str
+ postgres_password: str
+ postgres_db: str
+ db_host: str
+ db_port: str
class Config:
env_file = ".env"
From d5e175f0fff2230653d5a009bd586e314328e1cb Mon Sep 17 00:00:00 2001
From: Maxim Tsaregradtsev <3m3rcy3@gmail.com>
Date: Wed, 16 Oct 2024 12:45:31 +0300
Subject: [PATCH 65/75] update .example, makefile, readme
---
.env.example | 4 ++--
README.md | 2 +-
app/bot/keyborads.py | 24 ++++++++++++++++--------
app/bot/smtp.py | 4 +++-
app/main.py | 4 +++-
app/models/models_const.py | 5 +++++
docker-compose.yml | 2 +-
makefile | 22 ++++++++++++++++++++--
8 files changed, 51 insertions(+), 16 deletions(-)
create mode 100644 app/models/models_const.py
diff --git a/.env.example b/.env.example
index 376acf1..96a9f2c 100644
--- a/.env.example
+++ b/.env.example
@@ -5,8 +5,8 @@ DATABASE_URL=<'DATABASE URL'>
POSTGRES_USER=<'DB OWNER'>
POSTGRES_PASSWORD=<'PASSWORD'>
POSTGRES_DB=<'DB NAME'>
-DB_HOST=db
-DB_PORT=5432
+DB_HOST=<'DB HOST'>
+DB_PORT=<'DB PORT'>
EMAIL=<'MANAGER EMAIL'>
EMAIL_PASSWORD=<'PASSWORD'>
diff --git a/README.md b/README.md
index 65410e9..a4d99d6 100644
--- a/README.md
+++ b/README.md
@@ -105,7 +105,7 @@ cp .env.example .env
## Как использовать Makefile
-Makefile предоставляет команды для упрощения управления зависимостями, миграциями базы данных и запуском приложения. Вот описание каждой команды:
+[Makefile](https://victorz.ru/202402043262) предоставляет команды для упрощения управления зависимостями, миграциями базы данных и запуском приложения. Вот описание каждой команды:
### 1. Установка зависимостей
Команда устанавливает зависимости, указанные в `pyproject.toml`, с помощью `Poetry`.
diff --git a/app/bot/keyborads.py b/app/bot/keyborads.py
index a8344d0..4ae6b3f 100644
--- a/app/bot/keyborads.py
+++ b/app/bot/keyborads.py
@@ -24,17 +24,17 @@
],
[
InlineKeyboardButton(
- text="Получить информацию о компании.", callback_data="company_info"
+ text="Информация о компании.", callback_data="company_info"
)
],
[
InlineKeyboardButton(
- text="Узнать о продуктах и услугах.", callback_data="products_services"
+ text="Продуктах и услугах.", callback_data="products_services"
)
],
[
InlineKeyboardButton(
- text="Получить техническую поддержку.", callback_data="tech_support"
+ text="Техническая поддержка.", callback_data="tech_support"
)
],
[
@@ -72,7 +72,10 @@ async def inline_products_and_services(session: AsyncSession):
for obj in objects_in_db:
keyboard.add(
- InlineKeyboardButton(text=obj.title, callback_data=f"category_{obj.id}")
+ InlineKeyboardButton(
+ text=obj.title,
+ callback_data=f"category_{obj.id}"
+ )
)
keyboard.add(back_to_main_menu)
@@ -100,7 +103,8 @@ async def list_of_projects_keyboard(session: AsyncSession):
keyboard = InlineKeyboardBuilder()
for project in projects:
- keyboard.add(InlineKeyboardButton(text=project.project_name, url=project.url))
+ keyboard.add(InlineKeyboardButton(
+ text=project.project_name, url=project.url))
keyboard.add(back_to_main_menu)
@@ -112,12 +116,14 @@ async def list_of_projects_keyboard(session: AsyncSession):
[InlineKeyboardButton(text="F.A.Q", callback_data="get_faq")],
[
InlineKeyboardButton(
- text="Проблемы с продуктами", callback_data="get_problems_with_products"
+ text="Проблемы с продуктами",
+ callback_data="get_problems_with_products"
)
],
[
InlineKeyboardButton(
- text="Запрос на обратный звонок", callback_data="callback_request"
+ text="Запрос на обратный звонок",
+ callback_data="callback_request"
)
],
[back_to_main_menu],
@@ -156,7 +162,9 @@ async def category_type_inline_keyboard(
for category_type in category_types:
keyboard.add(
- InlineKeyboardButton(text=category_type.name, url=category_type.url)
+ InlineKeyboardButton(
+ text=category_type.name, url=category_type.url
+ )
)
keyboard.add(back_to_previous_menu)
diff --git a/app/bot/smtp.py b/app/bot/smtp.py
index 1bfe320..7297785 100644
--- a/app/bot/smtp.py
+++ b/app/bot/smtp.py
@@ -21,7 +21,9 @@ async def send_mail(subject, to, user_data):
message["From"] = BASE_EMAIL
message["To"] = to
message["Subject"] = subject
- message.attach(MIMEText(f"{text}", "html", "utf-8"))
+ message.attach(
+ MIMEText(f"{text}", "html", "utf-8")
+ )
smtp_client = SMTP(hostname="smtp.yandex.ru", port=465, use_tls=True)
async with smtp_client:
diff --git a/app/main.py b/app/main.py
index bb73b65..30542ba 100644
--- a/app/main.py
+++ b/app/main.py
@@ -37,7 +37,9 @@ async def main() -> None:
try:
logger.info("Запуск бота...")
- dispatcher.update.middleware(DataBaseSession(session_pool=AsyncSessionLocal))
+ dispatcher.update.middleware(
+ DataBaseSession(session_pool=AsyncSessionLocal)
+ )
await add_portfolio()
await dispatcher.start_polling(bot, skip_updates=True)
diff --git a/app/models/models_const.py b/app/models/models_const.py
new file mode 100644
index 0000000..eff3a28
--- /dev/null
+++ b/app/models/models_const.py
@@ -0,0 +1,5 @@
+NAME_LENGTH = 150
+URL_LENGTH = 128
+MEDIA_URL = 256
+FIRST_NAME_LENGHT = 32
+PHONE_NUMBER_LENGHT = 32
diff --git a/docker-compose.yml b/docker-compose.yml
index 9e51234..c990480 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -17,7 +17,7 @@ services:
container_name: scid-bot
#build: . # раскомментить для работы локально
image: greenvibe/scid_bot_3 # закомментить для работы локально
- command: bash -c "cd app && poetry run alembic stamp head && poetry run alembic revision --autogenerate && poetry run alembic upgrade head && poetry run python main.py"
+ command: bash -c docker_compose_command
env_file: .env
gateway:
diff --git a/makefile b/makefile
index 138e42b..3a22d71 100644
--- a/makefile
+++ b/makefile
@@ -1,30 +1,48 @@
PYTHON = python
APP = app/main.py
-
+# Устанавливает зависимости, указанные в pyproject.toml
install:
poetry install
+# Добавляет новый пакет в проект
+# Использование: make install-package PACKAGE=<имя_пакета>
install-package:
poetry add $(PACKAGE)
+# Обновляет все зависимости до последних версий
update:
poetry update
+# Запускает основное приложение
run:
poetry run $(PYTHON) $(APP)
+# Очищает файлы .pyc, которые создаются при компиляции Python
clean:
find . -name "*.pyc" -delete
+# Форматирует код по стандарту black
format:
poetry run black .
+# Применяет последние миграции к базе данных
migrate:
alembic upgrade head
+# Откатывает последнюю миграцию базы данных
downgrade:
alembic downgrade -1
+# Создает новую миграцию на основе изменений в моделях
+# Использование: make makemigration msg="описание изменений"
makemigration:
- alembic revision --autogenerate -m "$(msg)"
\ No newline at end of file
+ alembic revision --autogenerate -m "$(msg)"
+
+# Выполняет набор команд для Docker Compose:
+# 1. Проставляет текущее состояние миграций.
+# 2. Генерирует новую миграцию.
+# 3. Применяет миграции.
+# 4. Запускает основное приложение.
+docker_compose_command:
+ cd app && poetry run alembic stamp head && poetry run alembic revision --autogenerate && poetry run alembic upgrade head && poetry run python main.py
From 3551170196940360e8a24fbeef9a4c8a9ca4f013 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Wed, 16 Oct 2024 12:56:18 +0300
Subject: [PATCH 66/75] pydantic fix test
---
.../versions/fd795bdbba90_update_feedback.py | 41 -------------------
app/main.py | 4 +-
2 files changed, 2 insertions(+), 43 deletions(-)
delete mode 100644 app/alembic/versions/fd795bdbba90_update_feedback.py
diff --git a/app/alembic/versions/fd795bdbba90_update_feedback.py b/app/alembic/versions/fd795bdbba90_update_feedback.py
deleted file mode 100644
index c6f71cc..0000000
--- a/app/alembic/versions/fd795bdbba90_update_feedback.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""Update Feedback
-
-Revision ID: fd795bdbba90
-Revises:
-Create Date: 2024-10-11 19:56:11.060934
-
-"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-# revision identifiers, used by Alembic.
-revision: str = "fd795bdbba90"
-down_revision: Union[str, None] = None
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table(
- "feedback",
- sa.Column("user", sa.Integer(), nullable=False),
- sa.Column("rating", sa.INTEGER(), nullable=False),
- sa.Column("feedback_text", sa.TEXT(), nullable=False),
- sa.Column("feedback_date", postgresql.TIMESTAMP(), nullable=False),
- sa.Column("unread", sa.BOOLEAN(), nullable=False),
- sa.Column("id", sa.Integer(), nullable=False),
- sa.ForeignKeyConstraint(["user"], ["user.id"], ondelete="CASCADE"),
- sa.PrimaryKeyConstraint("id"),
- )
- # ### end Alembic commands ###
-
-
-def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table("feedback")
- # ### end Alembic commands ###
diff --git a/app/main.py b/app/main.py
index bb73b65..f2b9bd4 100644
--- a/app/main.py
+++ b/app/main.py
@@ -9,7 +9,7 @@
from bot.callbacks import router as callback_router
from bot.fsm_contexts.manager_context import router as fsm_context_router
from bot.fsm_contexts.feedback_context import router as feedback_context
-from core.init_db import add_portfolio
+#from core.init_db import add_portfolio
from admin.handlers.admin_handlers import admin_router
from admin.handlers.user import user_router
from loggers.log import setup_logging
@@ -38,7 +38,7 @@ async def main() -> None:
try:
logger.info("Запуск бота...")
dispatcher.update.middleware(DataBaseSession(session_pool=AsyncSessionLocal))
- await add_portfolio()
+ #await add_portfolio()
await dispatcher.start_polling(bot, skip_updates=True)
except Exception as e:
From 37d66175028c6292bdc840e45453241ad958ca10 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Wed, 16 Oct 2024 14:35:45 +0300
Subject: [PATCH 67/75] fix make
---
app/main.py | 6 +++---
docker-compose.yml | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/main.py b/app/main.py
index 49d42ff..32e26b1 100644
--- a/app/main.py
+++ b/app/main.py
@@ -9,7 +9,7 @@
from bot.callbacks import router as callback_router
from bot.fsm_contexts.manager_context import router as fsm_context_router
from bot.fsm_contexts.feedback_context import router as feedback_context
-#from core.init_db import add_portfolio, set_admin
+from core.init_db import add_portfolio, set_admin
from admin.handlers.admin_handlers import admin_router
from admin.handlers.user import user_router
from loggers.log import setup_logging
@@ -40,8 +40,8 @@ async def main() -> None:
dispatcher.update.middleware(
DataBaseSession(session_pool=AsyncSessionLocal)
)
- #await add_portfolio()
- #await set_admin()
+ await add_portfolio()
+ await set_admin()
await dispatcher.start_polling(bot)
except Exception as e:
diff --git a/docker-compose.yml b/docker-compose.yml
index dc0c989..a335f78 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,5 +15,5 @@ services:
container_name: scid-bot
# build: . # раскомментить для работы локально
image: greenvibe/scid_bot_3
- command: bash -c "cd app && poetry run alembic stamp head && poetry run alembic revision --autogenerate && poetry run alembic upgrade head && poetry run python main.py"
+ command: bash -c make docker_compose_command
env_file: .env
From d38d45ce4b52b4c76bd27679cf75b93fb911ef70 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Wed, 16 Oct 2024 14:36:51 +0300
Subject: [PATCH 68/75] fix make
---
app/makefile => makefile | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename app/makefile => makefile (100%)
diff --git a/app/makefile b/makefile
similarity index 100%
rename from app/makefile
rename to makefile
From 1d5559a3d3573d4b4b3004c6ece95525771ceaa3 Mon Sep 17 00:00:00 2001
From: Green Vibe <105653543+GreenVibesOnly@users.noreply.github.com>
Date: Wed, 16 Oct 2024 14:41:55 +0300
Subject: [PATCH 69/75] Rename Makefile
---
makefile => Makefile | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename makefile => Makefile (100%)
diff --git a/makefile b/Makefile
similarity index 100%
rename from makefile
rename to Makefile
From ffbd4c35457ba6c48fd3caa0469ac55578779906 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Wed, 16 Oct 2024 14:59:06 +0300
Subject: [PATCH 70/75] fix make
---
docker-compose.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index a335f78..565a3f0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,5 +15,5 @@ services:
container_name: scid-bot
# build: . # раскомментить для работы локально
image: greenvibe/scid_bot_3
- command: bash -c make docker_compose_command
+ command: make docker_compose_command
env_file: .env
From e1e8386bd49d1ea55f4d74d207042acd444ab977 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Wed, 16 Oct 2024 15:03:51 +0300
Subject: [PATCH 71/75] fix make
---
docker-compose.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 565a3f0..779621b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,5 +15,5 @@ services:
container_name: scid-bot
# build: . # раскомментить для работы локально
image: greenvibe/scid_bot_3
- command: make docker_compose_command
+ command: bash -c make
env_file: .env
From 69e7ad53e1dab064998c2a9dc86f21bba6659f22 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Wed, 16 Oct 2024 15:08:30 +0300
Subject: [PATCH 72/75] fix make
---
Makefile => app/makefile | 0
docker-compose.yml | 2 +-
2 files changed, 1 insertion(+), 1 deletion(-)
rename Makefile => app/makefile (100%)
diff --git a/Makefile b/app/makefile
similarity index 100%
rename from Makefile
rename to app/makefile
diff --git a/docker-compose.yml b/docker-compose.yml
index 779621b..a335f78 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,5 +15,5 @@ services:
container_name: scid-bot
# build: . # раскомментить для работы локально
image: greenvibe/scid_bot_3
- command: bash -c make
+ command: bash -c make docker_compose_command
env_file: .env
From 0099ed2ac1c1f9d0cacecaa847d81ee8470e57a4 Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Wed, 16 Oct 2024 16:45:47 +0300
Subject: [PATCH 73/75] update for master merge
---
.github/workflows/main.yml | 2 +-
docker-compose.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d545ccf..485665f 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -3,7 +3,7 @@ on:
push:
branches:
- master
- - dev # закомментить перед выходом в прод
+# - dev
# pull_request:
# branches:
# - dev
diff --git a/docker-compose.yml b/docker-compose.yml
index a335f78..54fa5d2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,5 +15,5 @@ services:
container_name: scid-bot
# build: . # раскомментить для работы локально
image: greenvibe/scid_bot_3
- command: bash -c make docker_compose_command
+ command: bash -c "make docker_compose_command"
env_file: .env
From 0bc4dba897933d4acae65cd3c8fdd884a517a5d8 Mon Sep 17 00:00:00 2001
From: Green Vibe <105653543+GreenVibesOnly@users.noreply.github.com>
Date: Wed, 16 Oct 2024 16:48:00 +0300
Subject: [PATCH 74/75] Update main.yml
---
.github/workflows/main.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 485665f..87bfa4d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -3,7 +3,7 @@ on:
push:
branches:
- master
-# - dev
+ - dev
# pull_request:
# branches:
# - dev
From 0f377fd0390f506530dff5886ea59cb70a93233a Mon Sep 17 00:00:00 2001
From: Kseniya Tetercheva
Date: Wed, 16 Oct 2024 16:52:50 +0300
Subject: [PATCH 75/75] update for master merge
---
.github/workflows/main.yml | 2 +-
docker-compose.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 485665f..87bfa4d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -3,7 +3,7 @@ on:
push:
branches:
- master
-# - dev
+ - dev
# pull_request:
# branches:
# - dev
diff --git a/docker-compose.yml b/docker-compose.yml
index 54fa5d2..dc0c989 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,5 +15,5 @@ services:
container_name: scid-bot
# build: . # раскомментить для работы локально
image: greenvibe/scid_bot_3
- command: bash -c "make docker_compose_command"
+ command: bash -c "cd app && poetry run alembic stamp head && poetry run alembic revision --autogenerate && poetry run alembic upgrade head && poetry run python main.py"
env_file: .env