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