From 3b5b71764c7402709447f3cd6c3e61b7fe8586eb Mon Sep 17 00:00:00 2001 From: Anastasia Lukina Date: Mon, 8 Dec 2025 14:28:50 +0300 Subject: [PATCH 01/12] Bot functions --- ...udios_bot.db => student_studios_bot (1).db | Bin 45056 -> 57344 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename student_studios_bot.db => student_studios_bot (1).db (74%) diff --git a/student_studios_bot.db b/student_studios_bot (1).db similarity index 74% rename from student_studios_bot.db rename to student_studios_bot (1).db index 827d44b9d62a0901e46f82da902f1834e0ef3b66..1f642ad200e9e539accd3413f10c942e31132431 100644 GIT binary patch delta 1278 zcmd^8%}>){7;o2Z(9#deBF(Y`O=EB~W6PMITugx%oQ@W2i;}3#SesD7*0GjFIXE`M zUG3S6|AFbnBkIMA2}cixc=ZH{G4beI3Q?Jf7jL$C+PA;w_k8qye((0@*!C{=G|f>6 zf-unH&@gE1vs~#Mh2*z3XzBnV@k4~!r(O{6D05&b{xUvBui%Gt3118ESKS-2&J&&+ z;0pzG=e}26S-0Gc^%cXj>P_S5(*mF=NE+xNr@Yc$y*MALF>ExYO2P9yS%?3&Z?sx{C3i8NbI-7%W0 zju)k#t|-$XGs4@pY5k7dt^pI+ZCLcU3t<+_NqR*SiecQURW*&xj%&3}cWO7$po(#{Krh{{OO4plYM*6z*)~k^zlA=v5!tqjQCs3 zS^onwh|YPtj}Nj0+FLD2A`oauz*{9&-x#g$r$?yuJ_o~85}F1eZsvBQ{jg4@w> z+1&aM@A0wd;<89e<%*=JB{Dria9Ne!b6}aw;*A7ZiQ!HzFO%fT($%CG?sZCX zDN|GmWkt>Rx-7IP->N7@IVb1Kr2#mj=F92KKTx5l=G0UntCrQ2A`SFf57h}plK*>A zN+~2!sxr|}3!wji`so$ z{koQnVT*^G(kCv-`it485qJpu(1s>-pauH`cIgJKQ;jyD3r#%lCa#Flgt)qJ{(_dZ z`M8=E(k|-K-1{1Av;J53)c_MZbPI_N2(9C>L${EL&^3HKfH%-0^bVL<9xzVq-X(~^ zOEwl!tiyyh2x@53I)N@?E!c&9oR3;iQ=@-J+BV&U9sDwb!`slJ>o{)@_iL*a%mnIP zJsUZv#fJp0Zr6MouP>Z>SAz$CjkTDuo Date: Mon, 8 Dec 2025 14:41:45 +0300 Subject: [PATCH 02/12] Files of Roles --- Admin.py | 62 +++++++++++++++++++++++++ Dtbase_config.py | 17 +++++++ Head_of_studio.py | 92 +++++++++++++++++++++++++++++++++++++ User.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 283 insertions(+) create mode 100644 Admin.py create mode 100644 Dtbase_config.py create mode 100644 Head_of_studio.py create mode 100644 User.py diff --git a/Admin.py b/Admin.py new file mode 100644 index 0000000..90fea11 --- /dev/null +++ b/Admin.py @@ -0,0 +1,62 @@ +from gettext import textdomain + +import telebot +from telebot import types +import uuid # Для генерации уникальных имен файлов +from datetime import datetime # Для работы с датами и временем +import sqlite3 + +bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +#====================== +#========ADMIN========= +#====================== + + +@bot.message_handler(commands=['start']) +def start(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Статистика') + btn2=types.KeyboardButton('Рассылка') + btn3 = types.KeyboardButton('Назначить администратора') + markup.row(btn1,btn2,btn3) + btn4 = types.KeyboardButton('Изменить информацию о студиях') + markup.row(btn4) + + bot.send_message(message.chat.id, + f' Добро пожаловать, {message.from_user.first_name}.', parse_mode='html', reply_markup=markup) + +@bot.message_handler(func=lambda message: message.text == 'Статистика') +def stat(message): + bot.send_message(message.chat.id, + f'Статистика активности:', parse_mode='html') + +@bot.message_handler(func=lambda message: message.text == 'Рассылка') +def stat(message): + bot.send_message(message.chat.id, + f'Введите сообщение:', parse_mode='html') + +@bot.message_handler(func=lambda message: message.text == 'Назначить администратора') +def give_role(message): + bot.send_message(message.chat.id, + f'Кого вы хотите назначить?', parse_mode='html') + # Регистрируем следующий шаг для получения username + bot.register_next_step_handler(message, get_admin_username) + + +def get_admin_username(message): + # Если пользователь переслал сообщение + if message.forward_from: + admin_username = f"@{message.forward_from.username}" if message.forward_from.username else str( + message.forward_from.id) + bot.send_message(message.chat.id, f"Username: {admin_username}") + + # Если ввели текст с @ + elif message.text and '@' in message.text: + admin_username = message.text + bot.send_message(message.chat.id, f"Username получен: {admin_username}") + + + + +bot.polling(none_stop=True) \ No newline at end of file diff --git a/Dtbase_config.py b/Dtbase_config.py new file mode 100644 index 0000000..de50f2f --- /dev/null +++ b/Dtbase_config.py @@ -0,0 +1,17 @@ +from telebot import types +import sqlite3 as sq + +bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +#-----------------admin-------------------------- +#------------------------------------------------ +def add_studio: + with sq.connect("student_studios_bot (1).db") as con: + cur=con.cursor() + +#Добавляем пока первичную информацию о студии + cur.execute("""INSERT into studios name promo_photo_id promo_video_id description contacts """) + + con.commit() + con.close() + diff --git a/Head_of_studio.py b/Head_of_studio.py new file mode 100644 index 0000000..2324aac --- /dev/null +++ b/Head_of_studio.py @@ -0,0 +1,92 @@ +from gettext import textdomain + +import telebot +from telebot import types +import sqlite3 + +bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +#====================== +#====HEAD OF STUDIO==== +#====================== +def show_main_menu(markup): + btn1 = types.KeyboardButton('Статистика') + btn2=types.KeyboardButton('Новые заявки') + markup.row(btn1, btn2) + btn3 = types.KeyboardButton('Изменить информацию о студии') + + +@bot.message_handler(commands=['start']) +def start(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + show_main_menu(markup) + + bot.send_message(message.chat.id, + f'Добро пожаловать, {message.from_user.first_name}.', parse_mode='html', + reply_markup=markup) + +@bot.message_handler(func=lambda message: message.text == 'Главное меню') +def main_menu(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + show_main_menu(markup) + + bot.send_message(message.chat.id, + f'Вы вернулись в меню.', parse_mode='html', + reply_markup=markup) + +@bot.message_handler(func=lambda message: message.text == 'Статистика') +def stat(message): + bot.send_message(message.chat.id, + f'Статистика подачи заявок в студию', parse_mode='html') + +@bot.message_handler(func=lambda message: message.text == 'Новые заявки') +def appls(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn_accept = types.KeyboardButton ('❤️-Принять') + btn_refuse = types.KeyboardButton ('Отклонить') + btn_communicate = types.KeyboardButton ('💬-Связаться') + markup.row(btn_accept, btn_communicate, btn_refuse) + btn_menu=(types.KeyboardButton('Главное меню')) + markup.row(btn_menu) + + bot.send_message(message.chat.id, + f'Просмотр заявок:', parse_mode='html', reply_markup=markup) + + +# ---------------------------ИЗМЕНЕНИЕ ИНФОРМАЦИИ О СТУДИИ--------------------------- +@bot.message_handler(content_types=['photo']) +def handle_image(message): + """ + Обрабатываем фото от пользователя. + """ + + # Шаг 1: Получаем file_id самого большого фото + file_id = message.photo[-1].file_id + + # Шаг 2: Скачиваем фото с серверов Telegram + file_info = bot.get_file(file_id) + downloaded = bot.download_file(file_info.file_path) + +@bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') +def edit_info(message): + +class StudioCreationStates(StatesGroup): +""" + Класс состояний для создания студии. + Наследуется от StatesGroup - специального класса из telebot для FSM. + + Каждое состояние - это шаг в диалоге с пользователем. + Порядок объявления важен для логики работы. + """ + name = State() # Шаг 1: Ожидаем название студии + promo_type = State() # Шаг 2: Ожидаем выбор типа промокода + promo_content = State() # Шаг 3: Ожидаем сам промокод (текст или файл) + description = State() # Шаг 4: Ожидаем описание студии + + bot.send_message(message.chat.id, 'Введите название студии') + studio_name = + studio_promo = + studio_description = + + +bot.polling(none_stop=True) \ No newline at end of file diff --git a/User.py b/User.py new file mode 100644 index 0000000..6b76db2 --- /dev/null +++ b/User.py @@ -0,0 +1,112 @@ +from gettext import textdomain + +import telebot +from telebot import types +import sqlite3 + +bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +#====================== +#=========USER========= +#====================== + + +@bot.message_handler(commands=['start']) +def start(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2=types.KeyboardButton('Мои заявки') + btn3 = types.KeyboardButton('Главное меню') + markup.row(btn2, btn3) + + bot.send_message(message.chat.id, + f'Привет, {message.from_user.first_name}, в этом боте ты можешь узнать про творческие студии' + f' ГУАП и стать частью активной студенческой жизни!', parse_mode='html', reply_markup=markup) + +# ДАЛЕЕ ВЕТКА : КАТАЛОГ СТУДИЙ + +@bot.message_handler(func=lambda message: message.text == 'Каталог студий') +def inline_button_catalog(message): + markup = types.InlineKeyboardMarkup() + btn_orgmero = types.InlineKeyboardButton("Студия организации мероприятий", callback_data='orgMero') + markup.row(btn_orgmero) + btn_tehguap = types.InlineKeyboardButton("Студия технического обеспечения мероприятий", callback_data='tehGuap') + markup.row(btn_tehguap) + btn_muzguap = types.InlineKeyboardButton("МУЗГУАП", callback_data='muzGuap') + markup.row(btn_muzguap) + btn_dance = types.InlineKeyboardButton("Танцевальная студия", callback_data='dance') + markup.row(btn_dance) + btn_theatr= types.InlineKeyboardButton("Театральная студия", callback_data='theatr') + markup.row(btn_theatr) + btn_comedy = types.InlineKeyboardButton("КВН", callback_data='comedy') + markup.row(btn_comedy) + btn_vedush = types.InlineKeyboardButton("Студия ведущих", callback_data='vedush') + markup.row(btn_vedush) + btn_media = types.InlineKeyboardButton("Медиацентр", callback_data='media') + markup.row(btn_media) + + bot.send_message(message.chat.id, 'Нажми на студию для просмотра:', reply_markup=markup) + +# @bot.callback_query_handler(func=lambda callback: callback.data == 'orgMero') +# def orgMeroInfo(callback): + +# ДАЛЕЕ ВЕТКА : МОИ ЗАЯВКИ + +@bot.message_handler(func=lambda message: message.text == 'Мои заявки') +def membership_appl(message): + bot.send_message(message.chat.id, 'Вот статус твоих заявок:') + + +#Данные пользователя (для себя сделала) +@bot.message_handler(commands=['user']) +def main(message): + + text=message + if not isinstance(text, str): + text = str(text) + + message_parts = split_message(text) + + # Отправляем каждую часть отдельным сообщением + for part in message_parts: + bot.send_message(message.chat.id, part) + + +def split_message(text, max_length=4096): + """Разделяет текст на части не превышающие max_length""" + return [text[i:i+max_length] for i in range(0, len(text), max_length)] + + +# @bot.message_handler(content_types=['photo']) +# def get_photo(message): +# markup = types.InlineKeyboardMarkup() +# btn1 = types.InlineKeyboardButton('Duuuuura', url='https://t.me/+lVcKrLQJxH9hODYy') +# markup.row(btn1) +# btn2 = types.InlineKeyboardButton('Удалить фото!', callback_data='delete') +# btn3 = types.InlineKeyboardButton('Изменить', callback_data='edit') +# markup.row(btn2,btn3) +# +# bot.reply_to(message, "Спасибо, подрочил", reply_markup = markup) +# +# @bot.callback_query_handler(func=lambda callback:True) +# def callback_message(callback): +# if callback.data == 'delete': +# bot.delete_message(callback.message.chat.id, callback.message.message_id-1) +# elif callback.data == 'edit': +# bot.edit_message_text('Падем выдим', callback.message.chat.id, callback.message.message_id) +# +# +# +# +# def on_click(message): +# if message.text.lower == 'вступить в табор': +# bot.send_message(message.chat.id, 'Добро пожаловать к дурам, тварина') +# elif message.text.lower == 'аборт': +# bot.send_message(message.chat.id, 'Ну удалил и чо') +# +# +# + + +bot.polling(none_stop=True) \ No newline at end of file From 395172b5043950ff1c9e7d4cbb96c9cb4a3da5ce Mon Sep 17 00:00:00 2001 From: Anastasia Lukina Date: Mon, 8 Dec 2025 16:37:28 +0300 Subject: [PATCH 03/12] Python files --- .venv/Admin.py | 62 +++++++++ .venv/Dtbase_config.py | 18 +++ .venv/Head_of_studio.py | 170 +++++++++++++++++++++++++ .venv/Studio_editor(headStudFunc).py | 184 +++++++++++++++++++++++++++ .venv/User.py | 112 ++++++++++++++++ README.md | 1 - student_studios_bot (1).db | Bin 57344 -> 0 bytes 7 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 .venv/Admin.py create mode 100644 .venv/Dtbase_config.py create mode 100644 .venv/Head_of_studio.py create mode 100644 .venv/Studio_editor(headStudFunc).py create mode 100644 .venv/User.py delete mode 100644 README.md delete mode 100644 student_studios_bot (1).db diff --git a/.venv/Admin.py b/.venv/Admin.py new file mode 100644 index 0000000..f72942f --- /dev/null +++ b/.venv/Admin.py @@ -0,0 +1,62 @@ +from gettext import textdomain + +import telebot +from telebot import types +import uuid # Для генерации уникальных имен файлов +from datetime import datetime # Для работы с датами и временем +import sqlite3 + +bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +#====================== +#========ADMIN========= +#====================== + + +@bot.message_handler(commands=['start']) +def start(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Статистика') + btn2=types.KeyboardButton('Рассылка') + btn3 = types.KeyboardButton('Назначить администратора') + markup.row(btn1,btn2,btn3) + btn4 = types.KeyboardButton('Изменить информацию о студиях') + markup.row(btn4) + + bot.send_message(message.chat.id, + f' Добро пожаловать, {message.from_user.first_name}.', parse_mode='html', reply_markup=markup) + +@bot.message_handler(func=lambda message: message.text == 'Статистика') +def stat(message): + bot.send_message(message.chat.id, + f'Статистика активности:', parse_mode='html') + +@bot.message_handler(func=lambda message: message.text == 'Рассылка') +def stat(message): + bot.send_message(message.chat.id, + f'Введите сообщение:', parse_mode='html') + +@bot.message_handler(func=lambda message: message.text == 'Назначить администратора') +def give_role(message): + bot.send_message(message.chat.id, + f'Кого вы хотите назначить?', parse_mode='html') + # Регистрируем следующий шаг для получения username + bot.register_next_step_handler(message, get_admin_username) + + +def get_admin_username(message): + # Если пользователь переслал сообщение + if message.forward_from: + admin_username = f"@{message.forward_from.username}" if message.forward_from.username else str( + message.forward_from.id) + bot.send_message(message.chat.id, f"Username: {admin_username}") + + # Если ввели текст с @ + elif message.text and '@' in message.text: + admin_username = message.text + bot.send_message(message.chat.id, f"Username получен: {admin_username}") + + + + +bot.polling(none_stop=True) \ No newline at end of file diff --git a/.venv/Dtbase_config.py b/.venv/Dtbase_config.py new file mode 100644 index 0000000..b2c7206 --- /dev/null +++ b/.venv/Dtbase_config.py @@ -0,0 +1,18 @@ +from telebot import types +import sqlite3 as sq + +bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +#-----------------admin-------------------------- +#------------------------------------------------ +def add_studio(): + with sq.connect("student_studios_bot (1).db") as con: + cur=con.cursor() + +#Добавляем пока первичную информацию о студии + cur.execute("""INSERT into studios name promo_photo_id promo_video_id description contacts """, + {name,}) + + con.commit() + con.close() + diff --git a/.venv/Head_of_studio.py b/.venv/Head_of_studio.py new file mode 100644 index 0000000..c996771 --- /dev/null +++ b/.venv/Head_of_studio.py @@ -0,0 +1,170 @@ +from gettext import textdomain + +import telebot +from telebot import types +import sqlite3 + +bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +#====================== +#====HEAD OF STUDIO==== +#====================== +def show_main_menu(markup): + btn1 = types.KeyboardButton('Статистика') + btn2=types.KeyboardButton('Новые заявки') + markup.row(btn1, btn2) + btn3 = types.KeyboardButton('Изменить информацию о студии') + + +@bot.message_handler(commands=['start']) +def start(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + show_main_menu(markup) + + bot.send_message(message.chat.id, + f'Добро пожаловать, {message.from_user.first_name}.', parse_mode='html', + reply_markup=markup) + +@bot.message_handler(func=lambda message: message.text == 'Главное меню') +def main_menu(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + show_main_menu(markup) + + bot.send_message(message.chat.id, + f'Вы вернулись в меню.', parse_mode='html', + reply_markup=markup) + +@bot.message_handler(func=lambda message: message.text == 'Статистика') +def stat(message): + bot.send_message(message.chat.id, + f'Статистика подачи заявок в студию', parse_mode='html') + +@bot.message_handler(func=lambda message: message.text == 'Новые заявки') +def appls(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn_accept = types.KeyboardButton ('❤️-Принять') + btn_refuse = types.KeyboardButton ('Отклонить') + btn_communicate = types.KeyboardButton ('💬-Связаться') + markup.row(btn_accept, btn_communicate, btn_refuse) + btn_menu=(types.KeyboardButton('Главное меню')) + markup.row(btn_menu) + + bot.send_message(message.chat.id, + f'Просмотр заявок:', parse_mode='html', reply_markup=markup) + + +# ---------------------------ИЗМЕНЕНИЕ ИНФОРМАЦИИ О СТУДИИ--------------------------- + +@bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') +# ==================== +# РЕДАКТИРОВАНИЕ СТУДИИ +# ==================== + +@bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') +def edit_info(message): + """ + Обработчик кнопки 'Изменить информацию о студии'. + Начинает процесс пошагового редактирования студии. + """ + user_id = message.from_user.id + + # Начинаем процесс редактирования через наш класс StudioEditor + response = studio_editor.start_editing(user_id) + + # Отправляем первое сообщение с инструкцией + bot.send_message( + message.chat.id, + response, + parse_mode='html' + ) + + +@bot.message_handler(func=lambda message: studio_editor.get_user_step(message.from_user.id) in + ['waiting_for_name', 'waiting_for_description']) +def handle_text_for_editing(message): + """ + Обработчик текстовых сообщений во время редактирования студии. + + Этот обработчик срабатывает ТОЛЬКО когда пользователь находится + на этапе 'waiting_for_name' или 'waiting_for_description'. + """ + user_id = message.from_user.id + + # Передаем текст сообщения в редактор студии + response, is_completed = studio_editor.handle_message(user_id, message.text) + + # Отправляем ответ пользователю + bot.send_message( + message.chat.id, + response, + parse_mode='html' + ) + + # Если процесс редактирования завершен, показываем главное меню + if is_completed: + # Показываем кнопки для дальнейших действий + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + show_main_menu(markup) + + bot.send_message( + message.chat.id, + "Что дальше?", + reply_markup=markup + ) + + +@bot.message_handler(content_types=['photo']) +def handle_photo_for_editing(message): + """ + Обработчик фото во время редактирования студии. + + Этот обработчик срабатывает, когда пользователь отправляет фото. + Проверяем, находится ли пользователь на этапе ожидания изображения. + """ + user_id = message.from_user.id + + # Проверяем, находится ли пользователь на этапе ожидания изображения + if studio_editor.get_user_step(user_id) != 'waiting_for_image': + # Если нет - игнорируем фото или сообщаем об ошибке + return + + # Получаем информацию о самом большом фото (Telegram отправляет несколько размеров) + file_id = message.photo[-1].file_id + + # Скачиваем фото с серверов Telegram + file_info = bot.get_file(file_id) + image_bytes = bot.download_file(file_info.file_path) + + # Передаем изображение в редактор студии + response, is_completed = studio_editor.handle_image(user_id, image_bytes) + + # Отправляем ответ пользователю + bot.send_message( + message.chat.id, + response, + parse_mode='html' + ) + + +@bot.message_handler(commands=['cancel']) +def cancel_editing(message): + """ + Обработчик команды /cancel для отмены редактирования. + """ + user_id = message.from_user.id + response = studio_editor.cancel_editing(user_id) + + # Возвращаем в главное меню + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + show_main_menu(markup) + + bot.send_message( + message.chat.id, + response, + parse_mode='html', + reply_markup=markup + ) + + + +bot.polling(none_stop=True) \ No newline at end of file diff --git a/.venv/Studio_editor(headStudFunc).py b/.venv/Studio_editor(headStudFunc).py new file mode 100644 index 0000000..709eaea --- /dev/null +++ b/.venv/Studio_editor(headStudFunc).py @@ -0,0 +1,184 @@ +class StudioEditor: + """ + Класс для управления процессом редактирования студии. + + Вместо сложного FSM используем простой словарь для хранения состояний. + Каждый пользователь имеет свой набор данных. + """ + + def __init__(self): + """ + Инициализация редактора студии. + + Создаем словарь для хранения состояния пользователей: + - Ключ: user_id (ID пользователя в Telegram) + - Значение: словарь с данными и этапом редактирования + """ + self.user_sessions = {} + # Пример структуры данных для одного пользователя: + # { + # 'step': 'waiting_for_name', # Текущий этап + # 'data': { # Собранные данные + # 'name': 'Название студии', + # 'image': b'...', # Бинарные данные изображения + # 'description': 'Описание' + # } + # } + + def start_editing(self, user_id): + """ + Начинает процесс редактирования для пользователя. + + Args: + user_id (int): ID пользователя в Telegram + + Returns: + str: Сообщение для отправки пользователю + """ + # Сбрасываем предыдущую сессию, если была + self.user_sessions[user_id] = { + 'step': 'waiting_for_name', # Первый шаг - ждем название + 'data': {} # Пока данных нет + } + + return "🎬 Редактирование студии\n\nШаг 1 из 3\n📝 Введите новое название студии:" + + def handle_message(self, user_id, message_text): + """ + Обрабатывает текстовое сообщение от пользователя. + + Args: + user_id (int): ID пользователя + message_text (str): Текст сообщения + + Returns: + tuple: (сообщение для пользователя, этап завершен?) + """ + # Проверяем, есть ли активная сессия у пользователя + if user_id not in self.user_sessions: + return "❌ Сначала начните редактирование студии!", False + + session = self.user_sessions[user_id] + current_step = session['step'] + + # Обрабатываем в зависимости от текущего этапа + if current_step == 'waiting_for_name': + # Этап 1: Получаем название студии + if len(message_text.strip()) < 2: + return "❌ Название слишком короткое! Введите название длиннее 2 символов.", False + + # Сохраняем название + session['data']['name'] = message_text.strip() + session['step'] = 'waiting_for_image' # Переходим к следующему этапу + + return ("✅ Название сохранено!\n\n" + "Шаг 2 из 3\n" + "🖼️ Отправьте новое промо-изображение:\n\n" + "Просто отправьте фото в чат ⤵️", False) + + elif current_step == 'waiting_for_description': + # Этап 3: Получаем описание студии + session['data']['description'] = message_text.strip() + + # Завершаем процесс редактирования + result = self._finish_editing(user_id) + return result, True # True означает, что процесс завершен + + else: + # Если этап не предусматривает текстовый ввод + return "⏳ Пожалуйста, отправьте изображение или завершите текущий этап.", False + + def handle_image(self, user_id, image_bytes): + """ + Обрабатывает изображение от пользователя. + + Args: + user_id (int): ID пользователя + image_bytes (bytes): Бинарные данные изображения + + Returns: + tuple: (сообщение для пользователя, этап завершен?) + """ + # Проверяем, есть ли активная сессия + if user_id not in self.user_sessions: + return "❌ Сначала начните редактирование студии!", False + + session = self.user_sessions[user_id] + + # Проверяем, что мы на этапе ожидания изображения + if session['step'] != 'waiting_for_image': + return "❌ Сейчас не время отправлять изображение!", False + + # Сохраняем изображение + session['data']['image'] = image_bytes + session['step'] = 'waiting_for_description' # Переходим к описанию + + return ("✅ Изображение сохранено!\n\n" + "Шаг 3 из 3\n" + "📝 Введите новое описание студии:", False) + + def _finish_editing(self, user_id): + """ + Завершает процесс редактирования. + + Args: + user_id (int): ID пользователя + + Returns: + str: Итоговое сообщение с собранными данными + """ + if user_id not in self.user_sessions: + return "❌ Нет активной сессии редактирования!" + + session = self.user_sessions[user_id] + data = session['data'] + + # Формируем итоговое сообщение + result_message = ( + "✅ Редактирование завершено!\n\n" + f"Название: {data.get('name', 'Не указано')}\n" + f"Описание: {data.get('description', 'Не указано')}\n" + f"Изображение: {'Загружено' if 'image' in data else 'Не загружено'}\n\n" + "Данные готовы для сохранения в базу данных." + ) + + # Здесь будет код для сохранения в БД (позже добавим) + # save_to_database(data) + + # Очищаем сессию пользователя + del self.user_sessions[user_id] + + return result_message + + def cancel_editing(self, user_id): + """ + Отменяет процесс редактирования. + + Args: + user_id (int): ID пользователя + + Returns: + str: Сообщение об отмене + """ + if user_id in self.user_sessions: + del self.user_sessions[user_id] + + return "❌ Редактирование отменено." + + def get_user_step(self, user_id): + """ + Получает текущий этап пользователя. + + Args: + user_id (int): ID пользователя + + Returns: + str or None: Текущий этап или None если сессии нет + """ + if user_id in self.user_sessions: + return self.user_sessions[user_id]['step'] + return None + + +# Создаем глобальный экземпляр редактора +studio_editor = StudioEditor() \ No newline at end of file diff --git a/.venv/User.py b/.venv/User.py new file mode 100644 index 0000000..14a9b61 --- /dev/null +++ b/.venv/User.py @@ -0,0 +1,112 @@ +from gettext import textdomain + +import telebot +from telebot import types +import sqlite3 + +bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +#====================== +#=========USER========= +#====================== + + +@bot.message_handler(commands=['start']) +def start(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2=types.KeyboardButton('Мои заявки') + btn3 = types.KeyboardButton('Главное меню') + markup.row(btn2, btn3) + + bot.send_message(message.chat.id, + f'Привет, {message.from_user.first_name}, в этом боте ты можешь узнать про творческие студии' + f' ГУАП и стать частью активной студенческой жизни!', parse_mode='html', reply_markup=markup) + +# ДАЛЕЕ ВЕТКА : КАТАЛОГ СТУДИЙ + +@bot.message_handler(func=lambda message: message.text == 'Каталог студий') +def inline_button_catalog(message): + markup = types.InlineKeyboardMarkup() + btn_orgmero = types.InlineKeyboardButton("Студия организации мероприятий", callback_data='orgMero') + markup.row(btn_orgmero) + btn_tehguap = types.InlineKeyboardButton("Студия технического обеспечения мероприятий", callback_data='tehGuap') + markup.row(btn_tehguap) + btn_muzguap = types.InlineKeyboardButton("МУЗГУАП", callback_data='muzGuap') + markup.row(btn_muzguap) + btn_dance = types.InlineKeyboardButton("Танцевальная студия", callback_data='dance') + markup.row(btn_dance) + btn_theatr= types.InlineKeyboardButton("Театральная студия", callback_data='theatr') + markup.row(btn_theatr) + btn_comedy = types.InlineKeyboardButton("КВН", callback_data='comedy') + markup.row(btn_comedy) + btn_vedush = types.InlineKeyboardButton("Студия ведущих", callback_data='vedush') + markup.row(btn_vedush) + btn_media = types.InlineKeyboardButton("Медиацентр", callback_data='media') + markup.row(btn_media) + + bot.send_message(message.chat.id, 'Нажми на студию для просмотра:', reply_markup=markup) + +# @bot.callback_query_handler(func=lambda callback: callback.data == 'orgMero') +# def orgMeroInfo(callback): + +# ДАЛЕЕ ВЕТКА : МОИ ЗАЯВКИ + +@bot.message_handler(func=lambda message: message.text == 'Мои заявки') +def membership_appl(message): + bot.send_message(message.chat.id, 'Вот статус твоих заявок:') + + +#Данные пользователя (для себя сделала) +@bot.message_handler(commands=['user']) +def main(message): + + text=message + if not isinstance(text, str): + text = str(text) + + message_parts = split_message(text) + + # Отправляем каждую часть отдельным сообщением + for part in message_parts: + bot.send_message(message.chat.id, part) + + +def split_message(text, max_length=4096): + """Разделяет текст на части не превышающие max_length""" + return [text[i:i+max_length] for i in range(0, len(text), max_length)] + + +# @bot.message_handler(content_types=['photo']) +# def get_photo(message): +# markup = types.InlineKeyboardMarkup() +# btn1 = types.InlineKeyboardButton('Duuuuura', url='https://t.me/+lVcKrLQJxH9hODYy') +# markup.row(btn1) +# btn2 = types.InlineKeyboardButton('Удалить фото!', callback_data='delete') +# btn3 = types.InlineKeyboardButton('Изменить', callback_data='edit') +# markup.row(btn2,btn3) +# +# bot.reply_to(message, "Спасибо, подрочил", reply_markup = markup) +# +# @bot.callback_query_handler(func=lambda callback:True) +# def callback_message(callback): +# if callback.data == 'delete': +# bot.delete_message(callback.message.chat.id, callback.message.message_id-1) +# elif callback.data == 'edit': +# bot.edit_message_text('Падем выдим', callback.message.chat.id, callback.message.message_id) +# +# +# +# +# def on_click(message): +# if message.text.lower == 'вступить в табор': +# bot.send_message(message.chat.id, 'Добро пожаловать к дурам, тварина') +# elif message.text.lower == 'аборт': +# bot.send_message(message.chat.id, 'Ну удалил и чо') +# +# +# + + +bot.polling(none_stop=True) \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 81d81fe..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# Coursework- \ No newline at end of file diff --git a/student_studios_bot (1).db b/student_studios_bot (1).db deleted file mode 100644 index 1f642ad200e9e539accd3413f10c942e31132431..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57344 zcmeI5-)~b#7RT?+4N09}-Y!~ME0krhgp#NUG)dVmNL4ok(`E??1ZOEySC(U6z$mee z?XYEEh)n}Qpn_EKNXrkkFI6QIuws(nRNa4I?pq&L`?`-?srv`)IdgrTYbT*iR3UzR zO>EEaIp=fEojWrg;}ge+V;NJNPNb&78PTu(M$>ie&q8RL)*?t{ zbSLnP_ZDr>KhC$d{8wwL|50oEcgr2mKU<=Wf8BO}+s@XB`X5{S>(BAOXS9GU5ZDBP z%f2@oeS7!nvwzEkCnnAG`N@e;#+*uqR5aATCZT0;EHDrZh~U7%;ehb3$@h!SCXYXz znU2O1p;*-K85#)&J_w9;Q))asW%@nAz(+wbGD^?!;bAd8GIV@Az`0Q~9ZAKKnOGuj z7vjW7BAy9HGU?TnWGXS02qn)ZGIsk1her=`Njv>QENWJ#$I>BcAa;RzE{h@#1r864 z4+ljr*E?&5qoL`vnW~jG7QaB3dMJ{JIzm@uJvKIUbYSe1I1)J3>C#mUj0Z=DMu^bS zz(}x*T0cBG78v?q#4cFXQx|3tk zuFJiiM&Da+=~sTQTsWCXWlkq1$r(?_$iHi%O;z4plS!T|=)v{KCc^O^m+`9&-mQvP zi&LrPIJ{jbV@{f9QsF5j6gOUVQP^cvzkJDTn3=hltjQFE9|i`Gh)z{kWe<&r&W_kr z_>9@nEjpr!$n=yM&u}=Likzj(qN9r(mGzuXPA0-pGa8~BTX5)T;6!lX=&{PRGB`d) z&Ke9=a_mYj2N$)LaC^9mO1idOQ`e{G-e~mg->=Ull+9d)T|TmYs;A0=>z64Tl0ETP zwqs&4T9(i@QW?K1S@BPr@n|f5Mo~}FKy|^4ayVt4Gb0%%JjItxv|^69`S+@(9S3qu z?8@JAACa@+_!+YJ#6@QW+`wNWi79p)rKD|mr|R=|mUZ0D5oJ}2s=-x_t)1l&o(9(0 zKQuJ@cJJ1&U8;4qw99?cjT<>uy6jG_!kcg_9w~|B&+x9?KR2+4yYFZ?6RsH|L{E?S z%Ly7XtyybE=H*jav~pI~x+&x$MHf|Zbc+l8a# z>vUtS^{Mpq#8fOp7sRH9py?zL*kqlmVXMX@&v2e~oUdZ3s%my>x^p$xu4>O9gi3dL zLAO{f*wpoIV}tL=?v3ubF4NPE*ByJ~*tQ0~^PN(DU7j+e&GXY{Ji;?OoDKNi+Mn8H!I&O_(Xy{U()|texvr#wm6kx|%wiaY~g{ zqmz?EH(Q>4s~a#6K9##{BxP=5@M!94B}do2-)i)S2MB-w2!H?xfB*=900@8p2!H?x zfWV7D;JDtX?F?zYetW}lB9W1|$wo&tU-Ry1`?&SMi=ZTA009sH0T2KI5C8!X009sH zfftQHZl}?rWl!eDNidJLOWvc^*JWCb73ow8uez*RwJ5tvp)8$JDal8&B6w5M2XDD-pUa0}9UzAUTU*4Cuu)%&&OXD==jTH z3SO0Y!K*!61X&b46s4SkT;dg>bwkJ{>oe-SBn#_zN6B=ShJ{kA#(5>&Q|r21?&hYb zPm+z+lB?;0x{(Vib%~_DCROR+hkTxP?A<@@&yLnLYnO7{6`*#~rJzLN13XLCjj{%- zLX$dvM^m9RzeTTi{qg~8r-Ul9@bll_wFO#W&6yy{p`>_3+Ak5k8J5JFfT7*`}JAFL)Evs4^g&g9li5+ z`}uqRcz^&1fB*=900@8p2!H?xfB*=900_J!1lo*U`szsxqs3^~okI+D%|^S&`TXDh z{{K&!_b2baUJ}(HCJ2B42!H?xfB*=900@8p2!H?xfPjm@@9K@c`kMa&+i0#gcIeK3 zdFg!r-&GAk5C8!X009sH0T2KI5C8!X009sHfme)x^ZoxbwPw(Gh4SiJng%F3s*BM-e8FK^4QgtI5$H{LTY>6BRDWjwF)f6+F=ZQ4k Date: Mon, 8 Dec 2025 17:04:25 +0300 Subject: [PATCH 04/12] Revert "Files of Roles" This reverts commit f6f9dd73ff5d8a18e54e31c2784cfe53eb0bf9b8. --- Admin.py | 62 ------------------------- Dtbase_config.py | 17 ------- Head_of_studio.py | 92 ------------------------------------- User.py | 112 ---------------------------------------------- 4 files changed, 283 deletions(-) delete mode 100644 Admin.py delete mode 100644 Dtbase_config.py delete mode 100644 Head_of_studio.py delete mode 100644 User.py diff --git a/Admin.py b/Admin.py deleted file mode 100644 index 90fea11..0000000 --- a/Admin.py +++ /dev/null @@ -1,62 +0,0 @@ -from gettext import textdomain - -import telebot -from telebot import types -import uuid # Для генерации уникальных имен файлов -from datetime import datetime # Для работы с датами и временем -import sqlite3 - -bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") - -#====================== -#========ADMIN========= -#====================== - - -@bot.message_handler(commands=['start']) -def start(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn1 = types.KeyboardButton('Статистика') - btn2=types.KeyboardButton('Рассылка') - btn3 = types.KeyboardButton('Назначить администратора') - markup.row(btn1,btn2,btn3) - btn4 = types.KeyboardButton('Изменить информацию о студиях') - markup.row(btn4) - - bot.send_message(message.chat.id, - f' Добро пожаловать, {message.from_user.first_name}.', parse_mode='html', reply_markup=markup) - -@bot.message_handler(func=lambda message: message.text == 'Статистика') -def stat(message): - bot.send_message(message.chat.id, - f'Статистика активности:', parse_mode='html') - -@bot.message_handler(func=lambda message: message.text == 'Рассылка') -def stat(message): - bot.send_message(message.chat.id, - f'Введите сообщение:', parse_mode='html') - -@bot.message_handler(func=lambda message: message.text == 'Назначить администратора') -def give_role(message): - bot.send_message(message.chat.id, - f'Кого вы хотите назначить?', parse_mode='html') - # Регистрируем следующий шаг для получения username - bot.register_next_step_handler(message, get_admin_username) - - -def get_admin_username(message): - # Если пользователь переслал сообщение - if message.forward_from: - admin_username = f"@{message.forward_from.username}" if message.forward_from.username else str( - message.forward_from.id) - bot.send_message(message.chat.id, f"Username: {admin_username}") - - # Если ввели текст с @ - elif message.text and '@' in message.text: - admin_username = message.text - bot.send_message(message.chat.id, f"Username получен: {admin_username}") - - - - -bot.polling(none_stop=True) \ No newline at end of file diff --git a/Dtbase_config.py b/Dtbase_config.py deleted file mode 100644 index de50f2f..0000000 --- a/Dtbase_config.py +++ /dev/null @@ -1,17 +0,0 @@ -from telebot import types -import sqlite3 as sq - -bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") - -#-----------------admin-------------------------- -#------------------------------------------------ -def add_studio: - with sq.connect("student_studios_bot (1).db") as con: - cur=con.cursor() - -#Добавляем пока первичную информацию о студии - cur.execute("""INSERT into studios name promo_photo_id promo_video_id description contacts """) - - con.commit() - con.close() - diff --git a/Head_of_studio.py b/Head_of_studio.py deleted file mode 100644 index 2324aac..0000000 --- a/Head_of_studio.py +++ /dev/null @@ -1,92 +0,0 @@ -from gettext import textdomain - -import telebot -from telebot import types -import sqlite3 - -bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") - -#====================== -#====HEAD OF STUDIO==== -#====================== -def show_main_menu(markup): - btn1 = types.KeyboardButton('Статистика') - btn2=types.KeyboardButton('Новые заявки') - markup.row(btn1, btn2) - btn3 = types.KeyboardButton('Изменить информацию о студии') - - -@bot.message_handler(commands=['start']) -def start(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - show_main_menu(markup) - - bot.send_message(message.chat.id, - f'Добро пожаловать, {message.from_user.first_name}.', parse_mode='html', - reply_markup=markup) - -@bot.message_handler(func=lambda message: message.text == 'Главное меню') -def main_menu(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - show_main_menu(markup) - - bot.send_message(message.chat.id, - f'Вы вернулись в меню.', parse_mode='html', - reply_markup=markup) - -@bot.message_handler(func=lambda message: message.text == 'Статистика') -def stat(message): - bot.send_message(message.chat.id, - f'Статистика подачи заявок в студию', parse_mode='html') - -@bot.message_handler(func=lambda message: message.text == 'Новые заявки') -def appls(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn_accept = types.KeyboardButton ('❤️-Принять') - btn_refuse = types.KeyboardButton ('Отклонить') - btn_communicate = types.KeyboardButton ('💬-Связаться') - markup.row(btn_accept, btn_communicate, btn_refuse) - btn_menu=(types.KeyboardButton('Главное меню')) - markup.row(btn_menu) - - bot.send_message(message.chat.id, - f'Просмотр заявок:', parse_mode='html', reply_markup=markup) - - -# ---------------------------ИЗМЕНЕНИЕ ИНФОРМАЦИИ О СТУДИИ--------------------------- -@bot.message_handler(content_types=['photo']) -def handle_image(message): - """ - Обрабатываем фото от пользователя. - """ - - # Шаг 1: Получаем file_id самого большого фото - file_id = message.photo[-1].file_id - - # Шаг 2: Скачиваем фото с серверов Telegram - file_info = bot.get_file(file_id) - downloaded = bot.download_file(file_info.file_path) - -@bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') -def edit_info(message): - -class StudioCreationStates(StatesGroup): -""" - Класс состояний для создания студии. - Наследуется от StatesGroup - специального класса из telebot для FSM. - - Каждое состояние - это шаг в диалоге с пользователем. - Порядок объявления важен для логики работы. - """ - name = State() # Шаг 1: Ожидаем название студии - promo_type = State() # Шаг 2: Ожидаем выбор типа промокода - promo_content = State() # Шаг 3: Ожидаем сам промокод (текст или файл) - description = State() # Шаг 4: Ожидаем описание студии - - bot.send_message(message.chat.id, 'Введите название студии') - studio_name = - studio_promo = - studio_description = - - -bot.polling(none_stop=True) \ No newline at end of file diff --git a/User.py b/User.py deleted file mode 100644 index 6b76db2..0000000 --- a/User.py +++ /dev/null @@ -1,112 +0,0 @@ -from gettext import textdomain - -import telebot -from telebot import types -import sqlite3 - -bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") - -#====================== -#=========USER========= -#====================== - - -@bot.message_handler(commands=['start']) -def start(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn1 = types.KeyboardButton('Каталог студий') - markup.row(btn1) - btn2=types.KeyboardButton('Мои заявки') - btn3 = types.KeyboardButton('Главное меню') - markup.row(btn2, btn3) - - bot.send_message(message.chat.id, - f'Привет, {message.from_user.first_name}, в этом боте ты можешь узнать про творческие студии' - f' ГУАП и стать частью активной студенческой жизни!', parse_mode='html', reply_markup=markup) - -# ДАЛЕЕ ВЕТКА : КАТАЛОГ СТУДИЙ - -@bot.message_handler(func=lambda message: message.text == 'Каталог студий') -def inline_button_catalog(message): - markup = types.InlineKeyboardMarkup() - btn_orgmero = types.InlineKeyboardButton("Студия организации мероприятий", callback_data='orgMero') - markup.row(btn_orgmero) - btn_tehguap = types.InlineKeyboardButton("Студия технического обеспечения мероприятий", callback_data='tehGuap') - markup.row(btn_tehguap) - btn_muzguap = types.InlineKeyboardButton("МУЗГУАП", callback_data='muzGuap') - markup.row(btn_muzguap) - btn_dance = types.InlineKeyboardButton("Танцевальная студия", callback_data='dance') - markup.row(btn_dance) - btn_theatr= types.InlineKeyboardButton("Театральная студия", callback_data='theatr') - markup.row(btn_theatr) - btn_comedy = types.InlineKeyboardButton("КВН", callback_data='comedy') - markup.row(btn_comedy) - btn_vedush = types.InlineKeyboardButton("Студия ведущих", callback_data='vedush') - markup.row(btn_vedush) - btn_media = types.InlineKeyboardButton("Медиацентр", callback_data='media') - markup.row(btn_media) - - bot.send_message(message.chat.id, 'Нажми на студию для просмотра:', reply_markup=markup) - -# @bot.callback_query_handler(func=lambda callback: callback.data == 'orgMero') -# def orgMeroInfo(callback): - -# ДАЛЕЕ ВЕТКА : МОИ ЗАЯВКИ - -@bot.message_handler(func=lambda message: message.text == 'Мои заявки') -def membership_appl(message): - bot.send_message(message.chat.id, 'Вот статус твоих заявок:') - - -#Данные пользователя (для себя сделала) -@bot.message_handler(commands=['user']) -def main(message): - - text=message - if not isinstance(text, str): - text = str(text) - - message_parts = split_message(text) - - # Отправляем каждую часть отдельным сообщением - for part in message_parts: - bot.send_message(message.chat.id, part) - - -def split_message(text, max_length=4096): - """Разделяет текст на части не превышающие max_length""" - return [text[i:i+max_length] for i in range(0, len(text), max_length)] - - -# @bot.message_handler(content_types=['photo']) -# def get_photo(message): -# markup = types.InlineKeyboardMarkup() -# btn1 = types.InlineKeyboardButton('Duuuuura', url='https://t.me/+lVcKrLQJxH9hODYy') -# markup.row(btn1) -# btn2 = types.InlineKeyboardButton('Удалить фото!', callback_data='delete') -# btn3 = types.InlineKeyboardButton('Изменить', callback_data='edit') -# markup.row(btn2,btn3) -# -# bot.reply_to(message, "Спасибо, подрочил", reply_markup = markup) -# -# @bot.callback_query_handler(func=lambda callback:True) -# def callback_message(callback): -# if callback.data == 'delete': -# bot.delete_message(callback.message.chat.id, callback.message.message_id-1) -# elif callback.data == 'edit': -# bot.edit_message_text('Падем выдим', callback.message.chat.id, callback.message.message_id) -# -# -# -# -# def on_click(message): -# if message.text.lower == 'вступить в табор': -# bot.send_message(message.chat.id, 'Добро пожаловать к дурам, тварина') -# elif message.text.lower == 'аборт': -# bot.send_message(message.chat.id, 'Ну удалил и чо') -# -# -# - - -bot.polling(none_stop=True) \ No newline at end of file From 251393b782961c30390a68e217f75ae42868cde3 Mon Sep 17 00:00:00 2001 From: Anastasia Lukina Date: Mon, 8 Dec 2025 17:10:18 +0300 Subject: [PATCH 05/12] Database added to .venv --- .venv/student_studios_bot (1).db | Bin 0 -> 57344 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .venv/student_studios_bot (1).db diff --git a/.venv/student_studios_bot (1).db b/.venv/student_studios_bot (1).db new file mode 100644 index 0000000000000000000000000000000000000000..a3c614d77baf81d0aeeaf3f0eb2038f893050980 GIT binary patch literal 57344 zcmeI5-)~b#7RT?+4N09}u2ik870NPLLP^x5G)Y+&q^g?@rp*FL2+mSgyRsbn0!E2# z{DUp~LTnlc0u`i+M_PWUeW@y$fEAMjr|SL#bKm-~+Sh&DO5HzT&zbA%UvX#?RcJ}S zPHf*hb7tn8&pCJQ%y<%?I(a;jGR4_gVlJ2x!`kbbu4}&+LesQ%dUlnb2EBLCvzeaG zLw62d@ZPSy_4kXN9skkVn!ne&{?&2M^^cBl>z{T#+_k53ruqBM;pPkc^MV?X1p-?j zaMjb*>e;_vU-)Y(I5TS|FV4;cQs!JdpiTor8xoAsDc^|SC;TISIPMeP4f$Tt+vf5n zQ|WLd7KntsuCZ~y?|t7?Kcz;4bEenj_kHLW;}i6pK7L$GkB^<4_Hl05OokGXcq$T$ z+CrQdibYewP%2qTi6>%nu|WKMEM?cv)v=_Vekl?*Yttji096pVL@k$85l4JSN2ZVa z#UPhEZwAAGbka=JYa5APqDwsxiiI7e%eqcZjhz^oIwOwx&h%E-r||wuWONL*!+kv zvo67ODyGf@8@M-c)h_BsMz>PM;D%b82iz^5k=>hWq1>Dd4BGxqHfOZ9R?qMuJ+oJ- z83}(Bn2nta1mp49NGQmztvU=OQ^8a^$rA0Qs2UqT;`>lEC?+PxMdPBq)ya`?-_=1^ ztLM!(^=rRYAsmk-QfFhc6pUvh6yJ5HZ8gzcmr0Q<=)vX4C&G!ID(BZaykCjes#7lI z1iUSjGH1oUdY2foo`qF0qw*<<6PwSl%6xADLxJ+Lg(qS=;@Z>T=sEKE%w?ws+$3H@u{jPKWu)zRr|8QLmIh(xs8X?_rgK$&Yj3HCr;c^uH!ZE6 zef#tqSL%Z;SrtCX*3AMdSqi5r;Vpy}_mo8PM|hX-pPTrjy6tc<6|Czb#K3^~<0V&r)cEK>kI^9_7 zV=9@RnTw?8g4j|Ql#UaDEtaY2w(4B+g5YTod}UA76th#4y_HW?FQ(0Ch(~rjf^be-YNER?vzPBW z{Uq8)BgrG4-MjULkX@&Jzb{!=9tyT}wWuNIKxd=D!T=3qetZZqRH}4W_A-q~xyi8? zHCY;c+WKJ3`7@0RRv)r5;mD)oY;ezn|BRgI49kY|DND8^1Y zo1UEwl+(&{0XvgM_E9qsP0!7kiK_ceA~st;)+zT>96EG0^*H^Ml2yHvlS4OK9(}7D zFn2!XyKE?7ZlUvN>+7UI*WF)h^os`wfB*=900@8p2!H?xfB*=900@A<{{w-OdaJf4 zpm~Pv1;?3KO5TxA<(e$0710c>iPGw*m6b(nUdRWuNJ>dEFW0P_s>IOSLk9*12L}cZ zh@s)bgKr-`@D9)ajn2cG`>n3O&>J2g00JNY0w4eaAOHd&00JNY0viZq_ZS^o=5%(N za$MS8`G96$S7|m@pj|0E>#`Qqr0hB!WoVa5Q9hAHL2r*$!aBWDmJ*>|ENgT+PnqlV zN)=G~f_x^t@}aydzYy|M`G{tEpL^wJRM&HoTcXz-Z+}^)?JrO1@Vd+ip6%Hx$buN4 zQ_9K9Rh|)Aw}f1^KB2~oGT*p4N~XOutdyEHE-B@nTQ}ueKUYOJAPKw~-}=+oQQX?r+>#I>7@3KmY_l00ck)1V8`;KmY_l z00e$H1THjZPV4ezEa@2U8dr3=-L;XX_nI??i;8b|9m1aff2X;AXHPG_fi{^Zj3zLc+um!DY~`CJy|620cE+d?kW_r5=+!|O69 ztc;bluF`2i7Q_IZ(qUe%Qf}6|CFH90iF{0D^Fpr4yfsgU>-1W%Zd+L@Ldh$tqCD3_ zE##?<=hjWR*3VT@n5XqinXZP;F=#uGAsmF$^kRjdV;S|@#1$etWow@Sy)l-BFAsOsa=OtUCg z1SMq!o#(7YC0C8p`R2ULq)uKpvYi#1s0!;E6CVJ;Z zjgD8_ZO|SBKmY_l00ck)1V8`;KmY_l00e$n1iFm9dSx$$(P4D!&Mt Date: Tue, 9 Dec 2025 22:57:46 +1000 Subject: [PATCH 06/12] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=20=D0=B2=20Colab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...201\320\276\320\262\320\260\321\217.ipynb" | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 "\320\272\321\203\321\200\321\201\320\276\320\262\320\260\321\217.ipynb" diff --git "a/\320\272\321\203\321\200\321\201\320\276\320\262\320\260\321\217.ipynb" "b/\320\272\321\203\321\200\321\201\320\276\320\262\320\260\321\217.ipynb" new file mode 100644 index 0000000..c46696c --- /dev/null +++ "b/\320\272\321\203\321\200\321\201\320\276\320\262\320\260\321\217.ipynb" @@ -0,0 +1,59 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [], + "authorship_tag": "ABX9TyOvtb5h6X+Er4UdtIIApWWB", + "include_colab_link": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "biU6qS7vJ9ke", + "outputId": "d9daad1a-8a25-4f8e-a67c-e9a310813b4a", + "colab": { + "base_uri": "https://localhost:8080/" + } + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Cloning into 'Coursework-'...\n", + "remote: Enumerating objects: 33, done.\u001b[K\n", + "remote: Counting objects: 100% (33/33), done.\u001b[K\n", + "remote: Compressing objects: 100% (27/27), done.\u001b[K\n", + "remote: Total 33 (delta 10), reused 21 (delta 2), pack-reused 0 (from 0)\u001b[K\n", + "Receiving objects: 100% (33/33), 15.76 KiB | 7.88 MiB/s, done.\n", + "Resolving deltas: 100% (10/10), done.\n" + ] + } + ], + "source": [ + "!git clone https://github.com/Rina969/Coursework-" + ] + } + ] +} \ No newline at end of file From 00c8fb9448ecc7e248f1aa480813f1bc7e497787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AF=D0=BA=D1=83=D0=BD=D0=B8=D0=BD=D0=B0=20=D0=9C=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B0?= Date: Wed, 10 Dec 2025 10:04:26 +1000 Subject: [PATCH 07/12] =?UTF-8?q?=D0=B0=D0=BD=D0=BA=D0=B5=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .venv/User.py | 112 ----- .venv/Users.py | 475 ++++++++++++++++++ ...20\277\321\200\320\276\321\201\321\213.py" | 224 +++++++++ ...20\260\320\275\320\272\320\265\321\202.py" | 221 ++++++++ 4 files changed, 920 insertions(+), 112 deletions(-) delete mode 100644 .venv/User.py create mode 100644 .venv/Users.py create mode 100644 ".venv/\320\262\320\276\320\277\321\200\320\276\321\201\321\213.py" create mode 100644 ".venv/\321\201\320\261\320\276\321\200\321\211\320\270\320\272_\320\260\320\275\320\272\320\265\321\202.py" diff --git a/.venv/User.py b/.venv/User.py deleted file mode 100644 index 14a9b61..0000000 --- a/.venv/User.py +++ /dev/null @@ -1,112 +0,0 @@ -from gettext import textdomain - -import telebot -from telebot import types -import sqlite3 - -bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") - -#====================== -#=========USER========= -#====================== - - -@bot.message_handler(commands=['start']) -def start(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn1 = types.KeyboardButton('Каталог студий') - markup.row(btn1) - btn2=types.KeyboardButton('Мои заявки') - btn3 = types.KeyboardButton('Главное меню') - markup.row(btn2, btn3) - - bot.send_message(message.chat.id, - f'Привет, {message.from_user.first_name}, в этом боте ты можешь узнать про творческие студии' - f' ГУАП и стать частью активной студенческой жизни!', parse_mode='html', reply_markup=markup) - -# ДАЛЕЕ ВЕТКА : КАТАЛОГ СТУДИЙ - -@bot.message_handler(func=lambda message: message.text == 'Каталог студий') -def inline_button_catalog(message): - markup = types.InlineKeyboardMarkup() - btn_orgmero = types.InlineKeyboardButton("Студия организации мероприятий", callback_data='orgMero') - markup.row(btn_orgmero) - btn_tehguap = types.InlineKeyboardButton("Студия технического обеспечения мероприятий", callback_data='tehGuap') - markup.row(btn_tehguap) - btn_muzguap = types.InlineKeyboardButton("МУЗГУАП", callback_data='muzGuap') - markup.row(btn_muzguap) - btn_dance = types.InlineKeyboardButton("Танцевальная студия", callback_data='dance') - markup.row(btn_dance) - btn_theatr= types.InlineKeyboardButton("Театральная студия", callback_data='theatr') - markup.row(btn_theatr) - btn_comedy = types.InlineKeyboardButton("КВН", callback_data='comedy') - markup.row(btn_comedy) - btn_vedush = types.InlineKeyboardButton("Студия ведущих", callback_data='vedush') - markup.row(btn_vedush) - btn_media = types.InlineKeyboardButton("Медиацентр", callback_data='media') - markup.row(btn_media) - - bot.send_message(message.chat.id, 'Нажми на студию для просмотра:', reply_markup=markup) - -# @bot.callback_query_handler(func=lambda callback: callback.data == 'orgMero') -# def orgMeroInfo(callback): - -# ДАЛЕЕ ВЕТКА : МОИ ЗАЯВКИ - -@bot.message_handler(func=lambda message: message.text == 'Мои заявки') -def membership_appl(message): - bot.send_message(message.chat.id, 'Вот статус твоих заявок:') - - -#Данные пользователя (для себя сделала) -@bot.message_handler(commands=['user']) -def main(message): - - text=message - if not isinstance(text, str): - text = str(text) - - message_parts = split_message(text) - - # Отправляем каждую часть отдельным сообщением - for part in message_parts: - bot.send_message(message.chat.id, part) - - -def split_message(text, max_length=4096): - """Разделяет текст на части не превышающие max_length""" - return [text[i:i+max_length] for i in range(0, len(text), max_length)] - - -# @bot.message_handler(content_types=['photo']) -# def get_photo(message): -# markup = types.InlineKeyboardMarkup() -# btn1 = types.InlineKeyboardButton('Duuuuura', url='https://t.me/+lVcKrLQJxH9hODYy') -# markup.row(btn1) -# btn2 = types.InlineKeyboardButton('Удалить фото!', callback_data='delete') -# btn3 = types.InlineKeyboardButton('Изменить', callback_data='edit') -# markup.row(btn2,btn3) -# -# bot.reply_to(message, "Спасибо, подрочил", reply_markup = markup) -# -# @bot.callback_query_handler(func=lambda callback:True) -# def callback_message(callback): -# if callback.data == 'delete': -# bot.delete_message(callback.message.chat.id, callback.message.message_id-1) -# elif callback.data == 'edit': -# bot.edit_message_text('Падем выдим', callback.message.chat.id, callback.message.message_id) -# -# -# -# -# def on_click(message): -# if message.text.lower == 'вступить в табор': -# bot.send_message(message.chat.id, 'Добро пожаловать к дурам, тварина') -# elif message.text.lower == 'аборт': -# bot.send_message(message.chat.id, 'Ну удалил и чо') -# -# -# - - -bot.polling(none_stop=True) \ No newline at end of file diff --git a/.venv/Users.py b/.venv/Users.py new file mode 100644 index 0000000..13011d3 --- /dev/null +++ b/.venv/Users.py @@ -0,0 +1,475 @@ +import telebot +from telebot import types +import sqlite3 +from datetime import datetime +from typing import Dict, Any, List, Tuple +import logging +import json +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import CallbackContext, ConversationHandler + +from .сборщик_анкет import QuestionnaireBuilder +from .вопросы import get_question_obj + +# Настройка логирования +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") +DB_PATH = "student_studios_bot (1).db" + +def get_connection() -> sqlite3.Connection: + """Возвращает соединение с БД""" + return sqlite3.connect(DB_PATH, check_same_thread=False) + +def load_active_studios() -> List[Tuple[int, str]]: + """Загрузка списка активных студий""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT studio_id, name FROM studios WHERE is_active = 1 ORDER BY studio_id") + return cursor.fetchall() + except Exception as e: + logger.error(f"Error loading studios: {e}") + return [] + +@bot.message_handler(commands=['start']) +def start(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + btn3 = types.KeyboardButton('Подать заявку') + markup.row(btn2, btn3) + + bot.send_message(message.chat.id, + f'Привет, в этом боте ты можешь узнать про творческие студии' + f' ГУАП и стать частью активной студенческой жизни!', parse_mode='html', reply_markup=markup) + +# ВЕТКА КАТАЛОГ СТУДИЙ + +@bot.message_handler(func=lambda message: message.text == 'Каталог студий') +def inline_button_catalog(message): + studios = load_active_studios() + + if not studios: + bot.send_message(message.chat.id, 'На данный момент активных студий нет.') + return + + markup = types.InlineKeyboardMarkup() + for studio_id, name in studios: + btn = types.InlineKeyboardButton(name, callback_data=f'info:{studio_id}') + markup.row(btn) + + bot.send_message(message.chat.id, 'Нажми на студию для просмотра:', reply_markup=markup) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('info:')) +def show_studio_info(call): + """Показывает информацию о студии""" + studio_id = int(call.data.split(':')[1]) + + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT name, description, contacts + FROM studios + WHERE studio_id = ? + """, (studio_id,)) + result = cursor.fetchone() + + if result: + name, description, contacts = result + text = f"{name}\n\n{description or 'Описание скоро появится.'}\n\nКонтакты: {contacts or '—'}" + + # Добавляем кнопку подачи заявки + markup = types.InlineKeyboardMarkup() + btn_apply = types.InlineKeyboardButton("📝 Подать заявку", callback_data=f'apply:{studio_id}') + markup.row(btn_apply) + + bot.send_message(call.message.chat.id, text, parse_mode='html', reply_markup=markup) + else: + bot.answer_callback_query(call.id, "Студия не найдена") + except Exception as e: + logger.error(f"Error showing studio info: {e}") + bot.answer_callback_query(call.id, "Ошибка при загрузке информации") + + +class QuestionnaireHandler: + """Обработчик анкет с автозаполнением""" + + def __init__(self, db_connection): + self.db = db_connection + + def start_questionnaire(self, update: Update, context: CallbackContext) -> int: + """Начало анкеты для выбранной студии""" + query = update.callback_query + query.answer() + + # Получаем ID студии из cтадии выбора студии + studio_id = int(query.data.split('_')[-1]) + + # Сохраняем данные в контексте + context.user_data['studio_id'] = studio_id + context.user_data['questionnaire'] = QuestionnaireBuilder.get_questionnaire_for_studio(studio_id) + context.user_data['current_question_index'] = 0 + context.user_data['answers'] = {} + + # Получаем данные пользователя для автозаполнения + user_id = update.effective_user.id + user_data = self.get_user_data(user_id) + context.user_data['user_data'] = user_data + + # Проверяем возможность автозаполнения + if QuestionnaireBuilder.can_autofill(studio_id, user_data): + return self.offer_autofill(update, context, studio_id, user_data) + else: + return self.start_manual_filling(update, context, studio_id) + + def get_user_data(self, user_id: int) -> Dict[str, Any]: + """Получает данные пользователя из БД для автозаполнения""" + try: + cursor = self.db.cursor() + cursor.execute(""" + SELECT full_name, student_group, phone, email + FROM users + WHERE telegram_id = %s + """, (user_id,)) + + result = cursor.fetchone() + cursor.close() + + if result: + return { + 'full_name': result[0] or '', + 'student_group': result[1] or '', + 'phone': result[2] or '', + 'email': result[3] or '' + } + except Exception as e: + logger.error(f"Error getting user data: {e}") + + return {} + + def offer_autofill(self, update: Update, context: CallbackContext, + studio_id: int, user_data: Dict[str, Any]) -> int: + """Предлагает автозаполнение""" + query = update.callback_query + + # Получаем поля для автозаполнения + autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) + autofill_info = [] + + for field in autofill_fields: + value = user_data.get(field, '') + if value: + field_name = { + 'full_name': 'ФИО', + 'student_group': 'Группа', + 'phone': 'Телефон', + 'email': 'Email' + }.get(field, field) + autofill_info.append(f"{field_name}: {value}") + + message = ( + f"Хотите использовать основную информацию из предыдущих анкет для автозаполнения?" + ) + + keyboard = [ + [ + InlineKeyboardButton("✅ Использовать мои данные", callback_data='use_autofill'), + InlineKeyboardButton("✏️ Заполнить вручную", callback_data='manual_fill') + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + query.edit_message_text(message, reply_markup=reply_markup, parse_mode='Markdown') + + return 1 # Состояние ожидания выбора + + def handle_autofill_choice(self, update: Update, context: CallbackContext) -> int: + """Обработка выбора автозаполнения""" + query = update.callback_query + query.answer() + + studio_id = context.user_data['studio_id'] + user_data = context.user_data['user_data'] + answers = context.user_data['answers'] + + if query.data == 'use_autofill': + # Автозаполняем данные + autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) + + for field in autofill_fields: + value = user_data.get(field) + if value: + # Находим ID вопроса, соответствующего этому полю + questions = QuestionnaireBuilder.get_question_objects(studio_id) + for q in questions: + if q.autofill_field == field: + answers[q.id] = value + break + + query.edit_message_text( + "✅ Данные автозаполнены.\n" + "Продолжаем с оставшимися вопросами..." + ) + + # Начинаем опрос с первого незаполненного вопроса + return self.ask_next_question(update, context) + + def start_manual_filling(self, update: Update, context: CallbackContext, + studio_id: int) -> int: + """Начинает ручное заполнение анкеты""" + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + + if update.callback_query: + update.callback_query.edit_message_text( + f"Начинаем заполнение анкеты для студии *{studio_name}*.\n\n" + "Пожалуйста, ответьте на несколько вопросов:", + parse_mode='Markdown' + ) + else: + update.message.reply_text( + f"Начинаем заполнение анкеты для студии *{studio_name}*.\n\n" + "Пожалуйста, ответьте на несколько вопросов:", + parse_mode='Markdown' + ) + + return self.ask_next_question(update, context) + + def ask_next_question(self, update: Update, context: CallbackContext) -> int: + """Задает следующий вопрос""" + studio_id = context.user_data['studio_id'] + question_ids = QuestionnaireBuilder.get_question_sequence(studio_id) + current_index = context.user_data['current_question_index'] + answers = context.user_data['answers'] + + # Пропускаем уже заполненные вопросы (при автозаполнении) + while current_index < len(question_ids): + qid = question_ids[current_index] + if qid not in answers: + break + current_index += 1 + + # Проверяем, не заполнили ли мы все вопросы + if current_index >= len(question_ids): + return self.show_summary(update, context) + + # Сохраняем текущий индекс + context.user_data['current_question_index'] = current_index + context.user_data['current_question_id'] = qid + + # Получаем и вызываем обработчик вопроса + handler = QuestionnaireBuilder.build_questionnaire_handlers(studio_id)[current_index] + + if update.callback_query: + # Если это callback, отправляем новое сообщение + update.callback_query.message.reply_text("Следующий вопрос:") + handler(update, context) + else: + handler(update, context) + + # Возвращаем состояние для обработки ответа + return current_index + 100 # Уникальное состояние для каждого вопроса + + def process_answer(self, update: Update, context: CallbackContext) -> int: + """Обработка ответа на вопрос""" + current_qid = context.user_data.get('current_question_id') + answer_text = update.message.text.strip() + + # Для вопросов с inline-кнопками + if update.callback_query: + answer_text = update.callback_query.data + if answer_text.startswith('activeness_'): + answer_text = 'Да' if answer_text.endswith('yes') else 'Нет' + + # Получаем объект вопроса для валидации + q_obj = get_question_obj(current_qid) + + if q_obj: + is_valid, error_msg = q_obj.validate(answer_text) + if not is_valid: + update.message.reply_text(f"❌ {error_msg}\n\nПопробуйте еще раз:") + return context.user_data['current_question_index'] + 100 + + # Сохраняем ответ + context.user_data['answers'][current_qid] = answer_text + + # Переходим к следующему вопросу + context.user_data['current_question_index'] += 1 + + return self.ask_next_question(update, context) + + def show_summary(self, update: Update, context: CallbackContext) -> int: + """Показывает сводку по ответам""" + studio_id = context.user_data['studio_id'] + answers = context.user_data['answers'] + + # Валидируем ответы + is_valid, errors = QuestionnaireBuilder.validate_answers(studio_id, answers) + + if not is_valid: + error_text = "❌ *Ошибки в заполнении:*\n\n" + for qid, error in errors.items(): + q_obj = get_question_obj(qid) + error_text += f"• {q_obj.text}: {error}\n" + + error_text += "\nПожалуйста, заполните анкету заново." + + update.message.reply_text(error_text, parse_mode='Markdown') + context.user_data['current_question_index'] = 0 + context.user_data['answers'] = {} + return self.ask_next_question(update, context) + + + + # Генерируем сводку + summary = QuestionnaireBuilder.generate_summary(studio_id, answers) + + # Создаем клавиатуру с кнопками + keyboard = [ + [ + InlineKeyboardButton("✅ Подтвердить и отправить", + callback_data='confirm_application'), + InlineKeyboardButton("✏️ Заполнить заново", + callback_data='restart_questionnaire') + ], + [ + InlineKeyboardButton("🚫 Отменить и вернуться в каталог", + callback_data='cancel_to_catalog') + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + if update.callback_query: + update.callback_query.edit_message_text( + f"{summary}\n\n" + f"*Всё верно? Подтвердите отправку заявки или внесите изменения.*", + reply_markup=reply_markup, + parse_mode='Markdown' + ) + else: + update.message.reply_text( + f"{summary}\n\n" + f"*Всё верно? Подтвердите отправку заявки или внесите изменения.*", + reply_markup=reply_markup, + parse_mode='Markdown' + ) + + return 'CONFIRMATION_STATE' + + def handle_confirmation_action(self, update: Update, context: CallbackContext) -> int: + """Обработка действий на этапе подтверждения""" + query = update.callback_query + query.answer() + + action = query.data + + if action == 'confirm_application': + return self.confirm_application(update, context) + + elif action == 'restart_questionnaire': + return self.restart_questionnaire(update, context) + + elif action == 'cancel_to_catalog': + return self.cancel_to_catalog(update, context) + + return 'CONFIRMATION_STATE' + + def confirm_application(self, update: Update, context: CallbackContext) -> int: + """Подтверждение и сохранение анкеты в БД""" + query = update.callback_query + + studio_id = context.user_data['studio_id'] + user_id = update.effective_user.id + answers = context.user_data['answers'] + user_data = context.user_data.get('user_data', {}) + + # Сохраняем анкету в БД + success, application_id = self.save_application_to_db( + user_id=user_id, + studio_id=studio_id, + answers=answers, + user_data=user_data + ) + + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + + if success: + # Создаем клавиатуру с кнопкой "Главное меню" + keyboard = [ + [InlineKeyboardButton("🏠 Главное меню", callback_data='go_to_main_menu')] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + success_message = ( + f"✅ Ваша заявка в студию *{studio_name}* отправлена.\n" + ) + + query.edit_message_text( + success_message, + reply_markup=reply_markup, + parse_mode='Markdown' + ) + + # Очищаем данные анкеты, но сохраняем studio_id для возможного повторного заполнения + context.user_data.pop('answers', None) + context.user_data.pop('current_question_index', None) + context.user_data.pop('current_question_id', None) + + return 'AFTER_CONFIRMATION' + + else: + # В случае ошибки + keyboard = [ + [InlineKeyboardButton("🔄 Попробовать снова", + callback_data='retry_save_application')], + [InlineKeyboardButton("🚫 Отменить", callback_data='cancel_to_catalog')] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + error_message = ( + f"Не удалось сохранить заявку в студию *{studio_name}*.\n" + ) + + query.edit_message_text( + error_message, + reply_markup=reply_markup, + parse_mode='Markdown' + ) + + return 'CONFIRMATION_STATE' + + def restart_questionnaire(self, update: Update, context: CallbackContext) -> int: + """Перезапуск заполнения анкеты""" + query = update.callback_query + + studio_id = context.user_data['studio_id'] + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + + # Полностью сбрасываем данные анкеты + context.user_data['answers'] = {} + context.user_data['current_question_index'] = 0 + context.user_data.pop('current_question_id', None) + + return self.start_manual_filling(update, context, studio_id) + + def cancel_to_catalog(self, update: Update, context: CallbackContext) -> int: + """Отмена анкеты и возврат в каталог студий""" + query = update.callback_query + + studio_id = context.user_data['studio_id'] + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + + # Полностью сбрасываем данные анкеты + context.user_data['answers'] = {} + context.user_data['current_question_index'] = 0 + context.user_data.pop('current_question_id', None) + + return self.inline_button_catalog(message) + + diff --git "a/.venv/\320\262\320\276\320\277\321\200\320\276\321\201\321\213.py" "b/.venv/\320\262\320\276\320\277\321\200\320\276\321\201\321\213.py" new file mode 100644 index 0000000..2759065 --- /dev/null +++ "b/.venv/\320\262\320\276\320\277\321\200\320\276\321\201\321\213.py" @@ -0,0 +1,224 @@ +from typing import Dict, Any, Callable, Optional +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import CallbackContext +import re + +class Question: + """Класс для хранения вопроса""" + def __init__(self, + question_id: str, + text: str, + question_type: str = 'text', + is_required: bool = True, + autofill_field: Optional[str] = None, + validation_func: Optional[Callable] = None, + options: Optional[list] = None, + keyboard: Optional[list] = None): + self.id = question_id + self.text = text + self.type = question_type # text, phone, email, boolean, select, multi_select + self.is_required = is_required + self.autofill_field = autofill_field # Поле для автозаполнения из БД + self.validation_func = validation_func + self.options = options or [] + self.keyboard = keyboard + + def validate(self, answer: str) -> tuple[bool, str]: + """Валидация ответа""" + answer = answer.strip() + + if self.is_required and not answer: + return False, "Это обязательный вопрос" + + if self.validation_func: + return self.validation_func(answer) + + # Базовая валидация по типу + if self.type == 'phone': + phone_digits = re.sub(r'[^\d]', '', answer) + if len(phone_digits) < 10: + return False, "Введите корректный номер телефона" + + elif self.type == 'email': + if '@' not in answer or '.' not in answer: + return False, "Введите корректный email" + + elif self.type == 'boolean': + if answer.lower() not in ['да', 'нет']: + return False, "Ответьте 'да' или 'нет'" + + elif self.type == 'select': + if answer not in self.options: + return False, f"Выберите вариант из списка: {', '.join(self.options)}" + + return True, "" + + +# ------ Общие вопросы------ +def ask_full_name(update: Update, context: CallbackContext) -> None: + """Вопрос: ФИО""" + update.message.reply_text("1. 📝 Укажите ваше ФИО (полностью):") + +def ask_group(update: Update, context: CallbackContext) -> None: + """Вопрос: Группа""" + update.message.reply_text("2. 🎓 Укажите номер вашей группу:") + +def ask_phone(update: Update, context: CallbackContext) -> None: + """Вопрос: Телефон""" + update.message.reply_text("3. 📱 Укажите ваш номер телефона:") + +def ask_email(update: Update, context: CallbackContext) -> None: + """Вопрос: Email""" + update.message.reply_text("4. 📧 Укажите ваш email адрес:") + +def ask_experience(update: Update, context: CallbackContext) -> None: + """Вопрос: Опыт в деятельности""" + update.message.reply_text("5. 💼 Расскажите о вашем опыте в данной деятельности:") + +def ask_motivation(update: Update, context: CallbackContext) -> None: + """Вопрос: Мотивация""" + update.message.reply_text("6. 🎯 Почему вы хотите вступить в эту студию?") + +def ask_expectations(update: Update, context: CallbackContext) -> None: + """Вопрос: Ожидания""" + update.message.reply_text("7. 🌟 Какие у вас ожидания от участия в студии?") + +def ask_activeness(update: Update, context: CallbackContext) -> None: + """Вопрос: Участие в мероприятиях""" + keyboard = [ + [InlineKeyboardButton("Да", callback_data="activeness_yes"), + InlineKeyboardButton("Нет", callback_data="activeness_no")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text( + "8. 📢 Готовы ли вы участвовать в мероприятиях студии?", + reply_markup=reply_markup + ) + +# ------ дополнительные опросы ------ + +def ask_dance_style(update: Update, context: CallbackContext) -> None: + """Вопрос: Стиль танца""" + keyboard = [ + [InlineKeyboardButton("Хип-хоп", callback_data="dance_hiphop")], + [InlineKeyboardButton("Бальные танцы", callback_data="dance_ballroom")], + [InlineKeyboardButton("Современные", callback_data="dance_modern")], + [InlineKeyboardButton("Народные", callback_data="dance_folk")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text( + "9. 💃 Какой танцевальный стиль вам интересен?", + reply_markup=reply_markup + ) + +def ask_music_instrument(update: Update, context: CallbackContext) -> None: + """Вопрос: Музыкальный инструмент""" + update.message.reply_text("10. 🎵 На каком инструменте вы играете (если играете)?") + +def ask_singing_experience(update: Update, context: CallbackContext) -> None: + """Вопрос: Опыт пения""" + update.message.reply_text("11. 🎤 Есть ли у вас опыт пения? Если да, опишите его:") + +def ask_theater_role(update: Update, context: CallbackContext) -> None: + """Вопрос: Предпочитаемые роли в театре""" + keyboard = [ + [InlineKeyboardButton("Драматические", callback_data="role_drama")], + [InlineKeyboardButton("Комические", callback_data="role_comedy")], + [InlineKeyboardButton("Характерные", callback_data="role_character")], + [InlineKeyboardButton("Не знаю", callback_data="role_unknown")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text( + "12. 🎭 Какие роли вам интересны в театре?", + reply_markup=reply_markup + ) + +def ask_tech_skills(update: Update, context: CallbackContext) -> None: + """Вопрос: Технические навыки""" + update.message.reply_text("13. 💻 Какими техническими навыками вы владеете?") + +def ask_organizational_exp(update: Update, context: CallbackContext) -> None: + """Вопрос: Организационный опыт""" + update.message.reply_text("14. 📋 Был ли у вас опыт организации мероприятий? Опишите:") + +def ask_leadership(update: Update, context: CallbackContext) -> None: + """Вопрос: Лидерские качества""" + keyboard = [ + [InlineKeyboardButton("Да, есть опыт", callback_data="leadership_yes")], + [InlineKeyboardButton("Хочу развивать", callback_data="leadership_develop")], + [InlineKeyboardButton("Пока нет", callback_data="leadership_no")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text( + "15. 👨‍💼 Есть ли у вас лидерский опыт?", + reply_markup=reply_markup + ) + +def ask_project_ideas(update: Update, context: CallbackContext) -> None: + """Вопрос: Идеи для проектов""" + update.message.reply_text("16. 💡 Есть ли у вас идеи для проектов в студии?") + +def ask_time_commitment(update: Update, context: CallbackContext) -> None: + """Вопрос: Временные возможности""" + update.message.reply_text("17. ⏰ Сколько часов в неделю вы готовы уделять студии?") + +def ask_why_choose_us(update: Update, context: CallbackContext) -> None: + """Вопрос: Почему выбрали нас""" + update.message.reply_text("18. ❓ Почему вы выбрали именно нашу студию?") + +def ask_feedback_source(update: Update, context: CallbackContext) -> None: + """Вопрос: Откуда узнали о студии""" + keyboard = [ + [InlineKeyboardButton("От друзей", callback_data="source_friends")], + [InlineKeyboardButton("Соцсети", callback_data="source_social")], + [InlineKeyboardButton("Сайт вуза", callback_data="source_website")], + [InlineKeyboardButton("Объявление", callback_data="source_poster")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text( + "19. 📢 Откуда вы узнали о нашей студии?", + reply_markup=reply_markup + ) + +def ask_special_needs(update: Update, context: CallbackContext) -> None: + """Вопрос: Особые потребности""" + update.message.reply_text("20. ♿ Есть ли у вас особые потребности, которые мы должны учитывать?") + + +# ========== СЛОВАРЬ ВСЕХ ВОПРОСОВ ========== +ALL_QUESTIONS = { + # ID вопроса: (функция-обработчик, объект Question) + 'full_name': (ask_full_name, Question('full_name', 'ФИО', autofill_field='full_name')), + 'group': (ask_group, Question('group', 'Группа', autofill_field='student_group')), + 'phone': (ask_phone, Question('phone', 'Телефон', 'phone', autofill_field='phone')), + 'email': (ask_email, Question('email', 'Email', 'email', autofill_field='email')), + 'experience': (ask_experience, Question('experience', 'Опыт в деятельности')), + 'motivation': (ask_motivation, Question('motivation', 'Мотивация вступления')), + 'expectations': (ask_expectations, Question('expectations', 'Ожидания от студии')), + 'activeness': (ask_activeness, Question('activeness', 'Участие в мероприятиях', 'boolean')), + 'dance_style': (ask_dance_style, Question('dance_style', 'Танцевальный стиль', 'select', + options=['Хип-хоп', 'Бальные танцы', 'Современные', 'Народные'])), + 'music_instrument': (ask_music_instrument, Question('music_instrument', 'Музыкальный инструмент')), + 'singing_experience': (ask_singing_experience, Question('singing_experience', 'Опыт пения')), + 'theater_role': (ask_theater_role, Question('theater_role', 'Театральные роли', 'select', + options=['Драматические', 'Комические', 'Характерные', 'Не знаю'])), + 'tech_skills': (ask_tech_skills, Question('tech_skills', 'Технические навыки')), + 'organizational_exp': (ask_organizational_exp, Question('organizational_exp', 'Организационный опыт')), + 'leadership': (ask_leadership, Question('leadership', 'Лидерский опыт', 'select', + options=['Да, есть опыт', 'Хочу развивать', 'Пока нет'])), + 'project_ideas': (ask_project_ideas, Question('project_ideas', 'Идеи для проектов')), + 'time_commitment': (ask_time_commitment, Question('time_commitment', 'Временные возможности')), + 'why_choose_us': (ask_why_choose_us, Question('why_choose_us', 'Почему выбрали нас')), + 'feedback_source': (ask_feedback_source, Question('feedback_source', 'Источник информации', 'select', + options=['От друзей', 'Соцсети', 'Сайт вуза', 'Объявление'])), + 'special_needs': (ask_special_needs, Question('special_needs', 'Особые потребности', is_required=False)) +} + +def get_question_handler(question_id: str): + """Возвращает обработчик вопроса по ID""" + return ALL_QUESTIONS.get(question_id, (None, None))[0] + +def get_question_obj(question_id: str) -> Optional[Question]: + """Возвращает объект вопроса по ID""" + result = ALL_QUESTIONS.get(question_id) + return result[1] if result else None diff --git "a/.venv/\321\201\320\261\320\276\321\200\321\211\320\270\320\272_\320\260\320\275\320\272\320\265\321\202.py" "b/.venv/\321\201\320\261\320\276\321\200\321\211\320\270\320\272_\320\260\320\275\320\272\320\265\321\202.py" new file mode 100644 index 0000000..6777ec1 --- /dev/null +++ "b/.venv/\321\201\320\261\320\276\321\200\321\211\320\270\320\272_\320\260\320\275\320\272\320\265\321\202.py" @@ -0,0 +1,221 @@ +from typing import Dict, List, Any, Optional, Callable +from .вопросы import get_question_handler, get_question_obj, ALL_QUESTIONS + +class QuestionnaireBuilder: + """Сборщик анкет для разных студий""" + + STUDIO_QUESTIONNAIRES = { + # ID студии: [список ID вопросов из ALL_QUESTIONS] + 1: { # Танцевальная студия + 'name': 'Танцевальная студия', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone', # 3. Телефон + 'email', # 4. Email + 'dance_style', # 5. Стиль танца + 'experience', # 6. Опыт + 'motivation', # 7. Мотивация + 'activeness', # 8. Участие в мероприятиях + ] + }, + 2: { # КВН + 'name': 'КВН', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone', # 3. Телефон + 'email', # 4. Email + 'experience', # 5. Опыт + 'why_choose_us', # 6. Почему выбрали нас + 'time_commitment',# 7. Временные возможности + ] + }, + 3: { # Театральная студия + 'name': 'Театральная студия', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone', # 3. Телефон + 'email', # 4. Email + 'theater_role', # 5. Театральные роли + 'experience', # 6. Опыт + 'motivation', # 7. Мотивация + 'activeness', # 8. Участие в мероприятиях + ] + }, + 4: { # МУЗГУАП + 'name': 'МУЗГУАП', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone', # 3. Телефон + 'email', # 4. Email + 'music_instrument', # 5. Музыкальный инструмент + 'singing_experience', # 6. Опыт пения + 'expectations', # 7. Ожидания + 'activeness', # 8. Участие в мероприятиях + ] + }, + 5: { # Медиацентр + 'name': 'Медиацентр', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone', # 3. Телефон + 'email', # 4. Email + 'tech_skills', # 5. Технические навыки + 'experience', # 6. Опыт + 'project_ideas', # 7. Идеи для проектов + 'time_commitment',# 8. Временные возможности + ] + }, + 6: { # Техничка + 'name': 'Студия технического обеспечения мероприятий', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone', # 3. Телефон + 'email', # 4. Email + 'tech_skills', # 5. Технические навыки + 'organizational_exp', # 6. Организационный опыт + 'leadership', # 7. Лидерский опыт + 'feedback_source',# 8. Откуда узнали + ] + }, + 7: { # Студия организации мероприятий + 'name': 'Студия организации мероприятий', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone', # 3. Телефон + 'email', # 4. Email + 'organizational_exp', # 5. Организационный опыт + 'motivation', # 6. Мотивация + 'activeness', # 7. Участие в мероприятиях + 'tech_skills', # 8. Технические навыки + ] + }, + 8: { # студия ведущих + 'name': 'Студия ведущих', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone', # 3. Телефон + 'email', # 4. Email + 'experience', # 5. Опыт в деятельности + 'project_ideas', # 6. Идеи для проектов + 'expectations', # 7. Ожидания + 'time_commitment',# 8. Временные возможности + ] + }, + } + + @classmethod + def get_questionnaire_for_studio(cls, studio_id: int) -> Optional[Dict[str, Any]]: + """Получает конфигурацию анкеты для студии""" + return cls.STUDIO_QUESTIONNAIRES.get(studio_id) + + @classmethod + def get_studio_name(cls, studio_id: int) -> str: + """Получает название студии""" + config = cls.get_questionnaire_for_studio(studio_id) + return config.get('name', 'Неизвестная студия') if config else 'Неизвестная студия' + + @classmethod + def get_question_sequence(cls, studio_id: int) -> List[str]: + """Получает последовательность вопросов для студии""" + config = cls.get_questionnaire_for_studio(studio_id) + return config.get('questions', []) if config else [] + + @classmethod + def build_questionnaire_handlers(cls, studio_id: int) -> List[Callable]: + """Собирает список обработчиков вопросов для студии""" + question_ids = cls.get_question_sequence(studio_id) + handlers = [] + + for qid in question_ids: + handler = get_question_handler(qid) + if handler: + handlers.append(handler) + + return handlers + + @classmethod + def get_question_objects(cls, studio_id: int) -> List[Any]: + """Получает объекты вопросов для студии""" + question_ids = cls.get_question_sequence(studio_id) + questions = [] + + for qid in question_ids: + q_obj = get_question_obj(qid) + if q_obj: + questions.append(q_obj) + + return questions + + @classmethod + def get_autofill_fields(cls, studio_id: int) -> List[str]: + """Получает список полей для автозаполнения""" + questions = cls.get_question_objects(studio_id) + autofill_fields = [] + + for q in questions: + if q.autofill_field: + autofill_fields.append(q.autofill_field) + + # Убираем дубликаты + return list(set(autofill_fields)) + + @classmethod + def can_autofill(cls, studio_id: int, user_data: Dict[str, Any]) -> bool: + """Проверяет, можно ли автозаполнить анкету""" + autofill_fields = cls.get_autofill_fields(studio_id) + + if not autofill_fields: + return False + + # Проверяем, сколько полей можно автозаполнить + fillable_count = 0 + for field in autofill_fields: + value = user_data.get(field, '') + if value and str(value).strip(): + fillable_count += 1 + + return fillable_count == len(autofill_fields) + + @classmethod + def generate_summary(cls, studio_id: int, answers: Dict[str, str]) -> str: + """Генерирует сводку по ответам""" + questions = cls.get_question_objects(studio_id) + studio_name = cls.get_studio_name(studio_id) + + lines = [f"📋 *Анкета для студии: {studio_name}*\n"] + + for i, q in enumerate(questions, 1): + answer = answers.get(q.id, "❌ Не заполнено") + lines.append(f"\n{i}. *{q.text}*\n{answer}") + + return "\n".join(lines) + + @classmethod + def validate_answers(cls, studio_id: int, answers: Dict[str, str]) -> tuple[bool, Dict[str, str]]: + """Валидирует все ответы анкеты""" + questions = cls.get_question_objects(studio_id) + errors = {} + + for q in questions: + answer = answers.get(q.id, '').strip() + + if q.is_required and not answer: + errors[q.id] = "Обязательный вопрос не заполнен" + continue + + is_valid, error_msg = q.validate(answer) + if not is_valid: + errors[q.id] = error_msg + + return len(errors) == 0, errors + + + From 530316b54d7d4f713699f796edd01c223d3d34f7 Mon Sep 17 00:00:00 2001 From: Anastasia Lukina Date: Mon, 15 Dec 2025 10:33:18 +0300 Subject: [PATCH 08/12] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B8=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B2=20=D0=B1=D0=B4=20=D0=B0=D0=BD=D0=BA?= =?UTF-8?q?=D0=B5=D1=82=D0=B0,=20=D0=BD=D0=B0=D0=B7=D0=BD=D0=B0=D1=87?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D1=80=D1=83=D0=BA.=20=D1=81?= =?UTF-8?q?=D1=82=D1=83=D0=B4=D0=B8=D0=B8,=20=D0=BF=D0=BE=D1=87=D1=82?= =?UTF-8?q?=D0=B8=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D0=BE=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D0=BF=D1=80-=D0=B5=20=D1=80=D0=BE=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .venv/Admin.py | 148 ++++++- .venv/Dtbase_config.py | 2 + .venv/Head_of_studio.py | 9 +- .venv/Users.py | 475 --------------------- .venv/questions.py | 264 ++++++++++++ .venv/student_studios_bot (1).db | Bin 57344 -> 57344 bytes .venv/user (2).py | 563 +++++++++++++++++++++++++ .venv/user (3).py | 696 +++++++++++++++++++++++++++++++ .venv/users_collector.py | 220 ++++++++++ 9 files changed, 1887 insertions(+), 490 deletions(-) delete mode 100644 .venv/Users.py create mode 100644 .venv/questions.py create mode 100644 .venv/user (2).py create mode 100644 .venv/user (3).py create mode 100644 .venv/users_collector.py diff --git a/.venv/Admin.py b/.venv/Admin.py index f72942f..5ce428d 100644 --- a/.venv/Admin.py +++ b/.venv/Admin.py @@ -1,20 +1,27 @@ from gettext import textdomain +from telegram import Update +from telegram.ext import CallbackContext import telebot from telebot import types import uuid # Для генерации уникальных имен файлов from datetime import datetime # Для работы с датами и временем -import sqlite3 +import sqlite3 as sq +import os bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") +def get_connection(): + # Путь к вашей базе данных + db_path = os.path.join(os.path.dirname(__file__), 'student_studios_bot (1).db') + conn = sq.connect(db_path) + return conn #====================== #========ADMIN========= #====================== - @bot.message_handler(commands=['start']) -def start(message): +def admin_start(message): markup = types.ReplyKeyboardMarkup(resize_keyboard=True) btn1 = types.KeyboardButton('Статистика') btn2=types.KeyboardButton('Рассылка') @@ -36,27 +43,146 @@ def stat(message): bot.send_message(message.chat.id, f'Введите сообщение:', parse_mode='html') + @bot.message_handler(func=lambda message: message.text == 'Назначить администратора') def give_role(message): bot.send_message(message.chat.id, - f'Кого вы хотите назначить?', parse_mode='html') - # Регистрируем следующий шаг для получения username + "Перешлите сообщение пользователя или введите @username") bot.register_next_step_handler(message, get_admin_username) def get_admin_username(message): - # Если пользователь переслал сообщение + # Получаем username + admin_username = None + if message.forward_from: - admin_username = f"@{message.forward_from.username}" if message.forward_from.username else str( - message.forward_from.id) + if message.forward_from.username: + admin_username = f"@{message.forward_from.username}" + else: + admin_username = str(message.forward_from.id) bot.send_message(message.chat.id, f"Username: {admin_username}") - # Если ввели текст с @ elif message.text and '@' in message.text: admin_username = message.text bot.send_message(message.chat.id, f"Username получен: {admin_username}") + else: + bot.send_message(message.chat.id, "Ошибка! Введите @username или перешлите сообщение") + return + + # Проверяем, существует ли пользователь в базе + user_exists, existing_name, existing_phone = check_user_exists(admin_username) + + if user_exists: + # Если пользователь существует, используем сохраненные данные + save_admin_to_db(admin_username, existing_name, existing_phone, message.chat.id) + return + + # Если пользователь не существует, запрашиваем ФИО + bot.send_message(message.chat.id, "Теперь введите ФИО нового администратора:") + bot.register_next_step_handler(message, get_admin_name, admin_username) + + +def get_admin_name(message, admin_username): + # Получаем ФИО + admin_name = message.text + + if not admin_name or len(admin_name) < 2: + bot.send_message(message.chat.id, "ФИО должно быть не короче 2 символов") + return + + bot.send_message(message.chat.id, f"ФИО: {admin_name}") + + # Запрашиваем номер телефона + bot.send_message(message.chat.id, "Теперь введите номер телефона администратора (например, +79123456789):") + bot.register_next_step_handler(message, get_admin_phone, admin_username, admin_name) + + +def get_admin_phone(message, admin_username, admin_name): + # Получаем номер телефона + admin_phone = message.text + + # Простая валидация номера телефона + if not admin_phone or len(admin_phone) < 5: + bot.send_message(message.chat.id, "Номер телефона должен быть не короче 5 символов") + return + + bot.send_message(message.chat.id, f"Номер телефона: {admin_phone}") + + # Сохраняем в базу данных + save_admin_to_db(admin_username, admin_name, admin_phone, message.chat.id) + + +def check_user_exists(username): + """Проверяет, существует ли пользователь в базе и возвращает его данные""" + with get_connection() as conn: + cursor = conn.cursor() + + # Ищем пользователя в базе + cursor.execute(""" + SELECT full_name, phone_number + FROM users + WHERE username = ? + """, (username,)) + + result = cursor.fetchone() + + if result : + # Пользователь найден, возвращаем его данные + full_name = result[0] + phone = result[1] if len(result) > 1 else None + + if full_name and not phone: + return False, None, None # Заставляем ввести все заново + + return True, full_name, phone + else: + # Пользователь не найден + return False, None, None + + +def save_admin_to_db(username, name, phone, chat_id): + with get_connection() as conn: + cursor = conn.cursor() + + # Проверяем, есть ли пользователь в базе + cursor.execute("SELECT user_id FROM users WHERE username = ?", (username,)) + user = cursor.fetchone() + + if user: + # Обновляем существующего пользователя + cursor.execute(""" + UPDATE users + SET role = 'head', + full_name = ?, + phone_number = ?, + created_at = datetime('now') + WHERE username = ? + """, (name, phone, username)) + bot.send_message(chat_id, + f"✅ {username} назначен администратором\n" + f"📛 ФИО: {name}\n" + f"📞 Телефон: {phone}") + else: + # Добавляем нового пользователя + cursor.execute(""" + INSERT INTO users (username, full_name, phone_number, role, created_at) + VALUES (?, ?, ?, 'head', datetime('now')) + """, (username, name, phone)) + bot.send_message(chat_id, + f"✅ Добавлен администратор:\n" + f"👤 Username: {username}\n" + f"📛 ФИО: {name}\n" + f"📞 Телефон: {phone}\n" + f"🎖️ Роль: Администратор (руководитель) студии") + #Добавляем в таблицу studios студию с данным админом + user_id = cursor.lastrowid + cursor.execute(""" + INSERT INTO studios (name, description, contacts, promo_photo_id, promo_video_id, is_active, + head_user_id, invite_code) + VALUES (?, ?, ?, ?, ?,? ,?, ?) + """, ("None" , None, None, None , None, 1, user_id , None)) + + conn.commit() - -bot.polling(none_stop=True) \ No newline at end of file diff --git a/.venv/Dtbase_config.py b/.venv/Dtbase_config.py index b2c7206..bdf4655 100644 --- a/.venv/Dtbase_config.py +++ b/.venv/Dtbase_config.py @@ -16,3 +16,5 @@ def add_studio(): con.commit() con.close() + + diff --git a/.venv/Head_of_studio.py b/.venv/Head_of_studio.py index c996771..825ba2c 100644 --- a/.venv/Head_of_studio.py +++ b/.venv/Head_of_studio.py @@ -9,6 +9,10 @@ #====================== #====HEAD OF STUDIO==== #====================== +from telegram import Update +from telegram.ext import CallbackContext + + def show_main_menu(markup): btn1 = types.KeyboardButton('Статистика') btn2=types.KeyboardButton('Новые заявки') @@ -16,8 +20,7 @@ def show_main_menu(markup): btn3 = types.KeyboardButton('Изменить информацию о студии') -@bot.message_handler(commands=['start']) -def start(message): +def head_start(message): markup = types.ReplyKeyboardMarkup(resize_keyboard=True) show_main_menu(markup) @@ -166,5 +169,3 @@ def cancel_editing(message): ) - -bot.polling(none_stop=True) \ No newline at end of file diff --git a/.venv/Users.py b/.venv/Users.py deleted file mode 100644 index 13011d3..0000000 --- a/.venv/Users.py +++ /dev/null @@ -1,475 +0,0 @@ -import telebot -from telebot import types -import sqlite3 -from datetime import datetime -from typing import Dict, Any, List, Tuple -import logging -import json -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.ext import CallbackContext, ConversationHandler - -from .сборщик_анкет import QuestionnaireBuilder -from .вопросы import get_question_obj - -# Настройка логирования -logging.basicConfig( - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - level=logging.INFO -) -logger = logging.getLogger(__name__) - -bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") -DB_PATH = "student_studios_bot (1).db" - -def get_connection() -> sqlite3.Connection: - """Возвращает соединение с БД""" - return sqlite3.connect(DB_PATH, check_same_thread=False) - -def load_active_studios() -> List[Tuple[int, str]]: - """Загрузка списка активных студий""" - try: - with get_connection() as conn: - cursor = conn.cursor() - cursor.execute("SELECT studio_id, name FROM studios WHERE is_active = 1 ORDER BY studio_id") - return cursor.fetchall() - except Exception as e: - logger.error(f"Error loading studios: {e}") - return [] - -@bot.message_handler(commands=['start']) -def start(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn1 = types.KeyboardButton('Каталог студий') - markup.row(btn1) - btn2 = types.KeyboardButton('Мои заявки') - btn3 = types.KeyboardButton('Подать заявку') - markup.row(btn2, btn3) - - bot.send_message(message.chat.id, - f'Привет, в этом боте ты можешь узнать про творческие студии' - f' ГУАП и стать частью активной студенческой жизни!', parse_mode='html', reply_markup=markup) - -# ВЕТКА КАТАЛОГ СТУДИЙ - -@bot.message_handler(func=lambda message: message.text == 'Каталог студий') -def inline_button_catalog(message): - studios = load_active_studios() - - if not studios: - bot.send_message(message.chat.id, 'На данный момент активных студий нет.') - return - - markup = types.InlineKeyboardMarkup() - for studio_id, name in studios: - btn = types.InlineKeyboardButton(name, callback_data=f'info:{studio_id}') - markup.row(btn) - - bot.send_message(message.chat.id, 'Нажми на студию для просмотра:', reply_markup=markup) - -@bot.callback_query_handler(func=lambda call: call.data.startswith('info:')) -def show_studio_info(call): - """Показывает информацию о студии""" - studio_id = int(call.data.split(':')[1]) - - try: - with get_connection() as conn: - cursor = conn.cursor() - cursor.execute(""" - SELECT name, description, contacts - FROM studios - WHERE studio_id = ? - """, (studio_id,)) - result = cursor.fetchone() - - if result: - name, description, contacts = result - text = f"{name}\n\n{description or 'Описание скоро появится.'}\n\nКонтакты: {contacts or '—'}" - - # Добавляем кнопку подачи заявки - markup = types.InlineKeyboardMarkup() - btn_apply = types.InlineKeyboardButton("📝 Подать заявку", callback_data=f'apply:{studio_id}') - markup.row(btn_apply) - - bot.send_message(call.message.chat.id, text, parse_mode='html', reply_markup=markup) - else: - bot.answer_callback_query(call.id, "Студия не найдена") - except Exception as e: - logger.error(f"Error showing studio info: {e}") - bot.answer_callback_query(call.id, "Ошибка при загрузке информации") - - -class QuestionnaireHandler: - """Обработчик анкет с автозаполнением""" - - def __init__(self, db_connection): - self.db = db_connection - - def start_questionnaire(self, update: Update, context: CallbackContext) -> int: - """Начало анкеты для выбранной студии""" - query = update.callback_query - query.answer() - - # Получаем ID студии из cтадии выбора студии - studio_id = int(query.data.split('_')[-1]) - - # Сохраняем данные в контексте - context.user_data['studio_id'] = studio_id - context.user_data['questionnaire'] = QuestionnaireBuilder.get_questionnaire_for_studio(studio_id) - context.user_data['current_question_index'] = 0 - context.user_data['answers'] = {} - - # Получаем данные пользователя для автозаполнения - user_id = update.effective_user.id - user_data = self.get_user_data(user_id) - context.user_data['user_data'] = user_data - - # Проверяем возможность автозаполнения - if QuestionnaireBuilder.can_autofill(studio_id, user_data): - return self.offer_autofill(update, context, studio_id, user_data) - else: - return self.start_manual_filling(update, context, studio_id) - - def get_user_data(self, user_id: int) -> Dict[str, Any]: - """Получает данные пользователя из БД для автозаполнения""" - try: - cursor = self.db.cursor() - cursor.execute(""" - SELECT full_name, student_group, phone, email - FROM users - WHERE telegram_id = %s - """, (user_id,)) - - result = cursor.fetchone() - cursor.close() - - if result: - return { - 'full_name': result[0] or '', - 'student_group': result[1] or '', - 'phone': result[2] or '', - 'email': result[3] or '' - } - except Exception as e: - logger.error(f"Error getting user data: {e}") - - return {} - - def offer_autofill(self, update: Update, context: CallbackContext, - studio_id: int, user_data: Dict[str, Any]) -> int: - """Предлагает автозаполнение""" - query = update.callback_query - - # Получаем поля для автозаполнения - autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) - autofill_info = [] - - for field in autofill_fields: - value = user_data.get(field, '') - if value: - field_name = { - 'full_name': 'ФИО', - 'student_group': 'Группа', - 'phone': 'Телефон', - 'email': 'Email' - }.get(field, field) - autofill_info.append(f"{field_name}: {value}") - - message = ( - f"Хотите использовать основную информацию из предыдущих анкет для автозаполнения?" - ) - - keyboard = [ - [ - InlineKeyboardButton("✅ Использовать мои данные", callback_data='use_autofill'), - InlineKeyboardButton("✏️ Заполнить вручную", callback_data='manual_fill') - ] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - - query.edit_message_text(message, reply_markup=reply_markup, parse_mode='Markdown') - - return 1 # Состояние ожидания выбора - - def handle_autofill_choice(self, update: Update, context: CallbackContext) -> int: - """Обработка выбора автозаполнения""" - query = update.callback_query - query.answer() - - studio_id = context.user_data['studio_id'] - user_data = context.user_data['user_data'] - answers = context.user_data['answers'] - - if query.data == 'use_autofill': - # Автозаполняем данные - autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) - - for field in autofill_fields: - value = user_data.get(field) - if value: - # Находим ID вопроса, соответствующего этому полю - questions = QuestionnaireBuilder.get_question_objects(studio_id) - for q in questions: - if q.autofill_field == field: - answers[q.id] = value - break - - query.edit_message_text( - "✅ Данные автозаполнены.\n" - "Продолжаем с оставшимися вопросами..." - ) - - # Начинаем опрос с первого незаполненного вопроса - return self.ask_next_question(update, context) - - def start_manual_filling(self, update: Update, context: CallbackContext, - studio_id: int) -> int: - """Начинает ручное заполнение анкеты""" - studio_name = QuestionnaireBuilder.get_studio_name(studio_id) - - if update.callback_query: - update.callback_query.edit_message_text( - f"Начинаем заполнение анкеты для студии *{studio_name}*.\n\n" - "Пожалуйста, ответьте на несколько вопросов:", - parse_mode='Markdown' - ) - else: - update.message.reply_text( - f"Начинаем заполнение анкеты для студии *{studio_name}*.\n\n" - "Пожалуйста, ответьте на несколько вопросов:", - parse_mode='Markdown' - ) - - return self.ask_next_question(update, context) - - def ask_next_question(self, update: Update, context: CallbackContext) -> int: - """Задает следующий вопрос""" - studio_id = context.user_data['studio_id'] - question_ids = QuestionnaireBuilder.get_question_sequence(studio_id) - current_index = context.user_data['current_question_index'] - answers = context.user_data['answers'] - - # Пропускаем уже заполненные вопросы (при автозаполнении) - while current_index < len(question_ids): - qid = question_ids[current_index] - if qid not in answers: - break - current_index += 1 - - # Проверяем, не заполнили ли мы все вопросы - if current_index >= len(question_ids): - return self.show_summary(update, context) - - # Сохраняем текущий индекс - context.user_data['current_question_index'] = current_index - context.user_data['current_question_id'] = qid - - # Получаем и вызываем обработчик вопроса - handler = QuestionnaireBuilder.build_questionnaire_handlers(studio_id)[current_index] - - if update.callback_query: - # Если это callback, отправляем новое сообщение - update.callback_query.message.reply_text("Следующий вопрос:") - handler(update, context) - else: - handler(update, context) - - # Возвращаем состояние для обработки ответа - return current_index + 100 # Уникальное состояние для каждого вопроса - - def process_answer(self, update: Update, context: CallbackContext) -> int: - """Обработка ответа на вопрос""" - current_qid = context.user_data.get('current_question_id') - answer_text = update.message.text.strip() - - # Для вопросов с inline-кнопками - if update.callback_query: - answer_text = update.callback_query.data - if answer_text.startswith('activeness_'): - answer_text = 'Да' if answer_text.endswith('yes') else 'Нет' - - # Получаем объект вопроса для валидации - q_obj = get_question_obj(current_qid) - - if q_obj: - is_valid, error_msg = q_obj.validate(answer_text) - if not is_valid: - update.message.reply_text(f"❌ {error_msg}\n\nПопробуйте еще раз:") - return context.user_data['current_question_index'] + 100 - - # Сохраняем ответ - context.user_data['answers'][current_qid] = answer_text - - # Переходим к следующему вопросу - context.user_data['current_question_index'] += 1 - - return self.ask_next_question(update, context) - - def show_summary(self, update: Update, context: CallbackContext) -> int: - """Показывает сводку по ответам""" - studio_id = context.user_data['studio_id'] - answers = context.user_data['answers'] - - # Валидируем ответы - is_valid, errors = QuestionnaireBuilder.validate_answers(studio_id, answers) - - if not is_valid: - error_text = "❌ *Ошибки в заполнении:*\n\n" - for qid, error in errors.items(): - q_obj = get_question_obj(qid) - error_text += f"• {q_obj.text}: {error}\n" - - error_text += "\nПожалуйста, заполните анкету заново." - - update.message.reply_text(error_text, parse_mode='Markdown') - context.user_data['current_question_index'] = 0 - context.user_data['answers'] = {} - return self.ask_next_question(update, context) - - - - # Генерируем сводку - summary = QuestionnaireBuilder.generate_summary(studio_id, answers) - - # Создаем клавиатуру с кнопками - keyboard = [ - [ - InlineKeyboardButton("✅ Подтвердить и отправить", - callback_data='confirm_application'), - InlineKeyboardButton("✏️ Заполнить заново", - callback_data='restart_questionnaire') - ], - [ - InlineKeyboardButton("🚫 Отменить и вернуться в каталог", - callback_data='cancel_to_catalog') - ] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - - if update.callback_query: - update.callback_query.edit_message_text( - f"{summary}\n\n" - f"*Всё верно? Подтвердите отправку заявки или внесите изменения.*", - reply_markup=reply_markup, - parse_mode='Markdown' - ) - else: - update.message.reply_text( - f"{summary}\n\n" - f"*Всё верно? Подтвердите отправку заявки или внесите изменения.*", - reply_markup=reply_markup, - parse_mode='Markdown' - ) - - return 'CONFIRMATION_STATE' - - def handle_confirmation_action(self, update: Update, context: CallbackContext) -> int: - """Обработка действий на этапе подтверждения""" - query = update.callback_query - query.answer() - - action = query.data - - if action == 'confirm_application': - return self.confirm_application(update, context) - - elif action == 'restart_questionnaire': - return self.restart_questionnaire(update, context) - - elif action == 'cancel_to_catalog': - return self.cancel_to_catalog(update, context) - - return 'CONFIRMATION_STATE' - - def confirm_application(self, update: Update, context: CallbackContext) -> int: - """Подтверждение и сохранение анкеты в БД""" - query = update.callback_query - - studio_id = context.user_data['studio_id'] - user_id = update.effective_user.id - answers = context.user_data['answers'] - user_data = context.user_data.get('user_data', {}) - - # Сохраняем анкету в БД - success, application_id = self.save_application_to_db( - user_id=user_id, - studio_id=studio_id, - answers=answers, - user_data=user_data - ) - - studio_name = QuestionnaireBuilder.get_studio_name(studio_id) - - if success: - # Создаем клавиатуру с кнопкой "Главное меню" - keyboard = [ - [InlineKeyboardButton("🏠 Главное меню", callback_data='go_to_main_menu')] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - - success_message = ( - f"✅ Ваша заявка в студию *{studio_name}* отправлена.\n" - ) - - query.edit_message_text( - success_message, - reply_markup=reply_markup, - parse_mode='Markdown' - ) - - # Очищаем данные анкеты, но сохраняем studio_id для возможного повторного заполнения - context.user_data.pop('answers', None) - context.user_data.pop('current_question_index', None) - context.user_data.pop('current_question_id', None) - - return 'AFTER_CONFIRMATION' - - else: - # В случае ошибки - keyboard = [ - [InlineKeyboardButton("🔄 Попробовать снова", - callback_data='retry_save_application')], - [InlineKeyboardButton("🚫 Отменить", callback_data='cancel_to_catalog')] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - - error_message = ( - f"Не удалось сохранить заявку в студию *{studio_name}*.\n" - ) - - query.edit_message_text( - error_message, - reply_markup=reply_markup, - parse_mode='Markdown' - ) - - return 'CONFIRMATION_STATE' - - def restart_questionnaire(self, update: Update, context: CallbackContext) -> int: - """Перезапуск заполнения анкеты""" - query = update.callback_query - - studio_id = context.user_data['studio_id'] - studio_name = QuestionnaireBuilder.get_studio_name(studio_id) - - # Полностью сбрасываем данные анкеты - context.user_data['answers'] = {} - context.user_data['current_question_index'] = 0 - context.user_data.pop('current_question_id', None) - - return self.start_manual_filling(update, context, studio_id) - - def cancel_to_catalog(self, update: Update, context: CallbackContext) -> int: - """Отмена анкеты и возврат в каталог студий""" - query = update.callback_query - - studio_id = context.user_data['studio_id'] - studio_name = QuestionnaireBuilder.get_studio_name(studio_id) - - # Полностью сбрасываем данные анкеты - context.user_data['answers'] = {} - context.user_data['current_question_index'] = 0 - context.user_data.pop('current_question_id', None) - - return self.inline_button_catalog(message) - - diff --git a/.venv/questions.py b/.venv/questions.py new file mode 100644 index 0000000..184c730 --- /dev/null +++ b/.venv/questions.py @@ -0,0 +1,264 @@ +from typing import Dict, Any, Callable, Optional +import telebot +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import CallbackContext +import re + +class Question: + """Класс для хранения вопроса""" + def __init__(self, + question_id: str, + text: str, #Текст вопроса выведет бот, зачем это? + question_type: str = 'text', + is_required: bool = True, + autofill_field: Optional[str] = None, + validation_func: Optional[Callable] = None, + options: Optional[list] = None, + keyboard: Optional[list] = None): + self.id = question_id + self.text = text + self.type = question_type # text, phone, email, boolean, select, multi_select + self.is_required = is_required + self.autofill_field = autofill_field # Поле для автозаполнения из БД + self.validation_func = validation_func + self.options = options or [] + self.keyboard = keyboard + + def validate(self, answer: str) -> tuple[bool, str]: + """Валидация ответа""" + answer = answer.strip() + + if self.is_required and not answer: + return False, "Это обязательный вопрос" + + if self.validation_func: + return self.validation_func(answer) + + # Базовая валидация по типу + if self.type == 'phone': + phone_digits = re.sub(r'[^\d]', '', answer) + if len(phone_digits) < 10: + return False, "Введите корректный номер телефона" + + elif self.type == 'email': + if '@' not in answer or '.' not in answer: + return False, "Введите корректный email" + + elif self.type == 'boolean': + if answer.lower() not in ['да', 'нет']: + return False, "Ответьте 'да' или 'нет'" + + elif self.type == 'select': + if answer not in self.options: + return False, f"Выберите вариант из списка: {', '.join(self.options)}" + + return True, "" + + +# ------ Общие вопросы------ +# update: Update - объект, содержащий информацию о входящем сообщении/действии +# context: CallbackContext - объект для хранения состояния и данных между вызовами +# update.message содержит информацию о сообщении от пользователя +# reply_text() отправляет ответ в тот же чат + +def ask_full_name(update: Update, context: CallbackContext) -> None: + """Вопрос: ФИО""" + update.message.reply_text("1. 📝 Укажите ваше ФИО (полностью):") + +def ask_group(update: Update, context: CallbackContext) -> None: + """Вопрос: Группа""" + update.message.reply_text("2. 🎓 Укажите номер вашей группу:") + +def ask_phone(update: Update, context: CallbackContext) -> None: + """Вопрос: Телефон""" + update.message.reply_text("3. 📱 Укажите ваш номер телефона:") + +def ask_email(update: Update, context: CallbackContext) -> None: + """Вопрос: Email""" + update.message.reply_text("4. 📧 Укажите ваш email адрес:") + +def ask_experience(update: Update, context: CallbackContext) -> None: + """Вопрос: Опыт в деятельности""" + update.message.reply_text("5. 💼 Расскажите о вашем опыте в данной деятельности:") + +def ask_motivation(update: Update, context: CallbackContext) -> None: + """Вопрос: Мотивация""" + update.message.reply_text("6. 🎯 Почему вы хотите вступить в эту студию?") + +def ask_expectations(update: Update, context: CallbackContext) -> None: + """Вопрос: Ожидания""" + update.message.reply_text("7. 🌟 Какие у вас ожидания от участия в студии?") + +def ask_activeness(update: Update, context: CallbackContext) -> None: + """Вопрос: Участие в мероприятиях""" + keyboard = [ + [InlineKeyboardButton("Да", callback_data="activeness_yes"), + InlineKeyboardButton("Нет", callback_data="activeness_no")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text( + "8. 📢 Готовы ли вы участвовать в мероприятиях студии?", + reply_markup=reply_markup + ) + +# ------ дополнительные опросы ------ + +#НЕ НУЖНО ИЛИ ИЗМЕНИТЬ НА ПИСЬМЕННЫЙ +def ask_dance_style(update: Update, context: CallbackContext) -> None: + """Вопрос: Стиль танца""" + keyboard = [ + [InlineKeyboardButton("Хип-хоп", callback_data="dance_hiphop")], + [InlineKeyboardButton("Бальные танцы", callback_data="dance_ballroom")], + [InlineKeyboardButton("Современные", callback_data="dance_modern")], + [InlineKeyboardButton("Народные", callback_data="dance_folk")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text( + "9. 💃 Какой танцевальный стиль вам интересен?", + reply_markup=reply_markup + ) + +def ask_music_instrument(update: Update, context: CallbackContext) -> None: + """Вопрос: Музыкальный инструмент""" + update.message.reply_text("10. 🎵 На каком инструменте вы играете (если играете)?") + +def ask_singing_experience(update: Update, context: CallbackContext) -> None: + """Вопрос: Опыт пения""" + update.message.reply_text("11. 🎤 Есть ли у вас опыт пения? Если да, опишите его:") + +# НЕ НУЖНО ИЛИ ИЗМЕНИТЬ +def ask_theater_role(update: Update, context: CallbackContext) -> None: + """Вопрос: Предпочитаемые роли в театре""" + keyboard = [ + [InlineKeyboardButton("Драматические", callback_data="role_drama")], + [InlineKeyboardButton("Комические", callback_data="role_comedy")], + [InlineKeyboardButton("Характерные", callback_data="role_character")], + [InlineKeyboardButton("Не знаю", callback_data="role_unknown")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text( + "12. 🎭 Какие роли вам интересны в театре?", + reply_markup=reply_markup + ) + +#СОМНИТЕЛЬНО, НАДО ДОБАВИТЬ ВЫБОР НАПРАВЛЕНИЯ: ТРАНСЛЯЦИИ, СВЕТ, ЗВУК +def ask_tech_skills(update: Update, context: CallbackContext) -> None: + """Вопрос: Технические навыки""" + update.message.reply_text("13. 💻 Какими техническими навыками вы владеете?") + +def ask_organizational_exp(update: Update, context: CallbackContext) -> None: + """Вопрос: Организационный опыт""" + update.message.reply_text("14. 📋 Был ли у вас опыт организации мероприятий? Опишите:") + +#ЛУЧШЕ ИЗМЕНИТЬ НА ВЫБОР НАПРАВЛЕНИЙ: РЕЖИССУРА, ПРОДЮССИРОВАНИЕ, СОБЫТИЙНОЕ ВОЛОНТЕРСТВО +def ask_leadership(update: Update, context: CallbackContext) -> None: + """Вопрос: Лидерские качества""" + keyboard = [ + [InlineKeyboardButton("Да, есть опыт", callback_data="leadership_yes")], + [InlineKeyboardButton("Хочу развивать", callback_data="leadership_develop")], + [InlineKeyboardButton("Пока нет", callback_data="leadership_no")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text( + "15. 👨‍💼 Есть ли у вас лидерский опыт?", + reply_markup=reply_markup + ) + +def ask_project_ideas(update: Update, context: CallbackContext) -> None: + """Вопрос: Идеи для проектов""" + update.message.reply_text("16. 💡 Есть ли у вас идеи для проектов в студии?") + +def ask_time_commitment(update: Update, context: CallbackContext) -> None: + """Вопрос: Временные возможности""" + update.message.reply_text("17. ⏰ Сколько часов в неделю вы готовы уделять студии?") + +def ask_why_choose_us(update: Update, context: CallbackContext) -> None: + """Вопрос: Почему выбрали нас""" + update.message.reply_text("18. ❓ Почему вы выбрали именно нашу студию?") + +#МОЖНО ДОБАВИТЬ ВАРИАНТ "ДРУГОЕ" +def ask_feedback_source(update: Update, context: CallbackContext) -> None: + """Вопрос: Откуда узнали о студии""" + keyboard = [ + [InlineKeyboardButton("От друзей", callback_data="source_friends")], + [InlineKeyboardButton("Соцсети", callback_data="source_social")], + [InlineKeyboardButton("Сайт вуза", callback_data="source_website")], + [InlineKeyboardButton("Объявление", callback_data="source_poster")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + update.message.reply_text( + "19. 📢 Откуда вы узнали о нашей студии?", + reply_markup=reply_markup + ) + +#ЛИШНЕЕ КАК БУДТО +def ask_special_needs(update: Update, context: CallbackContext) -> None: + """Вопрос: Особые потребности""" + update.message.reply_text("20. ♿ Есть ли у вас особые потребности, которые мы должны учитывать?") + + +# ========== СЛОВАРЬ ВСЕХ ВОПРОСОВ ========== +#В словаре ALL_QUESTIONS каждая запись содержит: +# • Ключ: строковый идентификатор вопроса (например, 'full_name') +# • Значение: кортеж из двух элементов: +# 1. Функция-обработчик (например, ask_full_name) +# 2. Объект класса Question с метаданными вопроса + +ALL_QUESTIONS = { + # ID вопроса: (функция-обработчик, объект Question) + 'full_name': (ask_full_name, Question('full_name', 'ФИО', autofill_field='full_name')), + 'group': (ask_group, Question('group', 'Группа', autofill_field='student_group')), + 'phone': (ask_phone, Question('phone', 'Телефон', 'phone', autofill_field='phone')), + 'email': (ask_email, Question('email', 'Email', 'email', autofill_field='email')), + 'experience': (ask_experience, Question('experience', 'Опыт в деятельности')), + 'motivation': (ask_motivation, Question('motivation', 'Мотивация вступления')), + 'expectations': (ask_expectations, Question('expectations', 'Ожидания от студии')), + 'activeness': (ask_activeness, Question('activeness', 'Участие в мероприятиях', 'boolean')), + 'dance_style': (ask_dance_style, Question('dance_style', 'Танцевальный стиль', 'select', + options=['Хип-хоп', 'Бальные танцы', 'Современные', 'Народные'])), + 'music_instrument': (ask_music_instrument, Question('music_instrument', 'Музыкальный инструмент')), + 'singing_experience': (ask_singing_experience, Question('singing_experience', 'Опыт пения')), + 'theater_role': (ask_theater_role, Question('theater_role', 'Театральные роли', 'select', + options=['Драматические', 'Комические', 'Характерные', 'Не знаю'])), + 'tech_skills': (ask_tech_skills, Question('tech_skills', 'Технические навыки')), + 'organizational_exp': (ask_organizational_exp, Question('organizational_exp', 'Организационный опыт')), + 'leadership': (ask_leadership, Question('leadership', 'Лидерский опыт', 'select', + options=['Да, есть опыт', 'Хочу развивать', 'Пока нет'])), + 'project_ideas': (ask_project_ideas, Question('project_ideas', 'Идеи для проектов')), + 'time_commitment': (ask_time_commitment, Question('time_commitment', 'Временные возможности')), + 'why_choose_us': (ask_why_choose_us, Question('why_choose_us', 'Почему выбрали нас')), + 'feedback_source': (ask_feedback_source, Question('feedback_source', 'Источник информации', 'select', + options=['От друзей', 'Соцсети', 'Сайт вуза', 'Объявление'])), + 'special_needs': (ask_special_needs, Question('special_needs', 'Особые потребности', is_required=False)) +} + +def get_question_handler(question_id: str): + # Извлекаем первый элемент кортежа - функцию-обработчик по айди + return ALL_QUESTIONS.get(question_id, (None, None))[0] + +def get_question_obj(question_id: str) -> Optional[Question]: + # Извлекаем второй элемент кортежа - объект Question по айди вопроса + result = ALL_QUESTIONS.get(question_id) + return result[1] if result else None + +#ПРИВЯЗАТЬ БД +#Убрать пол текск, так как сам вопрос выводитя ботом, тогда будет так: + +#ALL_QUESTIONS = { + # 'full_name': Question( + # question_id='full_name', + # text="1. 📝 Укажите ваше ФИО (полностью):", + # autofill_field='full_name' + # ), + +# def ask_question(question_id: str, update: Update, context: CallbackContext) -> None: +# """Универсальная функция для отправки любого вопроса""" +# question = ALL_QUESTIONS.get(question_id) +# if question: +# question.ask(update, context) +# else: +# update.message.reply_text("Вопрос не найден") +# +# +# def ask_full_name(update: Update, context: CallbackContext) -> None: +# ask_question('full_name', update, context) diff --git a/.venv/student_studios_bot (1).db b/.venv/student_studios_bot (1).db index a3c614d77baf81d0aeeaf3f0eb2038f893050980..c868a8348ae1c3c92d5acb14fef5e31dc482bba1 100644 GIT binary patch delta 2009 zcmcgsUrbw77(b`?mbRDPdq7-nfRUbUqO>U$?gh%~!>Dja&{_g?M$M9@jJvI_|JL4y zO%q|@oMy&9xi7qIOBP>Ekb-MQ21|UJi}7}QaEXg1dzmkb7=3nrx3}348nY+Q&AI2# z_y2dk@0-8L%->}EQxlP-rqbitd^({~88soB4GmF>5Nd-d)TSL)@k_t#X+wecPV;sT za^fY#eC)ntzsgUuTfFS-#Y?>W%?E9mTTHh&#LiB9p+o<{brADZ;!HC89}N0Eu7mMYUcXIbo1fP|YwUZ0@|>$s zOs5n1voxl>6|3nwJU*$6jYjF~%GO0EaZ{k4QUU|kRT#olhY(vYHsq9xk088p=Q(`M*9$wq*}>HJyL&nuhiWu)1F|cw4 zXJlNb=In%JI$hfcg+_*H$5xvwusryLh5Jaj4-VWB-d)&-T?pg$u)H|IFgZ0dlgyl` zk6ti1cyK$V&%$-W*(isozW317oR8zU-T<(F zWh@z&eVin5oMfK$$~>@c!nbVRnuFtHpeSZn^KnA~j_WfSp-e01!B)9)0e0uWX4zPy zaItKxR4$rFA2(}8X%`+Ma)(?dM~MvE>L`g#KMHfN*`3IBUl763#vLtmK?cCFk;EPb(B2 XGQo7iggQemm=1G=BrFq1%&va`|H5`A delta 1145 zcmbVMO-vI(7@c9a-O_G5#Y7ts=maHgW1#^|)E*4jRIM~EKP{rsY_{DMOXv^Xt*8em zCV3X=WH<`@5Z@+!t``*q>af2yt zFy66|a7vZ&^=v*Zs<8Xi3Xa@$*f@vJ2bZp?Vq#iWW~LK@DyMUT4u!sk023Gu4#t8w zHh4B1#GMWAwMW(voFB9s9dRE7sf;AwqB&H&AQsfDj)jJg{!qt2#RvX{qPLEftM(%F zv1FU|u?xV=k)wuLlbXtAgp}mb)(p=fdvD6bkt^KWKq!LUuAH2aQkhAY7rVqS`f#BG6`u^evG z>m;-3w470KEI1LXRo2tUN2AhF!{?%-!O-~#9tvK@?s|?^zLPh5mO6}_qpJ(vpC_B< z)1=+lMmj95R-If?3lb&Q%aI8S9@nGPgXvUK)E$#VRn%+aNHm5c@o?CSySwqlv1kNW zmMX>alk&^*i^^hUq4EHi*C|}r(`(ub`Mgd)XKByiz=dF7h$`F-*3A2Vw?v8l5uwo@ zrI1LcRI<=?oRpeQ5DRy}wBIIm1bWFZ@0V)U#Ur7Sco4hy5(;EY0MNekUm?5bgq-%t)(MRel>jPfEd!4cDLF5h8LJanxL1Z* z6GS@z?VxuwSVqSVGK>$_9f{hBU@Q!C1lC$pm}d`7SThxWMw_WdV69#r`;5N;< sqlite3.Connection: + """Возвращает соединение с БД""" + return sqlite3.connect(DB_PATH, check_same_thread=False) + +def load_active_studios() -> List[Tuple[int, str]]: + """Загрузка списка активных студий""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT studio_id, name FROM studios WHERE is_active = 1 ORDER BY studio_id") + return cursor.fetchall() + except Exception as e: + logger.error(f"Error loading studios: {e}") + return [] + + +#===========ОПРЕДЕЛЕНИЕ РОЛИ ПО БД========================== +def get_user_role(username: str) -> str: + """Получение роли пользователя из базы данных""" + if not username: + return 'user' + + try: + with get_connection() as conn: + cursor = conn.cursor() + # Ищем username с @ (как хранится в БД) + cursor.execute("SELECT role FROM users WHERE username = ?", (f"@{username}",)) + result = cursor.fetchone() + return result[0] if result else 'user' + except Exception as e: + logger.error(f"Database error getting role: {e}") + return 'user' + + +@bot.message_handler(commands=['start']) +def start(message): + """Главный обработчик команды /start с распределением по ролям""" + username = message.from_user.username + + # Получаем роль из базы данных + role = get_user_role(username) + + # Распределение по ролям + if role == 'admin': + try: + from Admin import admin_start + admin_start(message) + return + except ImportError as e: + logger.error(f"Error importing Admin: {e}") + bot.send_message(message.chat.id, "Модуль администратора недоступен") + except Exception as e: + logger.error(f"Error in admin_start: {e}") + bot.send_message(message.chat.id, "Ошибка в модуле администратора") + + elif role == 'head': + try: + from Head_of_studio import head_start + head_start( message) + return + except ImportError as e: + logger.error(f"Error importing Head_of_studio: {e}") + bot.send_message(message.chat.id, "Модуль руководителя недоступен") + except Exception as e: + logger.error(f"Error in head_start: {e}") + bot.send_message(message.chat.id, "Ошибка в модуле руководителя") + + # Для user или если пользователя нет в базе + user_start(message) + + +#==============ФУНКЦИОНАЛ СТАНДАРТНОГО ПОЛЬЗОВАТЕЛЯ========================= +def user_start(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + btn3 = types.KeyboardButton('Подать заявку') + markup.row(btn2, btn3) + + bot.send_message( + message.chat.id, + f'Привет, в этом боте ты можешь узнать про творческие студии' + f' ГУАП и стать частью активной студенческой жизни!', + parse_mode='html', + reply_markup=markup + ) + +# ВЕТКА КАТАЛОГ СТУДИЙ + +@bot.message_handler(func=lambda message: message.text == 'Каталог студий') +def inline_button_catalog(message): + studios = load_active_studios() + + if not studios: + bot.send_message(message.chat.id, 'На данный момент активных студий нет.') + return + + markup = types.InlineKeyboardMarkup() + for studio_id, name in studios: + btn = types.InlineKeyboardButton(name, callback_data=f'info:{studio_id}') + markup.row(btn) + + bot.send_message(message.chat.id, 'Нажми на студию для просмотра:', reply_markup=markup) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('info:')) +def show_studio_info(call): + """Показывает информацию о студии""" + studio_id = int(call.data.split(':')[1]) + + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT name, description, contacts + FROM studios + WHERE studio_id = ? + """, (studio_id,)) + result = cursor.fetchone() + + if result: + name, description, contacts = result + text = f"{name}\n\n{description or 'Описание скоро появится.'}\n\nКонтакты: {contacts or '—'}" + + # Добавляем кнопку подачи заявки + markup = types.InlineKeyboardMarkup() + btn_apply = types.InlineKeyboardButton("📝 Подать заявку", callback_data=f'apply:{studio_id}') + markup.row(btn_apply) + + bot.edit_message_text( + text, + call.message.chat.id, + call.message.message_id, + parse_mode='html', + reply_markup=markup + ) + else: + bot.answer_callback_query(call.id, "Студия не найдена") + except Exception as e: + logger.error(f"Error showing studio info: {e}") + bot.answer_callback_query(call.id, "Ошибка при загрузке информации") + +# Хранение состояний пользователей +user_states = {} + +class QuestionnaireHandler: + """Обработчик анкет с автозаполнением""" + + def __init__(self, db_connection): + self.db = db_connection + + def start_questionnaire(self, call): + """Начало анкеты для выбранной студии""" + studio_id = int(call.data.split(':')[1]) + user_id = call.from_user.id + + # Проверяем, существует ли такая студия в конфигурации + studio_config = QuestionnaireBuilder.get_questionnaire_for_studio(studio_id) + if not studio_config: + bot.answer_callback_query(call.id, f"Анкета для этой студии не настроена") + return + + # Сохраняем данные в состоянии пользователя + user_states[user_id] = { + 'state': 'questionnaire', + 'studio_id': studio_id, + 'studio_name': studio_config.get('name', 'Студия'), + 'current_question_index': 0, + 'answers': {}, + 'questionnaire': studio_config # Сохраняем конфигурацию студии + } + + # Получаем данные пользователя для автозаполнения + user_data = self.get_user_data(user_id) + user_states[user_id]['user_data'] = user_data + + # Проверяем возможность автозаполнения + if QuestionnaireBuilder.can_autofill(studio_id, user_data): + self.offer_autofill(call, studio_id, user_data) + else: + self.start_manual_filling(call, studio_id) + + def get_user_data(self, user_id: int) -> Dict[str, Any]: + """Получает данные пользователя из БД для автозаполнения""" + try: + conn = get_connection() + cursor = conn.cursor() + cursor.execute(""" + SELECT full_name, student_group, phone, email + FROM users + WHERE telegram_id = ? + """, (user_id,)) + + result = cursor.fetchone() + conn.close() + + if result: + return { + 'full_name': result[0] or '', + 'student_group': result[1] or '', + 'phone': result[2] or '', + 'email': result[3] or '' + } + except Exception as e: + logger.error(f"Error getting user data: {e}") + + return {} + + def offer_autofill(self, call, studio_id: int, user_data: Dict[str, Any]): + """Предлагает автозаполнение""" + # Получаем поля для автозаполнения + autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) + autofill_info = [] + + for field in autofill_fields: + value = user_data.get(field, '') + if value: + field_name = { + 'full_name': 'ФИО', + 'student_group': 'Группа', + 'phone': 'Телефон', + 'email': 'Email' + }.get(field, field) + autofill_info.append(f"{field_name}: {value}") + + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + message = f"*Анкета для студии: {studio_name}*\n\n" + + if autofill_info: + message += "Найдены ваши данные из предыдущих анкет:\n" + for info in autofill_info: + message += f"• {info}\n" + message += "\nХотите использовать их для автозаполнения?" + else: + message += "Хотите использовать основную информацию из предыдущих анкет для автозаполнения?" + + markup = types.InlineKeyboardMarkup() + markup.row( + types.InlineKeyboardButton("✅ Использовать мои данные", callback_data='use_autofill'), + types.InlineKeyboardButton("✏️ Заполнить вручную", callback_data='manual_fill') + ) + + bot.edit_message_text( + message, + call.message.chat.id, + call.message.message_id, + parse_mode='Markdown', + reply_markup=markup + ) + + def start_manual_filling(self, call, studio_id: int): + """Начинает ручное заполнение анкеты""" + user_id = call.from_user.id + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + + # Обновляем состояние + user_states[user_id]['state'] = 'answering' + + bot.edit_message_text( + f"Начинаем заполнение анкеты для студии *{studio_name}*.\n\n" + f"Пожалуйста, ответьте на несколько вопросов:", + call.message.chat.id, + call.message.message_id, + parse_mode='Markdown' + ) + + # Даем небольшую задержку перед первым вопросом + import time + time.sleep(1) + + # Задаем первый вопрос + self.ask_next_question(call.message.chat.id, user_id) + + def ask_next_question(self, chat_id: int, user_id: int): + """Задает следующий вопрос""" + if user_id not in user_states: + return + + state = user_states[user_id] + studio_id = state['studio_id'] + question_ids = QuestionnaireBuilder.get_question_sequence(studio_id) + current_index = state.get('current_question_index', 0) + answers = state.get('answers', {}) + + # Пропускаем уже заполненные вопросы + while current_index < len(question_ids): + qid = question_ids[current_index] + if qid not in answers: + break + current_index += 1 + + # Проверяем, не заполнили ли мы все вопросы + if current_index >= len(question_ids): + self.show_summary(chat_id, user_id) + return + + # Сохраняем текущий индекс + user_states[user_id]['current_question_index'] = current_index + user_states[user_id]['current_question_id'] = qid + + # Получаем текст вопроса + question_text = get_question_text(qid) + + # Отправляем вопрос + bot.send_message(chat_id, question_text) + + def process_answer(self, message): + """Обработка ответа на вопрос""" + user_id = message.from_user.id + + if user_id not in user_states: + return + + state = user_states[user_id] + if state.get('state') != 'answering': + return + + current_qid = state.get('current_question_id') + answer_text = message.text.strip() + + if not current_qid: + return + + # Получаем объект вопроса для валидации + q_obj = get_question_obj(current_qid) + + if q_obj: + is_valid, error_msg = q_obj.validate(answer_text) + if not is_valid: + bot.send_message(message.chat.id, f"❌ {error_msg}\n\nПопробуйте еще раз:") + return + + # Сохраняем ответ + user_states[user_id]['answers'][current_qid] = answer_text + + # Переходим к следующему вопросу + user_states[user_id]['current_question_index'] += 1 + + # Задаем следующий вопрос + self.ask_next_question(message.chat.id, user_id) + + def show_summary(self, chat_id: int, user_id: int): + """Показывает сводку по ответам""" + if user_id not in user_states: + return + + state = user_states[user_id] + studio_id = state['studio_id'] + answers = state.get('answers', {}) + + # Валидируем ответы + is_valid, errors = QuestionnaireBuilder.validate_answers(studio_id, answers) + + if not is_valid: + error_text = "❌ *Ошибки в заполнении:*\n\n" + for qid, error in errors.items(): + q_obj = get_question_obj(qid) + error_text += f"• {q_obj.text if q_obj else qid}: {error}\n" + + error_text += "\nПожалуйста, заполните анкету заново." + + bot.send_message(chat_id, error_text, parse_mode='Markdown') + + # Сбрасываем анкету + state['current_question_index'] = 0 + state['answers'] = {} + + # Запускаем заново + self.ask_next_question(chat_id, user_id) + return + + # Генерируем сводку + summary = QuestionnaireBuilder.generate_summary(studio_id, answers) + + # Создаем клавиатуру + markup = types.InlineKeyboardMarkup() + markup.row( + types.InlineKeyboardButton("✅ Подтвердить и отправить", callback_data='confirm_application'), + types.InlineKeyboardButton("✏️ Заполнить заново", callback_data='restart_questionnaire') + ) + markup.row( + types.InlineKeyboardButton("🚫 Отменить", callback_data='cancel_to_catalog') + ) + + bot.send_message( + chat_id, + f"{summary}\n\n*Всё верно? Подтвердите отправку заявки или внесите изменения.*", + parse_mode='Markdown', + reply_markup=markup + ) + + # Обновляем состояние + user_states[user_id]['state'] = 'confirmation' + + +# Инициализируем обработчик +questionnaire_handler = QuestionnaireHandler(None) + +# Обработчики для анкет +@bot.callback_query_handler(func=lambda call: call.data.startswith('apply:')) +def handle_apply(call): + """Обработка нажатия кнопки 'Подать заявку'""" + questionnaire_handler.start_questionnaire(call) + +@bot.callback_query_handler(func=lambda call: call.data in ['use_autofill', 'manual_fill']) +def handle_autofill_choice(call): + """Обработка выбора автозаполнения""" + user_id = call.from_user.id + studio_id = user_states[user_id]['studio_id'] + + if call.data == 'use_autofill': + # Автозаполняем данные + user_data = user_states[user_id]['user_data'] + autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) + + for field in autofill_fields: + value = user_data.get(field) + if value: + # Находим ID вопроса, соответствующего этому полю + questions = QuestionnaireBuilder.get_question_objects(studio_id) + for q in questions: + if hasattr(q, 'autofill_field') and q.autofill_field == field: + user_states[user_id]['answers'][q.id] = value + break + + bot.edit_message_text( + "✅ Данные автозаполнены.\nПродолжаем с оставшимися вопросами...", + call.message.chat.id, + call.message.message_id + ) + + # Начинаем опрос + user_states[user_id]['state'] = 'answering' + questionnaire_handler.ask_next_question(call.message.chat.id, user_id) + +# Обработка текстовых сообщений (ответов на вопросы) +@bot.message_handler(func=lambda message: True, content_types=['text']) +def handle_all_text_messages(message): + """Обработка всех текстовых сообщений""" + user_id = message.from_user.id + + # Проверяем, находится ли пользователь в процессе заполнения анкеты + if user_id in user_states and user_states[user_id].get('state') == 'answering': + questionnaire_handler.process_answer(message) + return + + # Обработка других команд + if message.text == 'Мои заявки': + bot.send_message(message.chat.id, "Функция 'Мои заявки' в разработке") + elif message.text == 'Подать заявку': + inline_button_catalog(message) + else: + bot.send_message(message.chat.id, "Используйте кнопки меню или команду /start") + +# Обработка действий подтверждения +@bot.callback_query_handler(func=lambda call: call.data in [ + 'confirm_application', 'restart_questionnaire', 'cancel_to_catalog' +]) + +def save_application_to_db(user_id: int, studio_id: int, answers: dict): + """Сохраняет заявку в БД""" + try: + with get_connection() as conn: + cursor = conn.cursor() + + # 1. Сохраняем основную информацию о заявке + cursor.execute(""" + INSERT INTO applications (user_id, studio_id, status, created_at) + VALUES (?, ?, ?, ?) + """, (user_id, studio_id, 'pending', datetime.now())) + + application_id = cursor.lastrowid + + # 2. Сохраняем ответы на каждый вопрос + for question_id, answer in answers.items(): + cursor.execute(""" + INSERT INTO application_answers + (application_id, question_id, answer_text) + VALUES (?, ?, ?) + """, (application_id, question_id, answer)) + + conn.commit() + return application_id + + except Exception as e: + logger.error(f"Error saving application: {e}") + return None + +def handle_confirmation_actions(call): + """Обработка действий подтверждения""" + user_id = call.from_user.id + + if user_id not in user_states: + bot.answer_callback_query(call.id, "Сессия устарела") + return + + state = user_states[user_id] + studio_id = state['studio_id'] + answers = state.get('answers', {}) + + if call.data == 'confirm_application': + # СОХРАНЯЕМ В БД + application_id = save_application_to_db(user_id, studio_id, answers) + + if application_id: + bot.answer_callback_query(call.id, "✅ Заявка отправлена") + + markup = types.InlineKeyboardMarkup() + markup.row(types.InlineKeyboardButton("🏠 Главное меню", callback_data='go_to_main_menu')) + + bot.edit_message_text( + "✅ Ваша заявка отправлена успешно!", + call.message.chat.id, + call.message.message_id, + reply_markup=markup + ) + else: + bot.answer_callback_query(call.id, "❌ Ошибка при сохранении") + + bot.edit_message_text( + "❌ Произошла ошибка при сохранении заявки. Попробуйте позже.", + call.message.chat.id, + call.message.message_id + ) + + # Очищаем состояние + del user_states[user_id] + + elif call.data == 'restart_questionnaire': + # Отменяем и возвращаем в каталог + if user_id in user_states: + del user_states[user_id] + + inline_button_catalog(call.message) + +# Запуск бота +if __name__ == '__main__': + print("Бот запущен...") + bot.polling(none_stop=True) + + + diff --git a/.venv/user (3).py b/.venv/user (3).py new file mode 100644 index 0000000..87c5b66 --- /dev/null +++ b/.venv/user (3).py @@ -0,0 +1,696 @@ +import telebot +from telebot import types +import sqlite3 +from datetime import datetime +from typing import Dict, Any, List, Tuple +import logging +import re + +from users_collector import QuestionnaireBuilder +from вопросы import get_question_text, get_question_obj + +# Настройка логирования +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") +DB_PATH = "student_studios_bot (1).db" + +def get_connection() -> sqlite3.Connection: + """Возвращает соединение с БД""" + return sqlite3.connect(DB_PATH, check_same_thread=False) + +def load_active_studios() -> List[Tuple[int, str]]: + """Загрузка списка активных студий""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT studio_id, name FROM studios WHERE is_active = 1 ORDER BY studio_id") + return cursor.fetchall() + except Exception as e: + logger.error(f"Error loading studios: {e}") + return [] + +@bot.message_handler(commands=['start']) +def start(message): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + btn3 = types.KeyboardButton('Подать заявку') + markup.row(btn2, btn3) + + bot.send_message( + message.chat.id, + f'Привет, в этом боте ты можешь узнать про творческие студии' + f' ГУАП и стать частью активной студенческой жизни!', + parse_mode='html', + reply_markup=markup + ) + +# ВЕТКА КАТАЛОГ СТУДИЙ + +@bot.message_handler(func=lambda message: message.text == 'Каталог студий') +def inline_button_catalog(message): + studios = load_active_studios() + + if not studios: + bot.send_message(message.chat.id, 'На данный момент активных студий нет.') + return + + markup = types.InlineKeyboardMarkup() + for studio_id, name in studios: + btn = types.InlineKeyboardButton(name, callback_data=f'info:{studio_id}') + markup.row(btn) + + bot.send_message(message.chat.id, 'Нажми на студию для просмотра:', reply_markup=markup) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('info:')) +def show_studio_info(call): + """Показывает информацию о студии""" + studio_id = int(call.data.split(':')[1]) + + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT name, description, contacts + FROM studios + WHERE studio_id = ? + """, (studio_id,)) + result = cursor.fetchone() + + if result: + name, description, contacts = result + text = f"{name}\n\n{description or 'Описание скоро появится.'}\n\nКонтакты: {contacts or '—'}" + + # Добавляем кнопку подачи заявки + markup = types.InlineKeyboardMarkup() + btn_apply = types.InlineKeyboardButton("📝 Подать заявку", callback_data=f'apply:{studio_id}') + markup.row(btn_apply) + + bot.edit_message_text( + text, + call.message.chat.id, + call.message.message_id, + parse_mode='html', + reply_markup=markup + ) + else: + bot.answer_callback_query(call.id, "Студия не найдена") + except Exception as e: + logger.error(f"Error showing studio info: {e}") + bot.answer_callback_query(call.id, "Ошибка при загрузке информации") + +# Хранение состояний пользователей +user_states = {} + +class QuestionnaireHandler: + """Обработчик анкет с автозаполнением""" + + def __init__(self, db_connection): + pass + + def start_questionnaire(self, call): + """Начало анкеты для выбранной студии""" + studio_id = int(call.data.split(':')[1]) + user_id = call.from_user.id + + # Проверяем, существует ли такая студия в конфигурации + studio_config = QuestionnaireBuilder.get_questionnaire_for_studio(studio_id) + if not studio_config: + bot.answer_callback_query(call.id, f"Анкета для этой студии не настроена") + return + + # Сохраняем данные в состоянии пользователя + user_states[user_id] = { + 'state': 'questionnaire', + 'studio_id': studio_id, + 'studio_name': studio_config.get('name', 'Студия'), + 'current_question_index': 0, + 'answers': {}, + 'questionnaire': studio_config # Сохраняем конфигурацию студии + } + + # Получаем данные пользователя для автозаполнения + user_data = self.get_user_data(user_id) + user_states[user_id]['user_data'] = user_data + + # Проверяем возможность автозаполнения + if QuestionnaireBuilder.can_autofill(studio_id, user_data): + self.offer_autofill(call, studio_id, user_data) + else: + self.start_manual_filling(call, studio_id) + + def get_user_data(self, user_id: int) -> Dict[str, Any]: + """Получает данные пользователя из БД для автозаполнения""" + try: + with get_connection() as conn: # Открывается соединение + cursor = conn.cursor() + cursor.execute(""" + SELECT full_name, student_group, phone_number, email + FROM users + WHERE telegram_id = ? + """, (user_id,)) + result = cursor.fetchone() + return { + 'full_name': result[0] or '', + 'student_group': result[1] or '', + 'phone_number': result[2] or '', + 'email': result[3] or '' + } if result else {} + except Exception as e: + logger.error(f"Error getting user data: {e}") + return {} + + def offer_autofill(self, call, studio_id: int, user_data: Dict[str, Any]): + """Предлагает автозаполнение""" + # Получаем поля для автозаполнения + autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) + autofill_info = [] + + for field in autofill_fields: + value = user_data.get(field, '') + if value: + field_name = { + 'full_name': 'ФИО', + 'student_group': 'Группа', + 'phone_number': 'Телефон', + 'email': 'Email' + }.get(field, field) + autofill_info.append(f"{field_name}: {value}") + + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + message = f"*Анкета для студии: {studio_name}*\n\n" + + if autofill_info: + message += "Найдены ваши данные из предыдущих анкет:\n" + for info in autofill_info: + message += f"• {info}\n" + message += "\nХотите использовать их для автозаполнения?" + else: + message += "Хотите использовать основную информацию из предыдущих анкет для автозаполнения?" + + markup = types.InlineKeyboardMarkup() + markup.row( + types.InlineKeyboardButton("✅ Использовать мои данные", callback_data='use_autofill'), + types.InlineKeyboardButton("✏️ Заполнить вручную", callback_data='manual_fill') + ) + + bot.edit_message_text( + message, + call.message.chat.id, + call.message.message_id, + parse_mode='Markdown', + reply_markup=markup + ) + + def start_manual_filling(self, call, studio_id: int): + """Начинает ручное заполнение анкеты""" + user_id = call.from_user.id + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + + # Обновляем состояние + user_states[user_id]['state'] = 'answering' + + bot.edit_message_text( + f"Начинаем заполнение анкеты для студии *{studio_name}*.\n\n" + f"Пожалуйста, ответьте на несколько вопросов:", + call.message.chat.id, + call.message.message_id, + parse_mode='Markdown' + ) + + # Даем небольшую задержку перед первым вопросом + import time + time.sleep(1) + + # Задаем первый вопрос + self.ask_next_question(call.message.chat.id, user_id) + + def ask_next_question(self, chat_id: int, user_id: int): + """Задает следующий вопрос""" + if user_id not in user_states: + return + + state = user_states[user_id] + studio_id = state['studio_id'] + question_ids = QuestionnaireBuilder.get_question_sequence(studio_id) + current_index = state.get('current_question_index', 0) + answers = state.get('answers', {}) + + # Пропускаем уже заполненные вопросы + while current_index < len(question_ids): + qid = question_ids[current_index] + if qid not in answers: + break + current_index += 1 + + # Проверяем, не заполнили ли мы все вопросы + if current_index >= len(question_ids): + self.show_summary(chat_id, user_id) + return + + # Сохраняем текущий индекс + user_states[user_id]['current_question_index'] = current_index + user_states[user_id]['current_question_id'] = qid + + # Получаем текст вопроса + question_text = get_question_text(qid) + + # Отправляем вопрос + bot.send_message(chat_id, question_text) + + def process_answer(self, message): + """Обработка ответа на вопрос""" + user_id = message.from_user.id + + if user_id not in user_states: + return + + state = user_states[user_id] + if state.get('state') != 'answering': + return + + current_qid = state.get('current_question_id') + answer_text = message.text.strip() + + if not current_qid: + return + + # Получаем объект вопроса для валидации + q_obj = get_question_obj(current_qid) + + if q_obj: + is_valid, error_msg = q_obj.validate(answer_text) + if not is_valid: + bot.send_message(message.chat.id, f"❌ {error_msg}\n\nПопробуйте еще раз:") + return + + # Сохраняем ответ + user_states[user_id]['answers'][current_qid] = answer_text + + # Переходим к следующему вопросу + user_states[user_id]['current_question_index'] += 1 + + # Задаем следующий вопрос + self.ask_next_question(message.chat.id, user_id) + + def show_summary(self, chat_id: int, user_id: int): + """Показывает сводку по ответам""" + if user_id not in user_states: + return + + state = user_states[user_id] + studio_id = state['studio_id'] + answers = state.get('answers', {}) + + # Валидируем ответы + is_valid, errors = QuestionnaireBuilder.validate_answers(studio_id, answers) + + if not is_valid: + error_text = "❌ *Ошибки в заполнении:*\n\n" + for qid, error in errors.items(): + q_obj = get_question_obj(qid) + error_text += f"• {q_obj.text if q_obj else qid}: {error}\n" + + error_text += "\nПожалуйста, заполните анкету заново." + + bot.send_message(chat_id, error_text, parse_mode='Markdown') + + # Сбрасываем анкету + state['current_question_index'] = 0 + state['answers'] = {} + + # Запускаем заново + self.ask_next_question(chat_id, user_id) + return + + # Генерируем сводку + summary = QuestionnaireBuilder.generate_summary(studio_id, answers) + + # Создаем клавиатуру + markup = types.InlineKeyboardMarkup() + markup.row( + types.InlineKeyboardButton("✅ Подтвердить и отправить", callback_data='confirm_application'), + types.InlineKeyboardButton("✏️ Заполнить заново", callback_data='restart_questionnaire') + ) + markup.row( + types.InlineKeyboardButton("🚫 Отменить", callback_data='cancel_to_catalog') + ) + + bot.send_message( + chat_id, + f"{summary}\n\n*Всё верно? Подтвердите отправку заявки или внесите изменения.*", + parse_mode='Markdown', + reply_markup=markup + ) + + # Обновляем состояние + user_states[user_id]['state'] = 'confirmation' + + +# Инициализируем обработчик +questionnaire_handler = QuestionnaireHandler(None) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('apply:')) +def handle_apply(call): + """Обработка нажатия кнопки 'Подать заявку'""" + questionnaire_handler.start_questionnaire(call) + +@bot.callback_query_handler(func=lambda call: call.data in ['use_autofill', 'manual_fill']) +def handle_autofill_choice(call): + """Обработка выбора автозаполнения""" + user_id = call.from_user.id + studio_id = user_states[user_id]['studio_id'] + + if call.data == 'use_autofill': + # Автозаполняем данные + user_data = user_states[user_id]['user_data'] + autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) + + for field in autofill_fields: + value = user_data.get(field) + if value: + # Находим ID вопроса, соответствующего этому полю + questions = QuestionnaireBuilder.get_question_objects(studio_id) + for q in questions: + if hasattr(q, 'autofill_field') and q.autofill_field == field: + user_states[user_id]['answers'][q.id] = value + break + + bot.edit_message_text( + "✅ Данные автозаполнены.\nПродолжаем с оставшимися вопросами...", + call.message.chat.id, + call.message.message_id + ) + + # Начинаем опрос + user_states[user_id]['state'] = 'answering' + questionnaire_handler.ask_next_question(call.message.chat.id, user_id) + + def save_application_to_db(telegram_id: int, studio_id: int, answers: dict, user_first_name: str = None, + user_last_name: str = None): + """Сохраняет заявку в БД""" + try: + with get_connection() as conn: + cursor = conn.cursor() + + # 1. Получаем или создаем пользователя + cursor.execute("SELECT user_id FROM users WHERE telegram_id = ?", (telegram_id,)) + user_result = cursor.fetchone() + + if user_result: + user_id = user_result[0] + else: + # Получаем ФИО из ответов + full_name = answers.get('full_name') + + # Если нет в ответах, используем имя из Telegram + if not full_name and user_first_name: + full_name = f"{user_first_name or ''} {user_last_name or ''}".strip() + if not full_name: + full_name = "Не указано" + elif not full_name: + full_name = "Не указано" + + # Создаем нового пользователя + cursor.execute(""" + INSERT INTO users (telegram_id, full_name, created_at) + VALUES (?, ?, ?) + """, (telegram_id, full_name, datetime.now())) + user_id = cursor.lastrowid + + # 2. Генерируем сводку + summary = QuestionnaireBuilder.generate_summary(studio_id, answers) + + # 3. Сохраняем заявку + cursor.execute(""" + INSERT INTO applications (user_id, studio_id, summary_text, status, created_at) + VALUES (?, ?, ?, ?, ?) + """, (user_id, studio_id, summary, 'pending', datetime.now())) + + application_id = cursor.lastrowid + + # 4. Сохраняем ответы на каждый вопрос (опционально) + for question_id, answer in answers.items(): + q_obj = get_question_obj(question_id) + question_text = q_obj.text if q_obj else question_id + + cursor.execute(""" + INSERT INTO application_answers + (application_id, question_id, question_text, answer_text) + VALUES (?, ?, ?, ?) + """, (application_id, question_id, question_text, answer)) + + conn.commit() + return application_id + + except Exception as e: + logger.error(f"Error saving application: {e}") + return None + +# ... предыдущий код до функции handle_autofill_choice ... + +@bot.callback_query_handler(func=lambda call: call.data in ['use_autofill', 'manual_fill']) +def handle_autofill_choice(call): + """Обработка выбора автозаполнения""" + user_id = call.from_user.id + studio_id = user_states[user_id]['studio_id'] + + if call.data == 'use_autofill': + # Автозаполняем данные + user_data = user_states[user_id]['user_data'] + autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) + + for field in autofill_fields: + value = user_data.get(field) + if value: + # Находим ID вопроса, соответствующего этому полю + questions = QuestionnaireBuilder.get_question_objects(studio_id) + for q in questions: + if hasattr(q, 'autofill_field') and q.autofill_field == field: + user_states[user_id]['answers'][q.id] = value + break + + bot.edit_message_text( + "✅ Данные автозаполнены.\nПродолжаем с оставшимися вопросами...", + call.message.chat.id, + call.message.message_id + ) + + # Начинаем опрос + user_states[user_id]['state'] = 'answering' + questionnaire_handler.ask_next_question(call.message.chat.id, user_id) + +# ========== ВЫНЕСИТЕ ЭТИ ФУНКЦИИ НА УРОВЕНЬ ДРУГИХ ФУНКЦИЙ ========== + +def save_application_to_db(telegram_id: int, studio_id: int, answers: dict, user_first_name: str = None, + user_last_name: str = None): + """Сохраняет заявку в БД""" + try: + with get_connection() as conn: + cursor = conn.cursor() + + # 1. Получаем или создаем пользователя + cursor.execute("SELECT user_id FROM users WHERE telegram_id = ?", (telegram_id,)) + user_result = cursor.fetchone() + + if user_result: + user_id = user_result[0] + else: + # Получаем ФИО из ответов + full_name = answers.get('full_name') + + # Если нет в ответах, используем имя из Telegram + if not full_name and user_first_name: + full_name = f"{user_first_name or ''} {user_last_name or ''}".strip() + if not full_name: + full_name = "Не указано" + elif not full_name: + full_name = "Не указано" + + # Создаем нового пользователя + cursor.execute(""" + INSERT INTO users (telegram_id, full_name, created_at) + VALUES (?, ?, ?) + """, (telegram_id, full_name, datetime.now())) + user_id = cursor.lastrowid + + # 2. Генерируем сводку + summary = QuestionnaireBuilder.generate_summary(studio_id, answers) + + # 3. Сохраняем заявку + cursor.execute(""" + INSERT INTO applications (user_id, studio_id, summary, status, created_at) + VALUES (?, ?, ?, ?, ?) + """, (user_id, studio_id, summary, 'pending', datetime.now())) + + application_id = cursor.lastrowid + + # 4. Сохраняем ответы на каждый вопрос (опционально) + for question_id, answer in answers.items(): + q_obj = get_question_obj(question_id) + question_text = q_obj.text if q_obj else question_id + + cursor.execute(""" + INSERT INTO application_answers + (application_id, question_id, question_text, answer_text) + VALUES (?, ?, ?, ?) + """, (application_id, question_id, question_text, answer)) + + conn.commit() + return application_id + + except Exception as e: + logger.error(f"Error saving application: {e}") + return None + +@bot.callback_query_handler(func=lambda call: call.data in [ + 'confirm_application', 'restart_questionnaire', 'cancel_to_catalog', 'go_to_main_menu' +]) +def handle_confirmation_actions(call): + """Обработка действий подтверждения""" + user_id = call.from_user.id + + if user_id not in user_states: + bot.answer_callback_query(call.id, "Сессия устарела") + return + + state = user_states[user_id] + studio_id = state['studio_id'] + answers = state.get('answers', {}) + + if call.data == 'confirm_application': + # СОХРАНЯЕМ В БД с передачей имени пользователя + application_id = save_application_to_db( + telegram_id=user_id, + studio_id=studio_id, + answers=answers, + user_first_name=call.from_user.first_name, + user_last_name=call.from_user.last_name + ) + + if application_id: + bot.answer_callback_query(call.id, "✅ Заявка отправлена") + + markup = types.InlineKeyboardMarkup() + markup.row(types.InlineKeyboardButton("🏠 Главное меню", callback_data='go_to_main_menu')) + + bot.edit_message_text( + "✅ Ваша заявка отправлена успешно!", + call.message.chat.id, + call.message.message_id, + reply_markup=markup + ) + else: + bot.answer_callback_query(call.id, "❌ Ошибка при сохранении") + + bot.edit_message_text( + "❌ Произошла ошибка при сохранении заявки. Попробуйте позже.", + call.message.chat.id, + call.message.message_id + ) + + # Очищаем состояние + del user_states[user_id] + + elif call.data == 'restart_questionnaire': + # Начинаем заново + state['current_question_index'] = 0 + state['answers'] = {} + state['state'] = 'answering' + + bot.edit_message_text( + "Начинаем заполнение анкеты заново...", + call.message.chat.id, + call.message.message_id + ) + + import time + time.sleep(1) + + questionnaire_handler.ask_next_question(call.message.chat.id, user_id) + + elif call.data == 'cancel_to_catalog': + # Отменяем и возвращаем в каталог + if user_id in user_states: + del user_states[user_id] + + # Загружаем студии и показываем каталог + studios = load_active_studios() + if studios: + markup = types.InlineKeyboardMarkup() + for studio_id, name in studios: + btn = types.InlineKeyboardButton(name, callback_data=f'info:{studio_id}') + markup.row(btn) + + bot.edit_message_text( + "Заполнение анкеты отменено.\n\nВыберите студию:", + call.message.chat.id, + call.message.message_id, + reply_markup=markup + ) + else: + bot.edit_message_text( + "Заполнение анкеты отменено.", + call.message.chat.id, + call.message.message_id + ) + + elif call.data == 'go_to_main_menu': + # Возвращаем в главное меню + if user_id in user_states: + del user_states[user_id] + + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + btn3 = types.KeyboardButton('Подать заявку') + markup.row(btn2, btn3) + + bot.edit_message_text( + "Вы вернулись в главное меню.", + call.message.chat.id, + call.message.message_id, + reply_markup=None + ) + + bot.send_message( + call.message.chat.id, + f'Привет! В этом боте ты можешь узнать про творческие студии' + f' ГУАП и стать частью активной студенческой жизни!', + parse_mode='html', + reply_markup=markup + ) + + +# Обработка текстовых сообщений +@bot.message_handler(func=lambda message: True, content_types=['text']) +def handle_all_text_messages(message): + """Обработка всех текстовых сообщений""" + user_id = message.from_user.id + + # Проверяем, находится ли пользователь в процессе заполнения анкеты + if user_id in user_states and user_states[user_id].get('state') == 'answering': + questionnaire_handler.process_answer(message) + return + + # Обработка других команд + if message.text == 'Мои заявки': + bot.send_message(message.chat.id, "Функция 'Мои заявки' в разработке") + elif message.text == 'Подать заявку': + inline_button_catalog(message) + else: + bot.send_message(message.chat.id, "Используйте кнопки меню или команду /start") + +# Запуск бота +if __name__ == '__main__': + print("Бот запущен...") + bot.polling(none_stop=True) + + diff --git a/.venv/users_collector.py b/.venv/users_collector.py new file mode 100644 index 0000000..d077ddc --- /dev/null +++ b/.venv/users_collector.py @@ -0,0 +1,220 @@ +from typing import Dict, List, Any, Optional, Callable +from questions import get_question_handler, get_question_obj, ALL_QUESTIONS + +class QuestionnaireBuilder: + """Сборщик анкет для разных студий""" + + STUDIO_QUESTIONNAIRES = { + # ID студии: [список ID вопросов из ALL_QUESTIONS] + 1: { # Танцевальная студия + 'name': 'Танцевальная студия', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'dance_style', # 5. Стиль танца + 'experience', # 6. Опыт + 'motivation', # 7. Мотивация + 'activeness', # 8. Участие в мероприятиях + ] + }, + 6: { # КВН + 'name': 'КВН', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'experience', # 5. Опыт + 'why_choose_us', # 6. Почему выбрали нас + 'time_commitment',# 7. Временные возможности + ] + }, + 2: { # Театральная студия + 'name': 'Театральная студия', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'theater_role', # 5. Театральные роли + 'experience', # 6. Опыт + 'motivation', # 7. Мотивация + 'activeness', # 8. Участие в мероприятиях + ] + }, + 4: { # МУЗГУАП + 'name': 'МУЗГУАП', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'music_instrument', # 5. Музыкальный инструмент + 'singing_experience', # 6. Опыт пения + 'expectations', # 7. Ожидания + 'activeness', # 8. Участие в мероприятиях + ] + }, + 5: { # Медиацентр + 'name': 'Медиацентр', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'tech_skills', # 5. Технические навыки + 'experience', # 6. Опыт + 'project_ideas', # 7. Идеи для проектов + 'time_commitment',# 8. Временные возможности + ] + }, + 3: { # Техничка + 'name': 'Студия технического обеспечения мероприятий', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'tech_skills', # 5. Технические навыки + # 'feedback_source',# 8. Откуда узнали + ] + }, + 7: { # Студия организации мероприятий + 'name': 'Студия организации мероприятий', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'organizational_exp', # 5. Организационный опыт + 'motivation', # 6. Мотивация + 'activeness', # 7. Участие в мероприятиях + 'tech_skills', # 8. Технические навыки + ] + }, + 8: { # студия ведущих + 'name': 'Студия ведущих', + 'questions': [ + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'experience', # 5. Опыт в деятельности + 'project_ideas', # 6. Идеи для проектов + 'expectations', # 7. Ожидания + 'time_commitment',# 8. Временные возможности + ] + }, + } + + @classmethod + def get_questionnaire_for_studio(cls, studio_id: int) -> Optional[Dict[str, Any]]: + """Получает конфигурацию анкеты для студии""" + return cls.STUDIO_QUESTIONNAIRES.get(studio_id) + + @classmethod + def get_studio_name(cls, studio_id: int) -> str: + """Получает название студии""" + config = cls.get_questionnaire_for_studio(studio_id) + return config.get('name', 'Неизвестная студия') if config else 'Неизвестная студия' + + @classmethod + def get_question_sequence(cls, studio_id: int) -> List[str]: + """Получает последовательность вопросов для студии""" + config = cls.get_questionnaire_for_studio(studio_id) + return config.get('questions', []) if config else [] + + @classmethod + def build_questionnaire_handlers(cls, studio_id: int) -> List[Callable]: + """Собирает список обработчиков вопросов для студии""" + question_ids = cls.get_question_sequence(studio_id) + handlers = [] + + for qid in question_ids: + handler = get_question_handler(qid) + if handler: + handlers.append(handler) + + return handlers + + @classmethod + def get_question_objects(cls, studio_id: int) -> List[Any]: + """Получает объекты вопросов для студии""" + question_ids = cls.get_question_sequence(studio_id) + questions = [] + + for qid in question_ids: + q_obj = get_question_obj(qid) + if q_obj: + questions.append(q_obj) + + return questions + + @classmethod + def get_autofill_fields(cls, studio_id: int) -> List[str]: + """Получает список полей для автозаполнения""" + questions = cls.get_question_objects(studio_id) + autofill_fields = [] + + for q in questions: + if q.autofill_field: + autofill_fields.append(q.autofill_field) + + # Убираем дубликаты + return list(set(autofill_fields)) + + #У нас каждый раз новая анкета, зачем автозаполнение?? + @classmethod + def can_autofill(cls, studio_id: int, user_data: Dict[str, Any]) -> bool: + """Проверяет, можно ли автозаполнить анкету""" + autofill_fields = cls.get_autofill_fields(studio_id) + + if not autofill_fields: + return False + + # Проверяем, сколько полей можно автозаполнить + fillable_count = 0 + for field in autofill_fields: + value = user_data.get(field, '') + if value and str(value).strip(): + fillable_count += 1 + + return fillable_count == len(autofill_fields) + + @classmethod + def generate_summary(cls, studio_id: int, answers: Dict[str, str]) -> str: + """Генерирует сводку по ответам""" + questions = cls.get_question_objects(studio_id) + studio_name = cls.get_studio_name(studio_id) + + lines = [f"📋 *Анкета для студии: {studio_name}*\n"] + + for i, q in enumerate(questions, 1): + answer = answers.get(q.id, "❌ Не заполнено") + lines.append(f"\n{i}. *{q.text}*\n{answer}") + + return "\n".join(lines) + + @classmethod + def validate_answers(cls, studio_id: int, answers: Dict[str, str]) -> tuple[bool, Dict[str, str]]: + """Валидирует все ответы анкеты""" + questions = cls.get_question_objects(studio_id) + errors = {} + + for q in questions: + answer = answers.get(q.id, '').strip() + + if q.is_required and not answer: + errors[q.id] = "Обязательный вопрос не заполнен" + continue + + is_valid, error_msg = q.validate(answer) + if not is_valid: + errors[q.id] = error_msg + + return len(errors) == 0, errors + + + From 7146fc7f296f94bb204fd712ee43a8a75a6fb559 Mon Sep 17 00:00:00 2001 From: Anastasia Lukina Date: Sun, 21 Dec 2025 19:24:51 +0300 Subject: [PATCH 09/12] =?UTF-8?q?=D0=9E=D1=81=D1=82=D0=B0=D0=BB=D1=81?= =?UTF-8?q?=D1=8F=20=D1=80=D1=83=D0=BA=20=D1=81=D1=82=D1=83=D0=B4=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .venv/Admin.py | 187 ++++++++-- .venv/Dtbase_config.py | 20 -- .venv/Head_of_studio.py | 514 +++++++++++++++++++------- .venv/questions.py | 280 ++++----------- .venv/student_studios_bot (1).db | Bin 57344 -> 57344 bytes .venv/studio_manager.py | 595 +++++++++++++++++++++++++++++++ .venv/user (2).py | 563 ----------------------------- .venv/{user (3).py => user.py} | 151 ++++---- .venv/users_collector.py | 70 ++-- 9 files changed, 1311 insertions(+), 1069 deletions(-) delete mode 100644 .venv/Dtbase_config.py create mode 100644 .venv/studio_manager.py delete mode 100644 .venv/user (2).py rename .venv/{user (3).py => user.py} (86%) diff --git a/.venv/Admin.py b/.venv/Admin.py index 5ce428d..10de175 100644 --- a/.venv/Admin.py +++ b/.venv/Admin.py @@ -1,7 +1,6 @@ from gettext import textdomain -from telegram import Update -from telegram.ext import CallbackContext +from typing import Optional import telebot from telebot import types import uuid # Для генерации уникальных имен файлов @@ -9,7 +8,8 @@ import sqlite3 as sq import os -bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") +# Импортируем bot из основного файла (будет установлен в initialize) +bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") def get_connection(): # Путь к вашей базе данных @@ -19,38 +19,80 @@ def get_connection(): #====================== #========ADMIN========= #====================== +# Функция инициализации модуля +def initialize(bot_instance): + global bot + bot = bot_instance + print("Admin module initialized") + return bot -@bot.message_handler(commands=['start']) def admin_start(message): markup = types.ReplyKeyboardMarkup(resize_keyboard=True) btn1 = types.KeyboardButton('Статистика') - btn2=types.KeyboardButton('Рассылка') + btn2 = types.KeyboardButton('Рассылка') btn3 = types.KeyboardButton('Назначить администратора') - markup.row(btn1,btn2,btn3) + markup.row(btn1, btn2, btn3) btn4 = types.KeyboardButton('Изменить информацию о студиях') markup.row(btn4) bot.send_message(message.chat.id, - f' Добро пожаловать, {message.from_user.first_name}.', parse_mode='html', reply_markup=markup) + f' Добро пожаловать, {message.from_user.first_name}.', + parse_mode='html', + reply_markup=markup) -@bot.message_handler(func=lambda message: message.text == 'Статистика') -def stat(message): - bot.send_message(message.chat.id, - f'Статистика активности:', parse_mode='html') +# Регистрация всех хендлеров администратора -@bot.message_handler(func=lambda message: message.text == 'Рассылка') -def stat(message): - bot.send_message(message.chat.id, - f'Введите сообщение:', parse_mode='html') +# Функции-обработчики (без декораторов!) +def handle_statistics(message): + """Обработчик кнопки 'Статистика'""" + bot.send_message(message.chat.id, f'Статистика активности:', parse_mode='html') +def handle_broadcast(message): + """Обработчик кнопки 'Рассылка'""" + bot.send_message(message.chat.id, f'Введите сообщение:', parse_mode='html') -@bot.message_handler(func=lambda message: message.text == 'Назначить администратора') -def give_role(message): - bot.send_message(message.chat.id, - "Перешлите сообщение пользователя или введите @username") +def handle_give_role(message): + """Обработчик кнопки 'Назначить администратора'""" + bot.send_message(message.chat.id, "Перешлите сообщение пользователя или введите @username") bot.register_next_step_handler(message, get_admin_username) +# Главный обработчик для админа +@bot.message_handler(func=lambda message: True, content_types=['text']) +def handle_admin_message(message): + """Обрабатывает сообщения для админа""" + if not message.text: + return False + + text=message.text + print(text) + # Обработка кнопок из admin_start + if text == 'Статистика': + handle_statistics(message) + return True + + elif text == 'Рассылка': + handle_broadcast(message) + # Для рассылки нужно обработать следующий шаг + # Это можно сделать через bot.register_next_step_handler + bot.register_next_step_handler(message, process_broadcast) + return True + + elif text == 'Назначить администратора': + handle_give_role(message) + # Следующий шаг - получение username + bot.register_next_step_handler(message, get_admin_username) + return True + + elif text == 'Изменить информацию о студиях': + handle_studio_info(message) + return True + + else: + # Если сообщение не обработано + return False + +#Надо забирать еще айдишник в тг def get_admin_username(message): # Получаем username admin_username = None @@ -145,44 +187,119 @@ def save_admin_to_db(username, name, phone, chat_id): with get_connection() as conn: cursor = conn.cursor() + # Получаем или создаем telegram_id для пользователя + # В пересланных сообщениях используем forward_from.id, в текстовом вводе - нужно получить ID пользователя + # chat_id - это ID чата отправителя (обычно совпадает с user_id для личных чатов) + + telegram_id = None + # Проверяем, есть ли пользователь в базе - cursor.execute("SELECT user_id FROM users WHERE username = ?", (username,)) - user = cursor.fetchone() + if '@' in username: + # Если username начинается с @ + clean_username = username.replace('@', '') + cursor.execute("SELECT telegram_id FROM users WHERE username LIKE ?", (f'%{clean_username}%',)) + result = cursor.fetchone() + + if result: + telegram_id = result[0] + else: + # Для новых пользователей используем chat_id как telegram_id + telegram_id = chat_id + else: + # Если username - это число (ID) + try: + telegram_id = int(username) + except ValueError: + telegram_id = chat_id + + # Проверяем, существует ли пользователь с таким telegram_id + cursor.execute("SELECT COUNT(*) FROM users WHERE telegram_id = ?", (telegram_id,)) + user_exists = cursor.fetchone()[0] > 0 - if user: + if user_exists: # Обновляем существующего пользователя cursor.execute(""" UPDATE users - SET role = 'head', + SET username = ?, + role = 'head', full_name = ?, phone_number = ?, created_at = datetime('now') - WHERE username = ? - """, (name, phone, username)) + WHERE telegram_id = ? + """, (username, name, phone, telegram_id)) bot.send_message(chat_id, - f"✅ {username} назначен администратором\n" + f"✅ Пользователь обновлен как администратор\n" + f"🆔 Telegram ID: {telegram_id}\n" + f"👤 Username: {username}\n" f"📛 ФИО: {name}\n" f"📞 Телефон: {phone}") else: # Добавляем нового пользователя cursor.execute(""" - INSERT INTO users (username, full_name, phone_number, role, created_at) - VALUES (?, ?, ?, 'head', datetime('now')) - """, (username, name, phone)) + INSERT INTO users (telegram_id, username, full_name, phone_number, role, created_at) + VALUES (?, ?, ?, ?, 'head', datetime('now')) + """, (telegram_id, username, name, phone)) bot.send_message(chat_id, f"✅ Добавлен администратор:\n" + f"🆔 Telegram ID: {telegram_id}\n" f"👤 Username: {username}\n" f"📛 ФИО: {name}\n" f"📞 Телефон: {phone}\n" f"🎖️ Роль: Администратор (руководитель) студии") - #Добавляем в таблицу studios студию с данным админом - user_id = cursor.lastrowid + + # Теперь добавляем в таблицу studios, используя telegram_id как head_user_id cursor.execute(""" - INSERT INTO studios (name, description, contacts, promo_photo_id, promo_video_id, is_active, - head_user_id, invite_code) - VALUES (?, ?, ?, ?, ?,? ,?, ?) - """, ("None" , None, None, None , None, 1, user_id , None)) + INSERT INTO studios (name, description, contacts, promo_photo_id, + promo_video_id, is_active, head_user_id, invite_code) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, (f"Студия {name}", f"Студия под руководством {name}", + phone, None, None, 1, telegram_id, None)) conn.commit() +# def save_admin_to_db(username, name, phone, chat_id): +# with get_connection() as conn: +# cursor = conn.cursor() +# +# # Проверяем, есть ли пользователь в базе +# cursor.execute("SELECT user_id FROM users WHERE username = ?", (username,)) +# user = cursor.fetchone() +# +# if user: +# # Обновляем существующего пользователя +# cursor.execute(""" +# UPDATE users +# SET role = 'head', +# full_name = ?, +# phone_number = ?, +# created_at = datetime('now') +# WHERE username = ? +# """, (name, phone, username)) +# bot.send_message(chat_id, +# f"✅ {username} назначен администратором\n" +# f"📛 ФИО: {name}\n" +# f"📞 Телефон: {phone}") +# else: +# # Добавляем нового пользователя +# cursor.execute(""" +# INSERT INTO users (username, full_name, phone_number, role, created_at) +# VALUES (?, ?, ?, 'head', datetime('now')) +# """, (username, name, phone)) +# bot.send_message(chat_id, +# f"✅ Добавлен администратор:\n" +# f"👤 Username: {username}\n" +# f"📛 ФИО: {name}\n" +# f"📞 Телефон: {phone}\n" +# f"🎖️ Роль: Администратор (руководитель) студии") +# #Добавляем в таблицу studios студию с данным админом +# user_id = cursor.lastrowid +# cursor.execute(""" +# INSERT INTO studios (name, description, contacts, promo_photo_id, promo_video_id, is_active, +# head_user_id, invite_code) +# VALUES (?, ?, ?, ?, ?,? ,?, ?) +# """, ("None" , None, None, None , None, 0, user_id , None)) +# +# conn.commit() + + diff --git a/.venv/Dtbase_config.py b/.venv/Dtbase_config.py deleted file mode 100644 index bdf4655..0000000 --- a/.venv/Dtbase_config.py +++ /dev/null @@ -1,20 +0,0 @@ -from telebot import types -import sqlite3 as sq - -bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") - -#-----------------admin-------------------------- -#------------------------------------------------ -def add_studio(): - with sq.connect("student_studios_bot (1).db") as con: - cur=con.cursor() - -#Добавляем пока первичную информацию о студии - cur.execute("""INSERT into studios name promo_photo_id promo_video_id description contacts """, - {name,}) - - con.commit() - con.close() - - - diff --git a/.venv/Head_of_studio.py b/.venv/Head_of_studio.py index 825ba2c..14fd07e 100644 --- a/.venv/Head_of_studio.py +++ b/.venv/Head_of_studio.py @@ -3,169 +3,431 @@ import telebot from telebot import types import sqlite3 +from typing import Optional, List, Tuple +from datetime import datetime +import logging -bot= telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") +from studio_manager import StudioManager + +#В коде логгер нигде не используется??? +logging.basicConfig( + # %(asctime)s — время события + # %(name)s — имя логгера (модуля) + # %(levelname)s — уровень важности (INFO, ERROR, WARNING) + # %(message)s — текст сообщения + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# на уровне INFO: запуск/остановка процессов, критические действия пользователей, ошибки доступа,но не обычные действия пользователей + level=logging.INFO +) +logger = logging.getLogger(__name__) + +bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +# Инициализируем менеджер студии +studio_manager = StudioManager(bot) +user_sessions = {} # Хранит активные сессии руководителей + +studio_manager.user_states = {} # Хранит состояние просмотра заявок #====================== #====HEAD OF STUDIO==== #====================== -from telegram import Update -from telegram.ext import CallbackContext +def cleanup_stale_sessions(): + #Очистка зависших сессий более чем на час - пока не решила куда вставить + current_time = datetime.now() + stale_users = [] + for user_id, session in user_sessions.items(): + if (current_time - session['start_time']).seconds > 3600: # 1 час + stale_users.append(user_id) + + for user_id in stale_users: + del user_sessions[user_id] + if user_id in studio_manager.user_states: + del studio_manager.user_states[user_id] + logger.warning(f"Очищена зависшая сессия пользователя {user_id}") def show_main_menu(markup): - btn1 = types.KeyboardButton('Статистика') + btn1 = types.KeyboardButton('') btn2=types.KeyboardButton('Новые заявки') markup.row(btn1, btn2) btn3 = types.KeyboardButton('Изменить информацию о студии') -def head_start(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - show_main_menu(markup) - - bot.send_message(message.chat.id, - f'Добро пожаловать, {message.from_user.first_name}.', parse_mode='html', - reply_markup=markup) +def get_connection() -> sqlite3.Connection: + """Возвращает соединение с БД""" + return sqlite3.connect("student_studios_bot (1).db", check_same_thread=False) -@bot.message_handler(func=lambda message: message.text == 'Главное меню') -def main_menu(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - show_main_menu(markup) - - bot.send_message(message.chat.id, - f'Вы вернулись в меню.', parse_mode='html', - reply_markup=markup) - -@bot.message_handler(func=lambda message: message.text == 'Статистика') -def stat(message): - bot.send_message(message.chat.id, - f'Статистика подачи заявок в студию', parse_mode='html') - -@bot.message_handler(func=lambda message: message.text == 'Новые заявки') -def appls(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn_accept = types.KeyboardButton ('❤️-Принять') - btn_refuse = types.KeyboardButton ('Отклонить') - btn_communicate = types.KeyboardButton ('💬-Связаться') - markup.row(btn_accept, btn_communicate, btn_refuse) - btn_menu=(types.KeyboardButton('Главное меню')) - markup.row(btn_menu) - - bot.send_message(message.chat.id, - f'Просмотр заявок:', parse_mode='html', reply_markup=markup) - - -# ---------------------------ИЗМЕНЕНИЕ ИНФОРМАЦИИ О СТУДИИ--------------------------- - -@bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') -# ==================== -# РЕДАКТИРОВАНИЕ СТУДИИ -# ==================== - -@bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') -def edit_info(message): - """ - Обработчик кнопки 'Изменить информацию о студии'. - Начинает процесс пошагового редактирования студии. - """ - user_id = message.from_user.id +def create_main_menu_markup(user_id: int = None) -> types.ReplyKeyboardMarkup: + """Создает главное меню руководителя""" + markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) - # Начинаем процесс редактирования через наш класс StudioEditor - response = studio_editor.start_editing(user_id) + + btn_statistics = types.KeyboardButton('Отчет по заявкам') + btn_applications = types.KeyboardButton("Новые заявки") + markup.row(btn_statistics, btn_applications) + + btn_refresh = types.KeyboardButton('Обновить') + markup.row(btn_refresh) + + return markup - # Отправляем первое сообщение с инструкцией +@bot.message_handler(commands=['head']) +def handle_head_start(message): + """Обработчик команды для входа руководителя""" + user_id = message.from_user.id + first_name = message.from_user.first_name + + studio_id = studio_manager.get_studio_id_for_head(user_id) + + if not studio_id: + bot.send_message( + message.chat.id, + "❌ Доступ запрещен\n\n" + "Вы не зарегистрированы как руководитель студии.", + parse_mode='HTML' + ) + return + + # Получаем название студии + studio_name = studio_manager.get_studio_name(studio_id) + + # Получаем количество ожидающих заявок + applications = studio_manager.get_new_applications(studio_id) + pending_count = len(applications) + + welcome_message = ( + f"👋 Добро пожаловать, {first_name}!\n\n" + ) + + # Отправляем главное меню + markup = create_main_menu_markup(user_id) bot.send_message( message.chat.id, - response, - parse_mode='html' + welcome_message, + parse_mode='HTML', + reply_markup=markup ) - -@bot.message_handler(func=lambda message: studio_editor.get_user_step(message.from_user.id) in - ['waiting_for_name', 'waiting_for_description']) -def handle_text_for_editing(message): - """ - Обработчик текстовых сообщений во время редактирования студии. - - Этот обработчик срабатывает ТОЛЬКО когда пользователь находится - на этапе 'waiting_for_name' или 'waiting_for_description'. - """ +@bot.message_handler(func=lambda message: message.text.startswith('📋 Новые заявки')) +def handle_applications_start(message): + """Начало просмотра заявок""" user_id = message.from_user.id - - # Передаем текст сообщения в редактор студии - response, is_completed = studio_editor.handle_message(user_id, message.text) - - # Отправляем ответ пользователю + + # Проверяем, не идет ли уже просмотр + if user_id in studio_manager.user_states: + bot.send_message( + message.chat.id, + "⚠️ Просмотр уже начат\n\n" + "Завершите текущий просмотр или нажмите 'Прекратить просмотр'.", + parse_mode='HTML' + ) + return + + studio_id = studio_manager.get_studio_id_for_head(user_id) + if not studio_id: + bot.send_message( + message.chat.id, + "❌ 1Ошибка доступа", + parse_mode='HTML' + ) + return + + # Получаем название студии + studio_name = studio_manager.get_studio_name(studio_id) + success = studio_manager.start_reviewing_applications_with_id(user_id, studio_id) + + if not success: + return + + # Запускаем сессию + user_sessions[user_id] = { + 'start_time': datetime.now(), + 'studio_name': studio_name, + 'total_applications': len(studio_manager.user_states[user_id]['applications']), + 'processed': 0, + 'accepted': 0, + 'rejected': 0 + } + + # Информационное сообщение + state = studio_manager.user_states[user_id] + total_apps = len(state['applications']) + bot.send_message( message.chat.id, - response, - parse_mode='html' + f"Начинаем просмотр заявок\n\n" + f"Студия: {studio_name}\n" + f"Всего заявок: {total_apps}\n", + parse_mode='HTML' ) + + # Показываем первую заявку + studio_manager.show_next_application(message.chat.id, user_id) - # Если процесс редактирования завершен, показываем главное меню - if is_completed: - # Показываем кнопки для дальнейших действий - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - show_main_menu(markup) - +@bot.message_handler(func=lambda message: message.text == 'отчет по заявкам') +def handle_statistics(message): + user_id = message.from_user.id + + studio_id = studio_manager.get_studio_id_for_head(user_id) + if not studio_id: bot.send_message( message.chat.id, - "Что дальше?", - reply_markup=markup + "❌ 2Ошибка доступа", + parse_mode='HTML' ) - - -@bot.message_handler(content_types=['photo']) -def handle_photo_for_editing(message): - """ - Обработчик фото во время редактирования студии. - - Этот обработчик срабатывает, когда пользователь отправляет фото. - Проверяем, находится ли пользователь на этапе ожидания изображения. - """ + return + + stats = studio_manager.get_statistics(studio_id) + studio_name = studio_manager.get_studio_name(studio_id) + + # Форматируем сообщение + message_text = studio_manager.format_statistics_message(stats, studio_name) + + + +@bot.message_handler(func=lambda message: message.text == 'Обновить') +def handle_refresh(message): + """Обновление списка заявок""" user_id = message.from_user.id - - # Проверяем, находится ли пользователь на этапе ожидания изображения - if studio_editor.get_user_step(user_id) != 'waiting_for_image': - # Если нет - игнорируем фото или сообщаем об ошибке + print(user_id) + + studio_id = studio_manager.get_studio_id_for_head(user_id) + if not studio_id: + bot.send_message( + message.chat.id, + "❌ 3Ошибка доступа", + parse_mode='HTML' + ) return - - # Получаем информацию о самом большом фото (Telegram отправляет несколько размеров) - file_id = message.photo[-1].file_id - - # Скачиваем фото с серверов Telegram - file_info = bot.get_file(file_id) - image_bytes = bot.download_file(file_info.file_path) - - # Передаем изображение в редактор студии - response, is_completed = studio_editor.handle_image(user_id, image_bytes) - - # Отправляем ответ пользователю + + # Получаем актуальный список заявок + applications = studio_manager.get_new_applications(studio_id) + studio_name = studio_manager.get_studio_name(studio_id) + + # Обновляем меню + markup = create_main_menu_markup(user_id) + + if applications: + message_text = ( + f"Список заявок обновлен\n\n" + f"Заявок в очереди: {len(applications)}\n\n" + f"Нажмите 'Новые заявки' для начала просмотра" + ) + else: + message_text = ( + f"Список заявок обновлен\n\n" + f"Заявок в очереди: 0\n\n" + f"Новых заявок пока нет" + ) + bot.send_message( message.chat.id, - response, - parse_mode='html' + message_text, + parse_mode='HTML', + reply_markup=markup ) - -@bot.message_handler(commands=['cancel']) -def cancel_editing(message): - """ - Обработчик команды /cancel для отмены редактирования. - """ +@bot.message_handler(func=lambda message: message.text == 'Главное меню') +def handle_main_menu(message): + """Возврат в главное меню""" user_id = message.from_user.id - response = studio_editor.cancel_editing(user_id) - - # Возвращаем в главное меню - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - show_main_menu(markup) - + + # Очищаем состояние просмотра + if user_id in studio_manager.user_states: + del studio_manager.user_states[user_id] + + + del user_sessions[user_id] + + # Возвращаем главное меню + markup = create_main_menu_markup(user_id) bot.send_message( message.chat.id, - response, - parse_mode='html', + "🏠 Главное меню руководителя", + parse_mode='HTML', reply_markup=markup ) - +@bot.callback_query_handler(func=lambda call: call.data.startswith(('accept:', 'reject:', 'skip:')) or + call.data == 'stop_review') +def handle_review_callbacks(call): + """Обработка callback-ов от просмотра заявок""" + user_id = call.from_user.id + + # Обновляем статистику сессии + if user_id in user_sessions: + session = user_sessions[user_id] + + if call.data.startswith('accept:'): + session['accepted'] += 1 + session['processed'] += 1 + elif call.data.startswith('reject:'): + session['rejected'] += 1 + session['processed'] += 1 + elif call.data.startswith('skip:'): + session['processed'] += 1 + + # Передаем обработку в менеджер студии + studio_manager.handle_application_action(call) + +@bot.message_handler(func=lambda message: True) +def handle_other_messages(message): + """Обработчик всех других сообщений""" + user_id = message.from_user.id + + # Если пользователь в режиме просмотра + if user_id in studio_manager.user_states: + bot.send_message( + message.chat.id, + "⚠️ Вы в режиме просмотра заявок\n\n" + "Используйте кнопки под заявкой для действий.", + parse_mode='HTML' + ) + else: + # Проверяем, является ли руководителем + # studio_id = studio_manager.get_studio_id_for_head(user_id) + studio_id = True + + if studio_id: + # Показываем меню руководителя + markup = create_main_menu_markup(user_id) + bot.send_message( + message.chat.id, + "Главное меню руководителя", + parse_mode='HTML', + reply_markup=markup + ) + else: + # Показываем обычное меню + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + btn3 = types.KeyboardButton('Подать заявку') + markup.row(btn2, btn3) + + bot.send_message( + message.chat.id, + "👋 Добро пожаловать в бот студенческих студий!", + parse_mode='HTML', + reply_markup=markup + ) + + + +# # ---------------------------ИЗМЕНЕНИЕ ИНФОРМАЦИИ О СТУДИИ--------------------------- +# +# @bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') +# # ==================== +# # РЕДАКТИРОВАНИЕ СТУДИИ +# # ==================== +# +# @bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') +# def edit_info(message): +# """ +# Обработчик кнопки 'Изменить информацию о студии'. +# Начинает процесс пошагового редактирования студии. +# """ +# user_id = message.from_user.id +# +# # Начинаем процесс редактирования через наш класс StudioEditor +# response = studio_editor.start_editing(user_id) +# +# # Отправляем первое сообщение с инструкцией +# bot.send_message( +# message.chat.id, +# response, +# parse_mode='html' +# ) +# +# +# @bot.message_handler(func=lambda message: studio_editor.get_user_step(message.from_user.id) in +# ['waiting_for_name', 'waiting_for_description']) +# def handle_text_for_editing(message): +# """ +# Обработчик текстовых сообщений во время редактирования студии. +# +# Этот обработчик срабатывает ТОЛЬКО когда пользователь находится +# на этапе 'waiting_for_name' или 'waiting_for_description'. +# """ +# user_id = message.from_user.id +# +# # Передаем текст сообщения в редактор студии +# response, is_completed = studio_editor.handle_message(user_id, message.text) +# +# # Отправляем ответ пользователю +# bot.send_message( +# message.chat.id, +# response, +# parse_mode='html' +# ) +# +# # Если процесс редактирования завершен, показываем главное меню +# if is_completed: +# # Показываем кнопки для дальнейших действий +# markup = types.ReplyKeyboardMarkup(resize_keyboard=True) +# show_main_menu(markup) +# +# bot.send_message( +# message.chat.id, +# "Что дальше?", +# reply_markup=markup +# ) +# +# +# @bot.message_handler(content_types=['photo']) +# def handle_photo_for_editing(message): +# """ +# Обработчик фото во время редактирования студии. +# +# Этот обработчик срабатывает, когда пользователь отправляет фото. +# Проверяем, находится ли пользователь на этапе ожидания изображения. +# """ +# user_id = message.from_user.id +# +# # Проверяем, находится ли пользователь на этапе ожидания изображения +# if studio_editor.get_user_step(user_id) != 'waiting_for_image': +# # Если нет - игнорируем фото или сообщаем об ошибке +# return +# +# # Получаем информацию о самом большом фото (Telegram отправляет несколько размеров) +# file_id = message.photo[-1].file_id +# +# # Скачиваем фото с серверов Telegram +# file_info = bot.get_file(file_id) +# image_bytes = bot.download_file(file_info.file_path) +# +# # Передаем изображение в редактор студии +# response, is_completed = studio_editor.handle_image(user_id, image_bytes) +# +# # Отправляем ответ пользователю +# bot.send_message( +# message.chat.id, +# response, +# parse_mode='html' +# ) +# +# +# @bot.message_handler(commands=['cancel']) +# def cancel_editing(message): +# """ +# Обработчик команды /cancel для отмены редактирования. +# """ +# user_id = message.from_user.id +# response = studio_editor.cancel_editing(user_id) +# +# # Возвращаем в главное меню +# markup = types.ReplyKeyboardMarkup(resize_keyboard=True) +# show_main_menu(markup) +# +# bot.send_message( +# message.chat.id, +# response, +# parse_mode='html', +# reply_markup=markup +# ) + +if __name__ == '__main__': + print("Бот запущен...") + bot.polling(none_stop=True) diff --git a/.venv/questions.py b/.venv/questions.py index 184c730..2b4805e 100644 --- a/.venv/questions.py +++ b/.venv/questions.py @@ -1,28 +1,24 @@ -from typing import Dict, Any, Callable, Optional -import telebot -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.ext import CallbackContext + +from typing import Dict, Any, Callable, Optional import re class Question: """Класс для хранения вопроса""" def __init__(self, question_id: str, - text: str, #Текст вопроса выведет бот, зачем это? + text: str, question_type: str = 'text', is_required: bool = True, autofill_field: Optional[str] = None, validation_func: Optional[Callable] = None, - options: Optional[list] = None, - keyboard: Optional[list] = None): + options: Optional[list] = None): self.id = question_id self.text = text - self.type = question_type # text, phone, email, boolean, select, multi_select + self.type = question_type # text, phone, email, boolean, select self.is_required = is_required self.autofill_field = autofill_field # Поле для автозаполнения из БД self.validation_func = validation_func self.options = options or [] - self.keyboard = keyboard def validate(self, answer: str) -> tuple[bool, str]: """Валидация ответа""" @@ -35,7 +31,7 @@ def validate(self, answer: str) -> tuple[bool, str]: return self.validation_func(answer) # Базовая валидация по типу - if self.type == 'phone': + if self.type == 'phone_number': phone_digits = re.sub(r'[^\d]', '', answer) if len(phone_digits) < 10: return False, "Введите корректный номер телефона" @@ -55,210 +51,76 @@ def validate(self, answer: str) -> tuple[bool, str]: return True, "" -# ------ Общие вопросы------ -# update: Update - объект, содержащий информацию о входящем сообщении/действии -# context: CallbackContext - объект для хранения состояния и данных между вызовами -# update.message содержит информацию о сообщении от пользователя -# reply_text() отправляет ответ в тот же чат - -def ask_full_name(update: Update, context: CallbackContext) -> None: - """Вопрос: ФИО""" - update.message.reply_text("1. 📝 Укажите ваше ФИО (полностью):") - -def ask_group(update: Update, context: CallbackContext) -> None: - """Вопрос: Группа""" - update.message.reply_text("2. 🎓 Укажите номер вашей группу:") - -def ask_phone(update: Update, context: CallbackContext) -> None: - """Вопрос: Телефон""" - update.message.reply_text("3. 📱 Укажите ваш номер телефона:") - -def ask_email(update: Update, context: CallbackContext) -> None: - """Вопрос: Email""" - update.message.reply_text("4. 📧 Укажите ваш email адрес:") - -def ask_experience(update: Update, context: CallbackContext) -> None: - """Вопрос: Опыт в деятельности""" - update.message.reply_text("5. 💼 Расскажите о вашем опыте в данной деятельности:") - -def ask_motivation(update: Update, context: CallbackContext) -> None: - """Вопрос: Мотивация""" - update.message.reply_text("6. 🎯 Почему вы хотите вступить в эту студию?") - -def ask_expectations(update: Update, context: CallbackContext) -> None: - """Вопрос: Ожидания""" - update.message.reply_text("7. 🌟 Какие у вас ожидания от участия в студии?") - -def ask_activeness(update: Update, context: CallbackContext) -> None: - """Вопрос: Участие в мероприятиях""" - keyboard = [ - [InlineKeyboardButton("Да", callback_data="activeness_yes"), - InlineKeyboardButton("Нет", callback_data="activeness_no")] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - "8. 📢 Готовы ли вы участвовать в мероприятиях студии?", - reply_markup=reply_markup - ) - -# ------ дополнительные опросы ------ - -#НЕ НУЖНО ИЛИ ИЗМЕНИТЬ НА ПИСЬМЕННЫЙ -def ask_dance_style(update: Update, context: CallbackContext) -> None: - """Вопрос: Стиль танца""" - keyboard = [ - [InlineKeyboardButton("Хип-хоп", callback_data="dance_hiphop")], - [InlineKeyboardButton("Бальные танцы", callback_data="dance_ballroom")], - [InlineKeyboardButton("Современные", callback_data="dance_modern")], - [InlineKeyboardButton("Народные", callback_data="dance_folk")] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - "9. 💃 Какой танцевальный стиль вам интересен?", - reply_markup=reply_markup - ) - -def ask_music_instrument(update: Update, context: CallbackContext) -> None: - """Вопрос: Музыкальный инструмент""" - update.message.reply_text("10. 🎵 На каком инструменте вы играете (если играете)?") - -def ask_singing_experience(update: Update, context: CallbackContext) -> None: - """Вопрос: Опыт пения""" - update.message.reply_text("11. 🎤 Есть ли у вас опыт пения? Если да, опишите его:") - -# НЕ НУЖНО ИЛИ ИЗМЕНИТЬ -def ask_theater_role(update: Update, context: CallbackContext) -> None: - """Вопрос: Предпочитаемые роли в театре""" - keyboard = [ - [InlineKeyboardButton("Драматические", callback_data="role_drama")], - [InlineKeyboardButton("Комические", callback_data="role_comedy")], - [InlineKeyboardButton("Характерные", callback_data="role_character")], - [InlineKeyboardButton("Не знаю", callback_data="role_unknown")] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - "12. 🎭 Какие роли вам интересны в театре?", - reply_markup=reply_markup - ) - -#СОМНИТЕЛЬНО, НАДО ДОБАВИТЬ ВЫБОР НАПРАВЛЕНИЯ: ТРАНСЛЯЦИИ, СВЕТ, ЗВУК -def ask_tech_skills(update: Update, context: CallbackContext) -> None: - """Вопрос: Технические навыки""" - update.message.reply_text("13. 💻 Какими техническими навыками вы владеете?") - -def ask_organizational_exp(update: Update, context: CallbackContext) -> None: - """Вопрос: Организационный опыт""" - update.message.reply_text("14. 📋 Был ли у вас опыт организации мероприятий? Опишите:") - -#ЛУЧШЕ ИЗМЕНИТЬ НА ВЫБОР НАПРАВЛЕНИЙ: РЕЖИССУРА, ПРОДЮССИРОВАНИЕ, СОБЫТИЙНОЕ ВОЛОНТЕРСТВО -def ask_leadership(update: Update, context: CallbackContext) -> None: - """Вопрос: Лидерские качества""" - keyboard = [ - [InlineKeyboardButton("Да, есть опыт", callback_data="leadership_yes")], - [InlineKeyboardButton("Хочу развивать", callback_data="leadership_develop")], - [InlineKeyboardButton("Пока нет", callback_data="leadership_no")] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - "15. 👨‍💼 Есть ли у вас лидерский опыт?", - reply_markup=reply_markup - ) - -def ask_project_ideas(update: Update, context: CallbackContext) -> None: - """Вопрос: Идеи для проектов""" - update.message.reply_text("16. 💡 Есть ли у вас идеи для проектов в студии?") - -def ask_time_commitment(update: Update, context: CallbackContext) -> None: - """Вопрос: Временные возможности""" - update.message.reply_text("17. ⏰ Сколько часов в неделю вы готовы уделять студии?") - -def ask_why_choose_us(update: Update, context: CallbackContext) -> None: - """Вопрос: Почему выбрали нас""" - update.message.reply_text("18. ❓ Почему вы выбрали именно нашу студию?") - -#МОЖНО ДОБАВИТЬ ВАРИАНТ "ДРУГОЕ" -def ask_feedback_source(update: Update, context: CallbackContext) -> None: - """Вопрос: Откуда узнали о студии""" - keyboard = [ - [InlineKeyboardButton("От друзей", callback_data="source_friends")], - [InlineKeyboardButton("Соцсети", callback_data="source_social")], - [InlineKeyboardButton("Сайт вуза", callback_data="source_website")], - [InlineKeyboardButton("Объявление", callback_data="source_poster")] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - "19. 📢 Откуда вы узнали о нашей студии?", - reply_markup=reply_markup - ) - -#ЛИШНЕЕ КАК БУДТО -def ask_special_needs(update: Update, context: CallbackContext) -> None: - """Вопрос: Особые потребности""" - update.message.reply_text("20. ♿ Есть ли у вас особые потребности, которые мы должны учитывать?") - - # ========== СЛОВАРЬ ВСЕХ ВОПРОСОВ ========== -#В словаре ALL_QUESTIONS каждая запись содержит: -# • Ключ: строковый идентификатор вопроса (например, 'full_name') -# • Значение: кортеж из двух элементов: -# 1. Функция-обработчик (например, ask_full_name) -# 2. Объект класса Question с метаданными вопроса - ALL_QUESTIONS = { - # ID вопроса: (функция-обработчик, объект Question) - 'full_name': (ask_full_name, Question('full_name', 'ФИО', autofill_field='full_name')), - 'group': (ask_group, Question('group', 'Группа', autofill_field='student_group')), - 'phone': (ask_phone, Question('phone', 'Телефон', 'phone', autofill_field='phone')), - 'email': (ask_email, Question('email', 'Email', 'email', autofill_field='email')), - 'experience': (ask_experience, Question('experience', 'Опыт в деятельности')), - 'motivation': (ask_motivation, Question('motivation', 'Мотивация вступления')), - 'expectations': (ask_expectations, Question('expectations', 'Ожидания от студии')), - 'activeness': (ask_activeness, Question('activeness', 'Участие в мероприятиях', 'boolean')), - 'dance_style': (ask_dance_style, Question('dance_style', 'Танцевальный стиль', 'select', - options=['Хип-хоп', 'Бальные танцы', 'Современные', 'Народные'])), - 'music_instrument': (ask_music_instrument, Question('music_instrument', 'Музыкальный инструмент')), - 'singing_experience': (ask_singing_experience, Question('singing_experience', 'Опыт пения')), - 'theater_role': (ask_theater_role, Question('theater_role', 'Театральные роли', 'select', - options=['Драматические', 'Комические', 'Характерные', 'Не знаю'])), - 'tech_skills': (ask_tech_skills, Question('tech_skills', 'Технические навыки')), - 'organizational_exp': (ask_organizational_exp, Question('organizational_exp', 'Организационный опыт')), - 'leadership': (ask_leadership, Question('leadership', 'Лидерский опыт', 'select', - options=['Да, есть опыт', 'Хочу развивать', 'Пока нет'])), - 'project_ideas': (ask_project_ideas, Question('project_ideas', 'Идеи для проектов')), - 'time_commitment': (ask_time_commitment, Question('time_commitment', 'Временные возможности')), - 'why_choose_us': (ask_why_choose_us, Question('why_choose_us', 'Почему выбрали нас')), - 'feedback_source': (ask_feedback_source, Question('feedback_source', 'Источник информации', 'select', - options=['От друзей', 'Соцсети', 'Сайт вуза', 'Объявление'])), - 'special_needs': (ask_special_needs, Question('special_needs', 'Особые потребности', is_required=False)) + # ID вопроса: (текст вопроса, объект Question) + 'full_name': ("Укажите ваше ФИО (полностью):", + Question('full_name', 'ФИО', autofill_field='full_name')), + + 'group': ("Укажите номер вашей группу:", + Question('group', 'Группа', autofill_field='student_group')), + + 'phone_number': ("Укажите ваш номер телефона:", + Question('phone_number', 'Телефон', 'phone_number', autofill_field='phone_number')), + + 'email': ("Укажите ваш email адрес:", + Question('email', 'Email', 'email', autofill_field='email')), + + 'experience': ("Расскажите о вашем опыте в данной деятельности:", + Question('experience', 'Опыт в деятельности')), + + 'motivation': ("Почему вы хотите вступить в эту студию?", + Question('motivation', 'Мотивация вступления')), + + 'expectations': ("Какие у вас ожидания от участия в студии?", + Question('expectations', 'Ожидания от студии')), + + 'activeness': ("Готовы ли вы участвовать в мероприятиях студии? (да/нет)", + Question('activeness', 'Участие в мероприятиях', 'boolean')), + + 'media': ("Какое направление вам интересно?\n" + "Варианты: Фотостуди, Видеостудия, Студия дизайна, Журналистика и СММ", + Question('dance_style', 'Танцевальный стиль', 'select', + options=['Фотостуди', 'Видеостудия', 'Студия дизайна', 'Журналистика и СММ'])), + + 'music_instrument': ("10. 🎵 На каком инструменте вы играете (если играете)?", + Question('music_instrument', 'Музыкальный инструмент')), + + 'singing_experience': ("Есть ли у вас опыт пения? Если да, опишите его:", + Question('singing_experience', 'Опыт пения')), + + 'improvisation': ("Как вы относитесь к импровизации?", + Question('improvisation', 'Импровизация')), + + 'tech_skills': ("Какими техническими навыками вы владеете?", + Question('tech_skills', 'Технические навыки')), + + 'organizational_exp': ("Был ли у вас опыт организации мероприятий? Опишите:", + Question('organizational_exp', 'Организационный опыт')), + 'quote': ("Девиз по жизни или цитата, которая вас вдохновляет:", + Question('quote', 'Цитата')), + + 'project_ideas': ("Есть ли у вас идеи для проектов в студии?", + Question('project_ideas', 'Идеи для проектов')), + 'leadership': ("Есть ли у вас лидерский опыт?", + Question('leadership', 'Лидерские качества')), + + 'why_choose_us': ("Почему вы выбрали именно нашу студию?", + Question('why_choose_us', 'Почему выбрали нас')), + + 'feedback_source': ("Откуда вы узнали о нашей студии?", + Question('feedback_source', 'Источник информации')), + + 'public_speaking': ("Как вы относитесь к публичным выступлениям??", + Question('public_speaking', 'Публичные выступления')), } -def get_question_handler(question_id: str): - # Извлекаем первый элемент кортежа - функцию-обработчик по айди - return ALL_QUESTIONS.get(question_id, (None, None))[0] +def get_question_handler(question_id: str) -> str: + """Возвращает текст вопроса по ID""" + result = ALL_QUESTIONS.get(question_id) + return result[0] if result else "" def get_question_obj(question_id: str) -> Optional[Question]: - # Извлекаем второй элемент кортежа - объект Question по айди вопроса + """Возвращает объект вопроса по ID""" result = ALL_QUESTIONS.get(question_id) return result[1] if result else None - -#ПРИВЯЗАТЬ БД -#Убрать пол текск, так как сам вопрос выводитя ботом, тогда будет так: - -#ALL_QUESTIONS = { - # 'full_name': Question( - # question_id='full_name', - # text="1. 📝 Укажите ваше ФИО (полностью):", - # autofill_field='full_name' - # ), - -# def ask_question(question_id: str, update: Update, context: CallbackContext) -> None: -# """Универсальная функция для отправки любого вопроса""" -# question = ALL_QUESTIONS.get(question_id) -# if question: -# question.ask(update, context) -# else: -# update.message.reply_text("Вопрос не найден") -# -# -# def ask_full_name(update: Update, context: CallbackContext) -> None: -# ask_question('full_name', update, context) diff --git a/.venv/student_studios_bot (1).db b/.venv/student_studios_bot (1).db index c868a8348ae1c3c92d5acb14fef5e31dc482bba1..f607c7674e915409f0962ddb01921ca8f12e9c9a 100644 GIT binary patch delta 3498 zcmbtWOKcle6dh;m#(s`HNuf0%Bykc<6PG%kc{BbMP#RQ3RRRbaK~)E^P>|J)j*3NP7Gj{Kqjr+;F#znnE-h zZ{i@p9Ry&X0h}tFNvg7%epSxpKaz2*^pU4#l4=D|@C!I0#1mp<>Brvju!sAdVYyX? zd&sSFzbti({J;&j+u49k&SX-_NjaZPt2u|y!Q$FC6SA87K*{F%A$&*ikBJ_y!+DeO z-t%m6N)UO2*LVU;{B;S#1WIl+<*U+qGvaDYqx5*kp0zE70v-%vl1D}N;!lEQ0 zgz5Wl$O_pYEBYtkvl&E^Bt^_88jT_(g3;j_IhhKgkq|->^_eGk^vn7MKUwrc9BoCv z0-*p#|D0@+E&XHowFYUF=Y~*(I$ehocuy9|f_{mt=~uv=ni^R_2H*-Wt8UaBp(u5| z26fyqNU#V1t0q9%R@)*Q2*nITBled5DFq|ze)!s?m4WZ=l9!R#Jk5RCXviuAzBRYo z4zqBS$$qJ0Fcz<}$D=_$rrA+3YVas&Bc%p&MjTg1jL%70X=?XWebLIY9b+Ah1?H#OV zX|twK@Fi$5G{)feb#2EyrIG}k1#v1jwK-3L{G}-5w#n&cRDJi~xq6|erQGPbqih3N zu|FJ5@B$^P5Q~SyRkFefaG0Jzqc4X+o=1Y`>OkGcL3c-tLQY^>-yQ1 zc8M;VbKQB>`CRM$)-PKz2;qZWn%wVXZ2RA0hOuK>QKnAHlV>J!=|Xl=A=jGqsHR0^ z`(-w%MK!dnlt=m?#J=a5ZtO_TWU}eA$(*6xR9G}CWhqP7h6UDfKVbEpU;@}Nlg=m4 zmVkC9Zl)qqhMCY>O`dP`QtWY>r}AM&$tD#Qy08Nto4_V$4d!4EVETMa2VhbeH-s_XeEf?BM$(mf8-7Y3Q^ChYsR0Z^45Zy2loP=K@GbVHEOyux(6(Q83Bmglo_#j^|8pFe_W&C zC@<^{yr6r~!YA?=xBx8%uzedXYWAvypS&CV3{7EcG)4RWqCz)Wd0&lnKnxI-0A?FR z#Zpt~W-*sQCFd&M@piy7goS-{2HvRdA`9qa9EBd&m_E#!WZWSaE9=n$cF+s327g#f z{#0vw(F2wNZHU>2+2#boScC@zhW-b&h{w0lc$7eF1A%IVrDjNy8(e@uQ22B~suz~Z z-wKORM-li9yvcC5rTFLhke095RQKq97fr{<_;5>Y`LHxCj1M`%7;ex2ZfV{CT7LPd nhnOs_^i~=$K$B{yU#xd9?4R8Q_E0?7Z@WEK)=Ll0cgp_&Ui*z` delta 153 zcmZoTz}#?vc>|*WF9QPuGru_l|62Y+e)G+O4x0Rv&E>6lte7|% sqlite3.Connection: + """Возвращает соединение с БД""" + return sqlite3.connect(self.db_path, check_same_thread=False) + + def get_studio_id_for_head(self, user_id: int) -> Optional[int]: + #Получает ID студии, за которой закреплен руководитель + try: + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT studio_id + FROM studios + WHERE head_user_id = ? + """, (user_id,)) + result = cursor.fetchone() + return result[0] if result else None + except Exception as e: + logger.error(f"Error getting studio for head {user_id}: {e}") + return None + + def get_new_applications(self, studio_id: int) -> List[Tuple[int, str, str, str]]: + #Получает ВСЕ новые заявки + #Возвращает:Список кортежей (application_id, summary, full_name, created_at) + + try: + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT + a.application_id, + a.summary, + u.full_name, + a.created_at, + u.username, + u.phone_number, + u.email + FROM applications a + JOIN users u ON a.user_id = u.user_id + WHERE a.studio_id = ? + AND a.status = 'pending' + ORDER BY a.created_at ASC + """, (studio_id,)) + + return cursor.fetchall() + except Exception as e: + logger.error(f"Error getting applications for studio {studio_id}: {e}") + return [] + + def get_application_details(self, application_id: int) -> Optional[Dict[str, Any]]: + #Получает детальную информацию о заявке по ее айди + try: + with self.get_connection() as conn: + cursor = conn.cursor() + + # Получаем основную информацию о заявке + cursor.execute(""" + SELECT + a.application_id, a.summary, a.status, a.created_at, + u.user_id, u.telegram_id, u.full_name, u.student_group, + u.phone_number, u.email, u.username, + s.name as studio_name + FROM applications a + JOIN users u ON a.user_id = u.user_id + JOIN studios s ON a.studio_id = s.studio_id + WHERE a.application_id = ? + """, (application_id,)) + + result = cursor.fetchone() + if not result: + return None + + # Получаем ответы на вопросы + cursor.execute(""" + SELECT question_text, answer_text + FROM application_answers + WHERE application_id = ? + ORDER BY answer_id + """, (application_id,)) + + answers = cursor.fetchall() + + # Формируем словарь с данными + app_data = { + 'application_id': result[0], + 'summary': result[1], + 'status': result[2], + 'created_at': result[3], + 'user_id': result[4], + 'user_telegram_id': result[5], + 'full_name': result[6], + 'student_group': result[7], + 'phone_number': result[8], + 'email': result[9], + 'username': result[10], + 'studio_name': result[11], + 'answers': dict(answers) if answers else {} + } + + # Сохраняем в кэш + self.applications_cache[application_id] = app_data + return app_data + + except Exception as e: + logger.error(f"Error getting application details {application_id}: {e}") + return None + + def format_application_message(self, app_data: Dict[str, Any]) -> str: + """Форматирует сообщение с информацией о заявке""" + if not app_data: + return "❌ Ошибка загрузки заявки" + + message = f"Заявка #{app_data['application_id']}\n" + message += f"Студент: {app_data['full_name']}\n" + message += f"Группа: {app_data['student_group'] or 'Не указана'}\n" + + if app_data.get('username'): + message += f"Telegram: @{app_data['username']}\n" + + if app_data.get('phone_number'): + message += f"Телефон: {app_data['phone_number']}\n" + + if app_data.get('email'): + message += f"Email: {app_data['email']}\n\n" + else: + message += "\n" + + # Добавляем ответы на вопросы + if app_data.get('answers'): + message += "Ответы на вопросы:\n" + for i, (question, answer) in enumerate(app_data['answers'].items(), 1): + message += f"{i}. {question}\n{answer}\n\n" + elif app_data.get('summary'): + message += "Сводка анкеты:\n" + # Преобразуем Markdown в HTML для лучшего отображения + summary = app_data['summary'].replace('*', '').replace('_', '') + message += f"{summary}\n\n" + + # Показываем прогресс + if 'user_id' in self.user_states: + user_id = None + for uid, state in self.user_states.items(): + if state.get('current_application_id') == app_data['application_id']: + user_id = uid + break + + if user_id: + state = self.user_states[user_id] + current_idx = state.get('current_index', 0) + 1 + total = len(state.get('applications', [])) + message += f"Прогресс: {current_idx} из {total}" + + return message + + def start_reviewing_applications(self, message): + """Начинает просмотр заявок руководителем""" + user_id = message.from_user.id + + # Получаем студию руководителя + studio_id = self.get_studio_id_for_head(user_id) + if not studio_id: + self.bot.send_message( + message.chat.id, + "❌ Вы не закреплены ни за одной студией.", + parse_mode='HTML' + ) + return + + # Получаем новые заявки + applications = self.get_new_applications(studio_id) + + if not applications: + self.bot.send_message( + message.chat.id, + "Нет новых заявок для рассмотрения.", + parse_mode='HTML' + ) + return + + # Получаем название студии для сообщения + studio_name = self.get_studio_name(studio_id) + + # Показываем информацию о количестве заявок + self.bot.send_message( + message.chat.id, + f"{studio_name}\n" + f"Найдено заявок: {len(applications)}\n\n", + parse_mode='HTML' + ) + + # Сохраняем состояние пользователя + self.user_states[user_id] = { + 'studio_id': studio_id, + 'applications': applications, + 'current_index': 0, + 'status': 'reviewing' + } + + # Показываем первую заявку + self.show_next_application(message.chat.id, user_id) + + def show_next_application(self, chat_id: int, user_id: int): + """Показывает следующую заявку в очереди""" + if user_id not in self.user_states: + return + + state = self.user_states[user_id] + applications = state['applications'] + current_index = state['current_index'] + + if current_index >= len(applications): + # Все заявки обработаны + self.bot.send_message( + chat_id, + "Все заявки рассмотрены!\n\n" + "Новых заявок в очереди нет.", + parse_mode='HTML' + ) + + # Очищаем состояние + del self.user_states[user_id] + return + + # Получаем текущую заявку + app_id, summary, full_name, created_at, username, phone, email = applications[current_index] + + # Получаем данные заявки + app_data = self.get_application_details(app_id) + + if not app_data: + # Если не удалось загрузить, пропускаем эту заявку + self.bot.send_message( + chat_id, + f"⚠️ Ошибка загрузки заявки #{app_id}.", + parse_mode='HTML' + ) + + # Увеличиваем индекс и показываем следующую + state['current_index'] += 1 + self.show_next_application(chat_id, user_id) + return + + # Сохраняем ID текущей заявки + state['current_application_id'] = app_id + + # Форматируем сообщение + message_text = self.format_application_message(app_data) + + markup = types.InlineKeyboardMarkup() + + markup.row( + types.InlineKeyboardButton("✅ Принять", callback_data=f"accept:{app_id}"), + types.InlineKeyboardButton("❌ Отклонить", callback_data=f"reject:{app_id}") + ) + + markup.row( + types.InlineKeyboardButton("⏭ Пропустить", callback_data=f"skip:{app_id}") + ) + + markup.row( + types.InlineKeyboardButton("⏹ Прекратить просмотр", callback_data="stop_review") + ) + + # Отправляем сообщение + self.bot.send_message( + chat_id, + message_text, + parse_mode='HTML', + reply_markup=markup + ) + + def handle_application_action(self, call): + + user_id = call.from_user.id + + if user_id not in self.user_states: + self.bot.answer_callback_query(call.id, "Сессия устарела") + return + + action_data = call.data + state = self.user_states[user_id] + + if action_data == "stop_review": + # Прекращаем просмотр + self.bot.answer_callback_query(call.id, "Просмотр прекращен") + + # Показываем сообщение о прекращении + self.bot.edit_message_text( + "Просмотр заявок прекращен\n\n" + "Вы можете возобновить в любой момент.", + call.message.chat.id, + call.message.message_id, + parse_mode='HTML' + ) + + # Очищаем состояние + del self.user_states[user_id] + return + + + # Извлекаем ID заявки и действие + parts = action_data.split(":") + if len(parts) != 2: + self.bot.answer_callback_query(call.id, "Ошибка") + return + + action, app_id_str = parts + app_id = int(app_id_str) + + # Проверяем, что это текущая заявка + if state.get('current_application_id') != app_id: + self.bot.answer_callback_query(call.id, "Заявка уже обработана") + return + + # Обрабатываем действие + if action == "accept": + self.accept_application(call, app_id) + elif action == "reject": + self.reject_application(call, app_id) + elif action == "skip": + self.skip_application(call, app_id) + + def accept_application(self, call, application_id: int): + """Принять заявку """ + try: + with self.get_connection() as conn: + cursor = conn.cursor() + + # Обновляем статус заявки + cursor.execute(""" + UPDATE applications + SET status = 'accepted', + processed_at = ? + WHERE application_id = ? + """, (datetime.now(), application_id)) + + conn.commit() + + # Получаем данные студента для уведомления + cursor.execute(""" + SELECT u.telegram_id, s.name, u.full_name + FROM applications a + JOIN users u ON a.user_id = u.user_id + JOIN studios s ON a.studio_id = s.studio_id + WHERE a.application_id = ? + """, (application_id,)) + + result = cursor.fetchone() + if result: + student_telegram_id, studio_name, student_name = result + # Отправляем уведомление студенту + try: + self.bot.send_message( + student_telegram_id, + f"🎉 Поздравляем, {student_name}!\n\n" + f"Ваша заявка в студию '{studio_name}' одобрена!\n\n", + parse_mode='HTML' + ) + except Exception as e: + logger.error(f"Error notifying student {student_telegram_id}: {e}") + + self.bot.answer_callback_query(call.id, "✅ Заявка принята") + + # Удаляем заявку из состояния пользователя + self.remove_application_from_state(call.from_user.id, application_id) + + # Удаляем сообщение с заявкой + self.bot.delete_message(call.message.chat.id, call.message.message_id) + + # Показываем следующую заявку (если есть) + self.show_next_application(call.message.chat.id, call.from_user.id) + + except Exception as e: + logger.error(f"Error accepting application {application_id}: {e}") + self.bot.answer_callback_query(call.id, "❌ Ошибка при принятии") + + def reject_application(self, call, application_id: int): + """ Отклонить заявку""" + try: + with self.get_connection() as conn: + cursor = conn.cursor() + + # Обновляем статус заявки + cursor.execute(""" + UPDATE applications + SET status = 'rejected', + processed_at = ? + WHERE application_id = ? + """, (datetime.now(), application_id)) + + conn.commit() + + # Получаем данные студента для уведомления + cursor.execute(""" + SELECT u.telegram_id, s.name, u.full_name + FROM applications a + JOIN users u ON a.user_id = u.user_id + JOIN studios s ON a.studio_id = s.studio_id + WHERE a.application_id = ? + """, (application_id,)) + + result = cursor.fetchone() + if result: + student_telegram_id, studio_name, student_name = result + try: + self.bot.send_message( + student_telegram_id, + f"Уважаемый(ая) {student_name}!\n\n" + f"К сожалению, ваша заявка в студию '{studio_name}' не может быть принята.\n\n" + f"Не расстраивайтесь! Вы можете подать заявку в другие студии " + f"или попробовать снова в следующем семестре.", + parse_mode='HTML' + ) + except Exception as e: + logger.error(f"Error notifying student {student_telegram_id}: {e}") + + self.bot.answer_callback_query(call.id, "❌ Заявка отклонена") + + # Удаляем заявку из состояния пользователя + self.remove_application_from_state(call.from_user.id, application_id) + + # Удаляем сообщение с заявкой + self.bot.delete_message(call.message.chat.id, call.message.message_id) + + # Показываем следующую заявку + self.show_next_application(call.message.chat.id, call.from_user.id) + + except Exception as e: + logger.error(f"Error rejecting application {application_id}: {e}") + self.bot.answer_callback_query(call.id, "❌ Ошибка при отклонении") + + + def skip_application(self, call, application_id: int): + """Пропустить заявку (оставить в очереди)""" + # Перемещаем заявку в конец очереди + self.move_application_to_end(call.from_user.id, application_id) + + self.bot.answer_callback_query(call.id, "Заявка перемещена в конец очереди") + + # Удаляем текущее сообщение и показываем следующую заявку + self.bot.delete_message(call.message.chat.id, call.message.message_id) + self.show_next_application(call.message.chat.id, call.from_user.id) + + def remove_application_from_state(self, user_id: int, application_id: int): + """Удаляет заявку из состояния пользователя""" + if user_id not in self.user_states: + return + + state = self.user_states[user_id] + applications = state['applications'] + + # Удаляем заявку из списка + new_applications = [ + app for app in applications + if app[0] != application_id + ] + + # Обновляем список заявок + state['applications'] = new_applications + + # Если текущая заявка была удалена, корректируем индекс + if state.get('current_index') >= len(new_applications): + state['current_index'] = max(0, len(new_applications) - 1) + + def move_application_to_end(self, user_id: int, application_id: int): + """Перемещает заявку в конец очереди """ + if user_id not in self.user_states: + return + + state = self.user_states[user_id] + applications = state['applications'] + + # Находим заявку + target_application = None + new_applications = [] + + for app in applications: + if app[0] == application_id: + target_application = app + else: + new_applications.append(app) + + # Если нашли заявку, добавляем ее в конец + if target_application: + new_applications.append(target_application) + + # Обновляем список заявок + state['applications'] = new_applications + + # Корректируем текущий индекс + # Текущая заявка будет удалена из сообщения, + # поэтому показываем следующую по порядку + # Индекс не меняем, так как список сдвинулся + if state.get('current_index') >= len(new_applications): + state['current_index'] = len(new_applications) - 1 + + def get_studio_name(self, studio_id: int) -> str: + """Получает название студии по ID""" + try: + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT name FROM studios WHERE studio_id = ?", + (studio_id,) + ) + result = cursor.fetchone() + return result[0] if result else "Неизвестная студия" + except Exception as e: + logger.error(f"Error getting studio name: {e}") + return "Неизвестная студия" + + def get_statistics(self, studio_id: int) -> Dict[str, Any]: + """Получает отчет по заявкам студии""" + try: + with self.get_connection() as conn: + cursor = conn.cursor() + + cursor.execute(""" + SELECT + COUNT(*) as total, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, + SUM(CASE WHEN status = 'accepted' THEN 1 ELSE 0 END) as accepted, + SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected + FROM applications + WHERE studio_id = ? + """, (studio_id,)) + + stats = cursor.fetchone() + + # Статистика по дням (последние 7 дней) + cursor.execute(""" + SELECT + DATE(created_at) as date, + COUNT(*) as count + FROM applications + WHERE studio_id = ? + AND created_at >= DATE('now', '-7 days') + GROUP BY DATE(created_at) + ORDER BY date DESC + """, (studio_id,)) + + daily_stats = cursor.fetchall() + + + + except Exception as e: + logger.error(f"Error getting statistics for studio {studio_id}: {e}") + return {} + + def format_statistics_message(self, stats: Dict[str, Any], studio_name: str) -> str: + """Форматирует сообщение с отчетом""" + if not stats: + return "❌ Не удалось получить отчет" + + message = f"отчет по заявкам студии '{studio_name}'\n\n" + + message += f"Общая статистика:\n" + message += f"📋 Всего заявок: {stats.get('total', 0)}\n" + message += f"⏳ Ожидают рассмотрения: {stats.get('pending', 0)}\n" + message += f"✅ Принято: {stats.get('accepted', 0)}\n" + message += f"❌ Отклонено: {stats.get('rejected', 0)}\n\n" + + if stats.get('daily'): + message += "За последние 7 дней:\n" + for date, count in stats['daily'][:7]: + message += f"{date}: {count} заявок\n" + message += "\n" + + return message \ No newline at end of file diff --git a/.venv/user (2).py b/.venv/user (2).py deleted file mode 100644 index d272a9a..0000000 --- a/.venv/user (2).py +++ /dev/null @@ -1,563 +0,0 @@ -import telebot -from telebot import types -import sqlite3 -from datetime import datetime -from typing import Dict, Any, List, Tuple -import logging -import re - -from users_collector import QuestionnaireBuilder -from вопросы import get_question_text, get_question_obj - -# Настройка логирования -logging.basicConfig( - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - level=logging.INFO -) -logger = logging.getLogger(__name__) - -bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") -DB_PATH = "student_studios_bot (1).db" - -def get_connection() -> sqlite3.Connection: - """Возвращает соединение с БД""" - return sqlite3.connect(DB_PATH, check_same_thread=False) - -def load_active_studios() -> List[Tuple[int, str]]: - """Загрузка списка активных студий""" - try: - with get_connection() as conn: - cursor = conn.cursor() - cursor.execute("SELECT studio_id, name FROM studios WHERE is_active = 1 ORDER BY studio_id") - return cursor.fetchall() - except Exception as e: - logger.error(f"Error loading studios: {e}") - return [] - - -#===========ОПРЕДЕЛЕНИЕ РОЛИ ПО БД========================== -def get_user_role(username: str) -> str: - """Получение роли пользователя из базы данных""" - if not username: - return 'user' - - try: - with get_connection() as conn: - cursor = conn.cursor() - # Ищем username с @ (как хранится в БД) - cursor.execute("SELECT role FROM users WHERE username = ?", (f"@{username}",)) - result = cursor.fetchone() - return result[0] if result else 'user' - except Exception as e: - logger.error(f"Database error getting role: {e}") - return 'user' - - -@bot.message_handler(commands=['start']) -def start(message): - """Главный обработчик команды /start с распределением по ролям""" - username = message.from_user.username - - # Получаем роль из базы данных - role = get_user_role(username) - - # Распределение по ролям - if role == 'admin': - try: - from Admin import admin_start - admin_start(message) - return - except ImportError as e: - logger.error(f"Error importing Admin: {e}") - bot.send_message(message.chat.id, "Модуль администратора недоступен") - except Exception as e: - logger.error(f"Error in admin_start: {e}") - bot.send_message(message.chat.id, "Ошибка в модуле администратора") - - elif role == 'head': - try: - from Head_of_studio import head_start - head_start( message) - return - except ImportError as e: - logger.error(f"Error importing Head_of_studio: {e}") - bot.send_message(message.chat.id, "Модуль руководителя недоступен") - except Exception as e: - logger.error(f"Error in head_start: {e}") - bot.send_message(message.chat.id, "Ошибка в модуле руководителя") - - # Для user или если пользователя нет в базе - user_start(message) - - -#==============ФУНКЦИОНАЛ СТАНДАРТНОГО ПОЛЬЗОВАТЕЛЯ========================= -def user_start(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn1 = types.KeyboardButton('Каталог студий') - markup.row(btn1) - btn2 = types.KeyboardButton('Мои заявки') - btn3 = types.KeyboardButton('Подать заявку') - markup.row(btn2, btn3) - - bot.send_message( - message.chat.id, - f'Привет, в этом боте ты можешь узнать про творческие студии' - f' ГУАП и стать частью активной студенческой жизни!', - parse_mode='html', - reply_markup=markup - ) - -# ВЕТКА КАТАЛОГ СТУДИЙ - -@bot.message_handler(func=lambda message: message.text == 'Каталог студий') -def inline_button_catalog(message): - studios = load_active_studios() - - if not studios: - bot.send_message(message.chat.id, 'На данный момент активных студий нет.') - return - - markup = types.InlineKeyboardMarkup() - for studio_id, name in studios: - btn = types.InlineKeyboardButton(name, callback_data=f'info:{studio_id}') - markup.row(btn) - - bot.send_message(message.chat.id, 'Нажми на студию для просмотра:', reply_markup=markup) - -@bot.callback_query_handler(func=lambda call: call.data.startswith('info:')) -def show_studio_info(call): - """Показывает информацию о студии""" - studio_id = int(call.data.split(':')[1]) - - try: - with get_connection() as conn: - cursor = conn.cursor() - cursor.execute(""" - SELECT name, description, contacts - FROM studios - WHERE studio_id = ? - """, (studio_id,)) - result = cursor.fetchone() - - if result: - name, description, contacts = result - text = f"{name}\n\n{description or 'Описание скоро появится.'}\n\nКонтакты: {contacts or '—'}" - - # Добавляем кнопку подачи заявки - markup = types.InlineKeyboardMarkup() - btn_apply = types.InlineKeyboardButton("📝 Подать заявку", callback_data=f'apply:{studio_id}') - markup.row(btn_apply) - - bot.edit_message_text( - text, - call.message.chat.id, - call.message.message_id, - parse_mode='html', - reply_markup=markup - ) - else: - bot.answer_callback_query(call.id, "Студия не найдена") - except Exception as e: - logger.error(f"Error showing studio info: {e}") - bot.answer_callback_query(call.id, "Ошибка при загрузке информации") - -# Хранение состояний пользователей -user_states = {} - -class QuestionnaireHandler: - """Обработчик анкет с автозаполнением""" - - def __init__(self, db_connection): - self.db = db_connection - - def start_questionnaire(self, call): - """Начало анкеты для выбранной студии""" - studio_id = int(call.data.split(':')[1]) - user_id = call.from_user.id - - # Проверяем, существует ли такая студия в конфигурации - studio_config = QuestionnaireBuilder.get_questionnaire_for_studio(studio_id) - if not studio_config: - bot.answer_callback_query(call.id, f"Анкета для этой студии не настроена") - return - - # Сохраняем данные в состоянии пользователя - user_states[user_id] = { - 'state': 'questionnaire', - 'studio_id': studio_id, - 'studio_name': studio_config.get('name', 'Студия'), - 'current_question_index': 0, - 'answers': {}, - 'questionnaire': studio_config # Сохраняем конфигурацию студии - } - - # Получаем данные пользователя для автозаполнения - user_data = self.get_user_data(user_id) - user_states[user_id]['user_data'] = user_data - - # Проверяем возможность автозаполнения - if QuestionnaireBuilder.can_autofill(studio_id, user_data): - self.offer_autofill(call, studio_id, user_data) - else: - self.start_manual_filling(call, studio_id) - - def get_user_data(self, user_id: int) -> Dict[str, Any]: - """Получает данные пользователя из БД для автозаполнения""" - try: - conn = get_connection() - cursor = conn.cursor() - cursor.execute(""" - SELECT full_name, student_group, phone, email - FROM users - WHERE telegram_id = ? - """, (user_id,)) - - result = cursor.fetchone() - conn.close() - - if result: - return { - 'full_name': result[0] or '', - 'student_group': result[1] or '', - 'phone': result[2] or '', - 'email': result[3] or '' - } - except Exception as e: - logger.error(f"Error getting user data: {e}") - - return {} - - def offer_autofill(self, call, studio_id: int, user_data: Dict[str, Any]): - """Предлагает автозаполнение""" - # Получаем поля для автозаполнения - autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) - autofill_info = [] - - for field in autofill_fields: - value = user_data.get(field, '') - if value: - field_name = { - 'full_name': 'ФИО', - 'student_group': 'Группа', - 'phone': 'Телефон', - 'email': 'Email' - }.get(field, field) - autofill_info.append(f"{field_name}: {value}") - - studio_name = QuestionnaireBuilder.get_studio_name(studio_id) - message = f"*Анкета для студии: {studio_name}*\n\n" - - if autofill_info: - message += "Найдены ваши данные из предыдущих анкет:\n" - for info in autofill_info: - message += f"• {info}\n" - message += "\nХотите использовать их для автозаполнения?" - else: - message += "Хотите использовать основную информацию из предыдущих анкет для автозаполнения?" - - markup = types.InlineKeyboardMarkup() - markup.row( - types.InlineKeyboardButton("✅ Использовать мои данные", callback_data='use_autofill'), - types.InlineKeyboardButton("✏️ Заполнить вручную", callback_data='manual_fill') - ) - - bot.edit_message_text( - message, - call.message.chat.id, - call.message.message_id, - parse_mode='Markdown', - reply_markup=markup - ) - - def start_manual_filling(self, call, studio_id: int): - """Начинает ручное заполнение анкеты""" - user_id = call.from_user.id - studio_name = QuestionnaireBuilder.get_studio_name(studio_id) - - # Обновляем состояние - user_states[user_id]['state'] = 'answering' - - bot.edit_message_text( - f"Начинаем заполнение анкеты для студии *{studio_name}*.\n\n" - f"Пожалуйста, ответьте на несколько вопросов:", - call.message.chat.id, - call.message.message_id, - parse_mode='Markdown' - ) - - # Даем небольшую задержку перед первым вопросом - import time - time.sleep(1) - - # Задаем первый вопрос - self.ask_next_question(call.message.chat.id, user_id) - - def ask_next_question(self, chat_id: int, user_id: int): - """Задает следующий вопрос""" - if user_id not in user_states: - return - - state = user_states[user_id] - studio_id = state['studio_id'] - question_ids = QuestionnaireBuilder.get_question_sequence(studio_id) - current_index = state.get('current_question_index', 0) - answers = state.get('answers', {}) - - # Пропускаем уже заполненные вопросы - while current_index < len(question_ids): - qid = question_ids[current_index] - if qid not in answers: - break - current_index += 1 - - # Проверяем, не заполнили ли мы все вопросы - if current_index >= len(question_ids): - self.show_summary(chat_id, user_id) - return - - # Сохраняем текущий индекс - user_states[user_id]['current_question_index'] = current_index - user_states[user_id]['current_question_id'] = qid - - # Получаем текст вопроса - question_text = get_question_text(qid) - - # Отправляем вопрос - bot.send_message(chat_id, question_text) - - def process_answer(self, message): - """Обработка ответа на вопрос""" - user_id = message.from_user.id - - if user_id not in user_states: - return - - state = user_states[user_id] - if state.get('state') != 'answering': - return - - current_qid = state.get('current_question_id') - answer_text = message.text.strip() - - if not current_qid: - return - - # Получаем объект вопроса для валидации - q_obj = get_question_obj(current_qid) - - if q_obj: - is_valid, error_msg = q_obj.validate(answer_text) - if not is_valid: - bot.send_message(message.chat.id, f"❌ {error_msg}\n\nПопробуйте еще раз:") - return - - # Сохраняем ответ - user_states[user_id]['answers'][current_qid] = answer_text - - # Переходим к следующему вопросу - user_states[user_id]['current_question_index'] += 1 - - # Задаем следующий вопрос - self.ask_next_question(message.chat.id, user_id) - - def show_summary(self, chat_id: int, user_id: int): - """Показывает сводку по ответам""" - if user_id not in user_states: - return - - state = user_states[user_id] - studio_id = state['studio_id'] - answers = state.get('answers', {}) - - # Валидируем ответы - is_valid, errors = QuestionnaireBuilder.validate_answers(studio_id, answers) - - if not is_valid: - error_text = "❌ *Ошибки в заполнении:*\n\n" - for qid, error in errors.items(): - q_obj = get_question_obj(qid) - error_text += f"• {q_obj.text if q_obj else qid}: {error}\n" - - error_text += "\nПожалуйста, заполните анкету заново." - - bot.send_message(chat_id, error_text, parse_mode='Markdown') - - # Сбрасываем анкету - state['current_question_index'] = 0 - state['answers'] = {} - - # Запускаем заново - self.ask_next_question(chat_id, user_id) - return - - # Генерируем сводку - summary = QuestionnaireBuilder.generate_summary(studio_id, answers) - - # Создаем клавиатуру - markup = types.InlineKeyboardMarkup() - markup.row( - types.InlineKeyboardButton("✅ Подтвердить и отправить", callback_data='confirm_application'), - types.InlineKeyboardButton("✏️ Заполнить заново", callback_data='restart_questionnaire') - ) - markup.row( - types.InlineKeyboardButton("🚫 Отменить", callback_data='cancel_to_catalog') - ) - - bot.send_message( - chat_id, - f"{summary}\n\n*Всё верно? Подтвердите отправку заявки или внесите изменения.*", - parse_mode='Markdown', - reply_markup=markup - ) - - # Обновляем состояние - user_states[user_id]['state'] = 'confirmation' - - -# Инициализируем обработчик -questionnaire_handler = QuestionnaireHandler(None) - -# Обработчики для анкет -@bot.callback_query_handler(func=lambda call: call.data.startswith('apply:')) -def handle_apply(call): - """Обработка нажатия кнопки 'Подать заявку'""" - questionnaire_handler.start_questionnaire(call) - -@bot.callback_query_handler(func=lambda call: call.data in ['use_autofill', 'manual_fill']) -def handle_autofill_choice(call): - """Обработка выбора автозаполнения""" - user_id = call.from_user.id - studio_id = user_states[user_id]['studio_id'] - - if call.data == 'use_autofill': - # Автозаполняем данные - user_data = user_states[user_id]['user_data'] - autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) - - for field in autofill_fields: - value = user_data.get(field) - if value: - # Находим ID вопроса, соответствующего этому полю - questions = QuestionnaireBuilder.get_question_objects(studio_id) - for q in questions: - if hasattr(q, 'autofill_field') and q.autofill_field == field: - user_states[user_id]['answers'][q.id] = value - break - - bot.edit_message_text( - "✅ Данные автозаполнены.\nПродолжаем с оставшимися вопросами...", - call.message.chat.id, - call.message.message_id - ) - - # Начинаем опрос - user_states[user_id]['state'] = 'answering' - questionnaire_handler.ask_next_question(call.message.chat.id, user_id) - -# Обработка текстовых сообщений (ответов на вопросы) -@bot.message_handler(func=lambda message: True, content_types=['text']) -def handle_all_text_messages(message): - """Обработка всех текстовых сообщений""" - user_id = message.from_user.id - - # Проверяем, находится ли пользователь в процессе заполнения анкеты - if user_id in user_states and user_states[user_id].get('state') == 'answering': - questionnaire_handler.process_answer(message) - return - - # Обработка других команд - if message.text == 'Мои заявки': - bot.send_message(message.chat.id, "Функция 'Мои заявки' в разработке") - elif message.text == 'Подать заявку': - inline_button_catalog(message) - else: - bot.send_message(message.chat.id, "Используйте кнопки меню или команду /start") - -# Обработка действий подтверждения -@bot.callback_query_handler(func=lambda call: call.data in [ - 'confirm_application', 'restart_questionnaire', 'cancel_to_catalog' -]) - -def save_application_to_db(user_id: int, studio_id: int, answers: dict): - """Сохраняет заявку в БД""" - try: - with get_connection() as conn: - cursor = conn.cursor() - - # 1. Сохраняем основную информацию о заявке - cursor.execute(""" - INSERT INTO applications (user_id, studio_id, status, created_at) - VALUES (?, ?, ?, ?) - """, (user_id, studio_id, 'pending', datetime.now())) - - application_id = cursor.lastrowid - - # 2. Сохраняем ответы на каждый вопрос - for question_id, answer in answers.items(): - cursor.execute(""" - INSERT INTO application_answers - (application_id, question_id, answer_text) - VALUES (?, ?, ?) - """, (application_id, question_id, answer)) - - conn.commit() - return application_id - - except Exception as e: - logger.error(f"Error saving application: {e}") - return None - -def handle_confirmation_actions(call): - """Обработка действий подтверждения""" - user_id = call.from_user.id - - if user_id not in user_states: - bot.answer_callback_query(call.id, "Сессия устарела") - return - - state = user_states[user_id] - studio_id = state['studio_id'] - answers = state.get('answers', {}) - - if call.data == 'confirm_application': - # СОХРАНЯЕМ В БД - application_id = save_application_to_db(user_id, studio_id, answers) - - if application_id: - bot.answer_callback_query(call.id, "✅ Заявка отправлена") - - markup = types.InlineKeyboardMarkup() - markup.row(types.InlineKeyboardButton("🏠 Главное меню", callback_data='go_to_main_menu')) - - bot.edit_message_text( - "✅ Ваша заявка отправлена успешно!", - call.message.chat.id, - call.message.message_id, - reply_markup=markup - ) - else: - bot.answer_callback_query(call.id, "❌ Ошибка при сохранении") - - bot.edit_message_text( - "❌ Произошла ошибка при сохранении заявки. Попробуйте позже.", - call.message.chat.id, - call.message.message_id - ) - - # Очищаем состояние - del user_states[user_id] - - elif call.data == 'restart_questionnaire': - # Отменяем и возвращаем в каталог - if user_id in user_states: - del user_states[user_id] - - inline_button_catalog(call.message) - -# Запуск бота -if __name__ == '__main__': - print("Бот запущен...") - bot.polling(none_stop=True) - - - diff --git a/.venv/user (3).py b/.venv/user.py similarity index 86% rename from .venv/user (3).py rename to .venv/user.py index 87c5b66..b59201c 100644 --- a/.venv/user (3).py +++ b/.venv/user.py @@ -6,8 +6,9 @@ import logging import re +from Admin import initialize as init_admin, admin_start, handle_admin_message from users_collector import QuestionnaireBuilder -from вопросы import get_question_text, get_question_obj +from questions import get_question_handler, get_question_obj # Настройка логирования logging.basicConfig( @@ -18,6 +19,8 @@ bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") DB_PATH = "student_studios_bot (1).db" +#Инициализация модуля для функционала админа +init_admin(bot) def get_connection() -> sqlite3.Connection: """Возвращает соединение с БД""" @@ -34,8 +37,64 @@ def load_active_studios() -> List[Tuple[int, str]]: logger.error(f"Error loading studios: {e}") return [] +#===========ОПРЕДЕЛЕНИЕ РОЛИ ПО БД========================== +def get_user_role(username: str) -> str: + #Получение роли пользователя из базы данных + if not username: + return 'student' + + try: + with get_connection() as conn: + cursor = conn.cursor() + # Ищем username с @ (как хранится в БД) + cursor.execute("SELECT role FROM users WHERE username = ?", (f"{username}",)) + result = cursor.fetchone() + return result[0] if result else 'student' + except Exception as e: + logger.error(f"Database error getting role: {e}") + return 'student' + + @bot.message_handler(commands=['start']) def start(message): + """Главный обработчик команды /start с распределением по ролям""" + username = message.from_user.username + + # Получаем роль из базы данных + role = get_user_role(username) + print(username) + print("Role:",role) + # Распределение по ролям + if role == 'admin': + try: + from Admin import admin_start + admin_start(message) + return + except ImportError as e: + logger.error(f"Error importing Admin: {e}") + bot.send_message(message.chat.id, "Модуль администратора недоступен") + except Exception as e: + logger.error(f"Error in admin_start: {e}") + bot.send_message(message.chat.id, "Ошибка в модуле администратора") + + elif role == 'head': + try: + from Head_studio import head_start + head_start( message) + return + except ImportError as e: + logger.error(f"Error importing Head_of_studio: {e}") + bot.send_message(message.chat.id, "Модуль руководителя недоступен") + except Exception as e: + logger.error(f"Error in head_start: {e}") + bot.send_message(message.chat.id, "Ошибка в модуле руководителя") + + # Для user или если пользователя нет в базе + user_start(message) + + +#==============ФУНКЦИОНАЛ СТАНДАРТНОГО ПОЛЬЗОВАТЕЛЯ========================= +def user_start(message): markup = types.ReplyKeyboardMarkup(resize_keyboard=True) btn1 = types.KeyboardButton('Каталог студий') markup.row(btn1) @@ -85,7 +144,7 @@ def show_studio_info(call): if result: name, description, contacts = result - text = f"{name}\n\n{description or 'Описание скоро появится.'}\n\nКонтакты: {contacts or '—'}" + text = f"{name}\n\n{description or 'Описание скоро появится.'}" # Добавляем кнопку подачи заявки markup = types.InlineKeyboardMarkup() @@ -94,6 +153,7 @@ def show_studio_info(call): bot.edit_message_text( text, + call.message.chat.id, call.message.message_id, parse_mode='html', @@ -148,7 +208,7 @@ def start_questionnaire(self, call): def get_user_data(self, user_id: int) -> Dict[str, Any]: """Получает данные пользователя из БД для автозаполнения""" try: - with get_connection() as conn: # Открывается соединение + with get_connection() as conn: cursor = conn.cursor() cursor.execute(""" SELECT full_name, student_group, phone_number, email @@ -192,7 +252,7 @@ def offer_autofill(self, call, studio_id: int, user_data: Dict[str, Any]): message += f"• {info}\n" message += "\nХотите использовать их для автозаполнения?" else: - message += "Хотите использовать основную информацию из предыдущих анкет для автозаполнения?" + message += "Хотите использовать основную информацию из предыдущих анкет для автозаполнения?"#### markup = types.InlineKeyboardMarkup() markup.row( @@ -224,10 +284,6 @@ def start_manual_filling(self, call, studio_id: int): parse_mode='Markdown' ) - # Даем небольшую задержку перед первым вопросом - import time - time.sleep(1) - # Задаем первый вопрос self.ask_next_question(call.message.chat.id, user_id) @@ -259,7 +315,8 @@ def ask_next_question(self, chat_id: int, user_id: int): user_states[user_id]['current_question_id'] = qid # Получаем текст вопроса - question_text = get_question_text(qid) + print(qid) + question_text = get_question_handler(qid) # Отправляем вопрос bot.send_message(chat_id, question_text) @@ -392,69 +449,6 @@ def handle_autofill_choice(call): user_states[user_id]['state'] = 'answering' questionnaire_handler.ask_next_question(call.message.chat.id, user_id) - def save_application_to_db(telegram_id: int, studio_id: int, answers: dict, user_first_name: str = None, - user_last_name: str = None): - """Сохраняет заявку в БД""" - try: - with get_connection() as conn: - cursor = conn.cursor() - - # 1. Получаем или создаем пользователя - cursor.execute("SELECT user_id FROM users WHERE telegram_id = ?", (telegram_id,)) - user_result = cursor.fetchone() - - if user_result: - user_id = user_result[0] - else: - # Получаем ФИО из ответов - full_name = answers.get('full_name') - - # Если нет в ответах, используем имя из Telegram - if not full_name and user_first_name: - full_name = f"{user_first_name or ''} {user_last_name or ''}".strip() - if not full_name: - full_name = "Не указано" - elif not full_name: - full_name = "Не указано" - - # Создаем нового пользователя - cursor.execute(""" - INSERT INTO users (telegram_id, full_name, created_at) - VALUES (?, ?, ?) - """, (telegram_id, full_name, datetime.now())) - user_id = cursor.lastrowid - - # 2. Генерируем сводку - summary = QuestionnaireBuilder.generate_summary(studio_id, answers) - - # 3. Сохраняем заявку - cursor.execute(""" - INSERT INTO applications (user_id, studio_id, summary_text, status, created_at) - VALUES (?, ?, ?, ?, ?) - """, (user_id, studio_id, summary, 'pending', datetime.now())) - - application_id = cursor.lastrowid - - # 4. Сохраняем ответы на каждый вопрос (опционально) - for question_id, answer in answers.items(): - q_obj = get_question_obj(question_id) - question_text = q_obj.text if q_obj else question_id - - cursor.execute(""" - INSERT INTO application_answers - (application_id, question_id, question_text, answer_text) - VALUES (?, ?, ?, ?) - """, (application_id, question_id, question_text, answer)) - - conn.commit() - return application_id - - except Exception as e: - logger.error(f"Error saving application: {e}") - return None - -# ... предыдущий код до функции handle_autofill_choice ... - @bot.callback_query_handler(func=lambda call: call.data in ['use_autofill', 'manual_fill']) def handle_autofill_choice(call): """Обработка выбора автозаполнения""" @@ -486,7 +480,6 @@ def handle_autofill_choice(call): user_states[user_id]['state'] = 'answering' questionnaire_handler.ask_next_question(call.message.chat.id, user_id) -# ========== ВЫНЕСИТЕ ЭТИ ФУНКЦИИ НА УРОВЕНЬ ДРУГИХ ФУНКЦИЙ ========== def save_application_to_db(telegram_id: int, studio_id: int, answers: dict, user_first_name: str = None, user_last_name: str = None): @@ -565,7 +558,6 @@ def handle_confirmation_actions(call): answers = state.get('answers', {}) if call.data == 'confirm_application': - # СОХРАНЯЕМ В БД с передачей имени пользователя application_id = save_application_to_db( telegram_id=user_id, studio_id=studio_id, @@ -599,7 +591,6 @@ def handle_confirmation_actions(call): del user_states[user_id] elif call.data == 'restart_questionnaire': - # Начинаем заново state['current_question_index'] = 0 state['answers'] = {} state['state'] = 'answering' @@ -616,7 +607,6 @@ def handle_confirmation_actions(call): questionnaire_handler.ask_next_question(call.message.chat.id, user_id) elif call.data == 'cancel_to_catalog': - # Отменяем и возвращаем в каталог if user_id in user_states: del user_states[user_id] @@ -642,7 +632,6 @@ def handle_confirmation_actions(call): ) elif call.data == 'go_to_main_menu': - # Возвращаем в главное меню if user_id in user_states: del user_states[user_id] @@ -674,12 +663,16 @@ def handle_confirmation_actions(call): def handle_all_text_messages(message): """Обработка всех текстовых сообщений""" user_id = message.from_user.id - + username = message.from_user.username # Проверяем, находится ли пользователь в процессе заполнения анкеты if user_id in user_states and user_states[user_id].get('state') == 'answering': questionnaire_handler.process_answer(message) return + if get_user_role(username) == "admin": + from Admin import handle_admin_message + handle_admin_message(message) + # Обработка других команд if message.text == 'Мои заявки': bot.send_message(message.chat.id, "Функция 'Мои заявки' в разработке") @@ -688,6 +681,8 @@ def handle_all_text_messages(message): else: bot.send_message(message.chat.id, "Используйте кнопки меню или команду /start") + + # Запуск бота if __name__ == '__main__': print("Бот запущен...") diff --git a/.venv/users_collector.py b/.venv/users_collector.py index d077ddc..928f8dd 100644 --- a/.venv/users_collector.py +++ b/.venv/users_collector.py @@ -6,46 +6,42 @@ class QuestionnaireBuilder: STUDIO_QUESTIONNAIRES = { # ID студии: [список ID вопросов из ALL_QUESTIONS] - 1: { # Танцевальная студия - 'name': 'Танцевальная студия', + 1: { 'name': 'Танцевальная студия', 'questions': [ 'full_name', # 1. ФИО 'group', # 2. Группа 'phone_number', # 3. Телефон 'email', # 4. Email - 'dance_style', # 5. Стиль танца + 'project_ideas', # 5. Стиль танца 'experience', # 6. Опыт 'motivation', # 7. Мотивация 'activeness', # 8. Участие в мероприятиях ] }, - 6: { # КВН - 'name': 'КВН', + 6: {'name': 'КВН', 'questions': [ 'full_name', # 1. ФИО 'group', # 2. Группа 'phone_number', # 3. Телефон 'email', # 4. Email - 'experience', # 5. Опыт - 'why_choose_us', # 6. Почему выбрали нас - 'time_commitment',# 7. Временные возможности + 'experience', + 'quot', + 'public_speakin', ] }, - 2: { # Театральная студия - 'name': 'Театральная студия', + 2: {'name': 'Театральная студия', 'questions': [ 'full_name', # 1. ФИО 'group', # 2. Группа 'phone_number', # 3. Телефон - 'email', # 4. Email - 'theater_role', # 5. Театральные роли + 'email', # 5. Театральные роли 'experience', # 6. Опыт 'motivation', # 7. Мотивация - 'activeness', # 8. Участие в мероприятиях + 'improvisation', + 'feedback_source', ] }, - 4: { # МУЗГУАП - 'name': 'МУЗГУАП', + 4: {'name': 'МУЗГУАП', 'questions': [ 'full_name', # 1. ФИО 'group', # 2. Группа @@ -53,58 +49,56 @@ class QuestionnaireBuilder: 'email', # 4. Email 'music_instrument', # 5. Музыкальный инструмент 'singing_experience', # 6. Опыт пения - 'expectations', # 7. Ожидания - 'activeness', # 8. Участие в мероприятиях + 'public_speakin', + 'activeness', ] }, - 5: { # Медиацентр - 'name': 'Медиацентр', + 5: {'name': 'Медиацентр', 'questions': [ 'full_name', # 1. ФИО 'group', # 2. Группа 'phone_number', # 3. Телефон 'email', # 4. Email - 'tech_skills', # 5. Технические навыки - 'experience', # 6. Опыт - 'project_ideas', # 7. Идеи для проектов - 'time_commitment',# 8. Временные возможности + 'media', + 'experience', + 'project_ideas', + 'time_commitment', ] }, - 3: { # Техничка - 'name': 'Студия технического обеспечения мероприятий', + 3: {'name': 'Студия технического обеспечения мероприятий', 'questions': [ 'full_name', # 1. ФИО 'group', # 2. Группа 'phone_number', # 3. Телефон 'email', # 4. Email 'tech_skills', # 5. Технические навыки - # 'feedback_source',# 8. Откуда узнали + 'organizational_exp', + 'leadership', + 'feedback_source', ] }, - 7: { # Студия организации мероприятий - 'name': 'Студия организации мероприятий', + 7: {'name': 'Студия организации мероприятий', 'questions': [ 'full_name', # 1. ФИО 'group', # 2. Группа 'phone_number', # 3. Телефон 'email', # 4. Email - 'organizational_exp', # 5. Организационный опыт - 'motivation', # 6. Мотивация - 'activeness', # 7. Участие в мероприятиях - 'tech_skills', # 8. Технические навыки + 'organizational_exp', + 'motivation', + 'activeness', + 'tech_skills', ] }, - 8: { # студия ведущих - 'name': 'Студия ведущих', + 8: {'name': 'Студия ведущих', 'questions': [ 'full_name', # 1. ФИО 'group', # 2. Группа 'phone_number', # 3. Телефон 'email', # 4. Email - 'experience', # 5. Опыт в деятельности - 'project_ideas', # 6. Идеи для проектов - 'expectations', # 7. Ожидания - 'time_commitment',# 8. Временные возможности + 'experience', # 6. Идеи для проектов + 'public_speakin', + 'quot', + 'improvisation', ] }, } From 671f3b08b8002180443b7762a6895281eb8985a3 Mon Sep 17 00:00:00 2001 From: Anastasia Lukina Date: Mon, 22 Dec 2025 15:53:35 +0300 Subject: [PATCH 10/12] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=8E=D0=B7=D0=B5=D1=80=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .venv/Head_of_studio.py | 565 +++++++++++++++++-------- .venv/Studio_editing.py | 218 ++++++++++ .venv/applications_manager.py | 130 ++++++ .venv/auth.py | 40 ++ .venv/catalog.py | 63 +++ .venv/database.py | 110 +++++ .venv/questionnaire_flow.py | 405 ++++++++++++++++++ .venv/questions.py | 207 +++++---- .venv/student_studios_bot (1).db | Bin 57344 -> 57344 bytes .venv/studio_manager.py | 40 +- .venv/user.py | 691 ------------------------------- .venv/usernew.py | 202 +++++++++ .venv/userrrrrr.py | 216 ++++++++++ .venv/users_collector.py | 359 +++++++++------- 14 files changed, 2155 insertions(+), 1091 deletions(-) create mode 100644 .venv/Studio_editing.py create mode 100644 .venv/applications_manager.py create mode 100644 .venv/auth.py create mode 100644 .venv/catalog.py create mode 100644 .venv/database.py create mode 100644 .venv/questionnaire_flow.py delete mode 100644 .venv/user.py create mode 100644 .venv/usernew.py create mode 100644 .venv/userrrrrr.py diff --git a/.venv/Head_of_studio.py b/.venv/Head_of_studio.py index 14fd07e..0c6a0ab 100644 --- a/.venv/Head_of_studio.py +++ b/.venv/Head_of_studio.py @@ -26,7 +26,7 @@ # Инициализируем менеджер студии studio_manager = StudioManager(bot) user_sessions = {} # Хранит активные сессии руководителей - +user_edit_states = {} # Добавляем словарь для состояний редактирования studio_manager.user_states = {} # Хранит состояние просмотра заявок #====================== @@ -47,12 +47,6 @@ def cleanup_stale_sessions(): del studio_manager.user_states[user_id] logger.warning(f"Очищена зависшая сессия пользователя {user_id}") -def show_main_menu(markup): - btn1 = types.KeyboardButton('') - btn2=types.KeyboardButton('Новые заявки') - markup.row(btn1, btn2) - btn3 = types.KeyboardButton('Изменить информацию о студии') - def get_connection() -> sqlite3.Connection: """Возвращает соединение с БД""" @@ -62,44 +56,54 @@ def create_main_menu_markup(user_id: int = None) -> types.ReplyKeyboardMarkup: """Создает главное меню руководителя""" markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) - btn_statistics = types.KeyboardButton('Отчет по заявкам') btn_applications = types.KeyboardButton("Новые заявки") markup.row(btn_statistics, btn_applications) - + + btn_edit = types.KeyboardButton('Изменить информацию о студии') btn_refresh = types.KeyboardButton('Обновить') markup.row(btn_refresh) - + markup.row(btn_edit) return markup + @bot.message_handler(commands=['head']) def handle_head_start(message): """Обработчик команды для входа руководителя""" user_id = message.from_user.id first_name = message.from_user.first_name - + + print(f"\n=== DEBUG /head команда ===") + print(f"Пользователь: {user_id} ({first_name})") + studio_id = studio_manager.get_studio_id_for_head(user_id) - + print(f"studio_id из базы: {studio_id}") + if not studio_id: + print("Пользователь не найден как руководитель в базе данных") bot.send_message( message.chat.id, "❌ Доступ запрещен\n\n" - "Вы не зарегистрированы как руководитель студии.", + "Вы не зарегистрированы как руководитель студии.\n\n" + f"Ваш ID: {user_id}", parse_mode='HTML' ) return - + # Получаем название студии studio_name = studio_manager.get_studio_name(studio_id) - + print(f"Название студии: {studio_name}") + # Получаем количество ожидающих заявок applications = studio_manager.get_new_applications(studio_id) pending_count = len(applications) - + welcome_message = ( f"👋 Добро пожаловать, {first_name}!\n\n" + f"Студия: {studio_name}\n" + f"Ожидающих заявок: {pending_count}" ) - + # Отправляем главное меню markup = create_main_menu_markup(user_id) bot.send_message( @@ -108,64 +112,207 @@ def handle_head_start(message): parse_mode='HTML', reply_markup=markup ) + print("=== Конец /head команды ===\n") -@bot.message_handler(func=lambda message: message.text.startswith('📋 Новые заявки')) +@bot.message_handler(func=lambda message: message.text == "Новые заявки") def handle_applications_start(message): """Начало просмотра заявок""" user_id = message.from_user.id - + chat_id = message.chat.id + + print(f"\n=== DEBUG: Начало просмотра заявок ===") + print(f"Время: {datetime.now()}") + print(f"Пользователь ID: {user_id}") + print(f"Chat ID: {chat_id}") + # Проверяем, не идет ли уже просмотр if user_id in studio_manager.user_states: + print(f"Ошибка: у пользователя {user_id} уже есть активный просмотр") bot.send_message( - message.chat.id, - "⚠️ Просмотр уже начат\n\n" + chat_id, + "⚠️ Просмотр заявок уже начат\n\n" "Завершите текущий просмотр или нажмите 'Прекратить просмотр'.", parse_mode='HTML' ) return - + + # Получаем studio_id с детальной отладкой + print(f"Вызов studio_manager.get_studio_id_for_head({user_id})...") studio_id = studio_manager.get_studio_id_for_head(user_id) + print(f"Полученный studio_id: {studio_id}") + if not studio_id: + print(f"ОШИБКА: studio_id не найден для пользователя {user_id}") + + # Детальная диагностика проблемы + conn = get_connection() + cursor = conn.cursor() + + try: + # 1. Проверяем таблицу studios + cursor.execute(""" + SELECT studio_id, name, head_user_id + FROM studios + WHERE head_user_id = ? + """, (user_id,)) + studio_result = cursor.fetchone() + print(f"Результат прямого SQL запроса в studios: {studio_result}") + + if studio_result: + print(f"Найдена студия: ID={studio_result[0]}, Название={studio_result[1]}, Head={studio_result[2]}") + else: + print(f"Запись в studios с head_user_id={user_id} НЕ НАЙДЕНА") + + # Показываем все студии для отладки + cursor.execute("SELECT studio_id, name, head_user_id FROM studios ORDER BY studio_id") + all_studios = cursor.fetchall() + print(f"Все студии в базе ({len(all_studios)} шт.):") + for studio in all_studios: + print(f" ID: {studio[0]}, Название: '{studio[1]}', Руководитель: {studio[2]}") + + # 2. Проверяем таблицу users + cursor.execute("SELECT user_id, full_name, role FROM users WHERE user_id = ?", (user_id,)) + user_result = cursor.fetchone() + print(f"Запись в users: {user_result}") + + if user_result: + print(f"Пользователь найден: {user_result[1]}, роль: {user_result[2]}") + else: + print(f"ВНИМАНИЕ: Пользователь с ID={user_id} не найден в таблице users") + + except Exception as e: + print(f"Ошибка при диагностике: {e}") + finally: + conn.close() + + # Отправляем информативное сообщение об ошибке + error_message = ( + f"❌ Ошибка доступа\n\n" + f"Ваш Telegram ID: {user_id}\n" + f"ID студии: {studio_id or 'Не найден'}\n\n" + f"Возможные причины:\n" + f"1. Вы не привязаны к студии как руководитель\n" + f"2. Ваш ID ({user_id}) не указан в поле head_user_id\n" + f"3. Студия не активирована (is_active = 0)\n\n" + f"Решение:\n" + f"• Обратитесь к администратору\n" + f"• Используйте команду /check_access для диагностики" + ) + bot.send_message( - message.chat.id, - "❌ 1Ошибка доступа", + chat_id, + error_message, parse_mode='HTML' ) return - + + # Успешно получили studio_id - продолжаем + print(f"УСПЕХ: Найдена студия ID={studio_id}") + # Получаем название студии studio_name = studio_manager.get_studio_name(studio_id) + print(f"Название студии: '{studio_name}'") + + # Запускаем просмотр заявок + print(f"Вызов studio_manager.start_reviewing_applications_with_id({user_id}, {studio_id})...") success = studio_manager.start_reviewing_applications_with_id(user_id, studio_id) - + if not success: + print(f"Ошибка: start_reviewing_applications_with_id вернула False") + bot.send_message( + chat_id, + "❌ Не удалось начать просмотр\n\n" + "Возможно, возникла ошибка при загрузке заявок.", + parse_mode='HTML' + ) return - - # Запускаем сессию + + print(f"Просмотр успешно запущен. Состояние пользователя создано.") + + # Проверяем, создано ли состояние + if user_id not in studio_manager.user_states: + print(f"КРИТИЧЕСКАЯ ОШИБКА: состояние пользователя не создано") + bot.send_message( + chat_id, + "❌ Внутренняя ошибка\n\n" + "Не удалось инициализировать сессию просмотра.", + parse_mode='HTML' + ) + return + + # Получаем информацию о заявках + state = studio_manager.user_states[user_id] + applications = state.get('applications', []) + total_apps = len(applications) + + print(f"Загружено заявок: {total_apps}") + + # Создаем сессию для статистики user_sessions[user_id] = { 'start_time': datetime.now(), 'studio_name': studio_name, - 'total_applications': len(studio_manager.user_states[user_id]['applications']), + 'studio_id': studio_id, + 'total_applications': total_apps, 'processed': 0, 'accepted': 0, - 'rejected': 0 + 'rejected': 0, + 'skipped': 0 } - - # Информационное сообщение - state = studio_manager.user_states[user_id] - total_apps = len(state['applications']) - + + print(f"Сессия создана: {user_sessions[user_id]}") + + # Отправляем информационное сообщение + if total_apps > 0: + welcome_message = ( + f"Начинаем просмотр заявок\n\n" + f"Студия: {studio_name}\n" + f"Всего заявок: {total_apps}\n\n" + f"Используйте кнопки для принятия решений:\n" + f"✅ Принять — одобрить заявку\n" + f"❌ Отклонить — отказать кандидату\n" + f"⏭ Пропустить — оставить на потом\n" + f"Прекратить — завершить просмотр\n\n" + f"Прогресс отображается внизу каждой заявки." + ) + else: + welcome_message = ( + f"Нет новых заявок\n\n" + f"Студия: {studio_name}\n" + f"Ожидающих заявок: 0\n\n" + f"Новых заявок для рассмотрения нет.\n" + f"Вы можете проверить позже или нажать 'Обновить'." + ) + bot.send_message( - message.chat.id, - f"Начинаем просмотр заявок\n\n" - f"Студия: {studio_name}\n" - f"Всего заявок: {total_apps}\n", + chat_id, + welcome_message, parse_mode='HTML' ) - - # Показываем первую заявку - studio_manager.show_next_application(message.chat.id, user_id) -@bot.message_handler(func=lambda message: message.text == 'отчет по заявкам') + # Если есть заявки, показываем первую + if total_apps > 0: + print(f"Показываем первую заявку из {total_apps}...") + studio_manager.show_next_application(chat_id, user_id) + else: + # Если заявок нет, очищаем состояние + print(f"Заявок нет, очищаем состояние...") + if user_id in studio_manager.user_states: + del studio_manager.user_states[user_id] + if user_id in user_sessions: + del user_sessions[user_id] + + # Возвращаем главное меню + markup = create_main_menu_markup(user_id) + bot.send_message( + chat_id, + "Возврат в главное меню", + parse_mode='HTML', + reply_markup=markup + ) + + print(f"=== DEBUG: Конец обработки ===\n") + +@bot.message_handler(func=lambda message: message.text.lower() == 'отчет по заявкам') def handle_statistics(message): user_id = message.from_user.id @@ -183,20 +330,25 @@ def handle_statistics(message): # Форматируем сообщение message_text = studio_manager.format_statistics_message(stats, studio_name) - + bot.send_message(message.chat.id, message_text, parse_mode='HTML') @bot.message_handler(func=lambda message: message.text == 'Обновить') def handle_refresh(message): - """Обновление списка заявок""" user_id = message.from_user.id - print(user_id) - + print(f"\n=== DEBUG Обновление ===") + print(f"Пользователь: {user_id}") + studio_id = studio_manager.get_studio_id_for_head(user_id) + print(f"studio_id: {studio_id}") + if not studio_id: + print("Ошибка: studio_id не найден") bot.send_message( message.chat.id, - "❌ 3Ошибка доступа", + "❌ 3Ошибка доступа\n\n" + f"User ID: {user_id}\n" + f"Studio ID: {studio_id}", parse_mode='HTML' ) return @@ -271,143 +423,196 @@ def handle_review_callbacks(call): # Передаем обработку в менеджер студии studio_manager.handle_application_action(call) -@bot.message_handler(func=lambda message: True) -def handle_other_messages(message): - """Обработчик всех других сообщений""" + + +# # ---------------------------ИЗМЕНЕНИЕ ИНФОРМАЦИИ О СТУДИИ--------------------------- + +def show_current_studio_info(chat_id: int, studio_id: int): + """Показывает текущую информацию о студии""" + conn = get_connection() + cursor = conn.cursor() + + cursor.execute(""" + SELECT name, description, contacts FROM studios WHERE id = ? + """, (studio_id,)) + + result = cursor.fetchone() + conn.close() + + if result: + name, description, contacts = result + + # Форматируем сообщение + name_display = name if name else '❌ Не указано' + description_display = description if description else '❌ Не указано' + contacts_display = contacts if contacts else '❌ Не указаны' + + message = ( + "Текущая информация о студии:\n\n" + f"Название:\n{name_display}\n\n" + f"Описание:\n{description_display}\n\n" + f"Контакты:\n{contacts_display}\n\n" + "Выберите, что хотите изменить:" + ) + else: + message = "Информация о студии не найдена в базе данных" + + bot.send_message(chat_id, message) + + +def create_edit_menu_markup() -> types.ReplyKeyboardMarkup: + """Создает меню для редактирования информации о студии""" + markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) + + btn_name = types.KeyboardButton('Изменить название') + btn_description = types.KeyboardButton('Изменить описание') + btn_contacts = types.KeyboardButton('Изменить контакты') + markup.row(btn_name, btn_description) + markup.row(btn_contacts) + + btn_save_all = types.KeyboardButton('Сохранить все изменения') + btn_cancel = types.KeyboardButton('Отмена') + markup.row(btn_save_all, btn_cancel) + + return markup + + +def create_main_menu_markup(user_id: int = None) -> types.ReplyKeyboardMarkup: + """Создает главное меню руководителя""" + markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) + + btn_statistics = types.KeyboardButton('Отчет по заявкам') + btn_applications = types.KeyboardButton("Новые заявки") + markup.row(btn_statistics, btn_applications) + + btn_edit = types.KeyboardButton('Изменить информацию о студии') + btn_refresh = types.KeyboardButton('Обновить') + markup.row(btn_refresh) + markup.row(btn_edit) + return markup + + +@bot.message_handler(func=lambda message: message.text.lower() == 'изменить информацию о студии') +def handle_edit_studio_start(message): + """Начало редактирования информации о студии""" + print(f"КНОПКА НАЖАТА: {message.text}") user_id = message.from_user.id - - # Если пользователь в режиме просмотра + print(f"Пользователь: {user_id}") + + # Проверяем, не идет ли уже просмотр заявок if user_id in studio_manager.user_states: + print("Пользователь в режиме просмотра") bot.send_message( message.chat.id, - "⚠️ Вы в режиме просмотра заявок\n\n" - "Используйте кнопки под заявкой для действий.", - parse_mode='HTML' + "Сначала завершите просмотр заявок" ) - else: - # Проверяем, является ли руководителем - # studio_id = studio_manager.get_studio_id_for_head(user_id) - studio_id = True - - if studio_id: - # Показываем меню руководителя - markup = create_main_menu_markup(user_id) - bot.send_message( - message.chat.id, - "Главное меню руководителя", - parse_mode='HTML', - reply_markup=markup - ) - else: - # Показываем обычное меню - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn1 = types.KeyboardButton('Каталог студий') - markup.row(btn1) - btn2 = types.KeyboardButton('Мои заявки') - btn3 = types.KeyboardButton('Подать заявку') - markup.row(btn2, btn3) - - bot.send_message( - message.chat.id, - "👋 Добро пожаловать в бот студенческих студий!", - parse_mode='HTML', - reply_markup=markup - ) + return + studio_id = studio_manager.get_studio_id_for_head(user_id) + print(f"ID студии: {studio_id}") + if not studio_id: + print("Ошибка: studio_id не найден") + bot.send_message( + message.chat.id, + "Ошибка доступа" + ) + return + + # Инициализируем состояние редактирования + user_edit_states[user_id] = { + 'studio_id': studio_id, + 'changes': {}, + 'awaiting_input': None + } + print(f"Создано состояние: {user_edit_states[user_id]}") + + # Показываем текущую информацию + print("Показываем информацию о студии...") + show_current_studio_info(message.chat.id, studio_id) + + # Показываем меню редактирования + print("Показываем меню редактирования...") + markup = create_edit_menu_markup() + bot.send_message( + message.chat.id, + "Редактирование информации о студии\n\n" + "Выберите параметр для изменения или 'Сохранить все изменения' для сохранения.", + reply_markup=markup + ) + print("Функция завершена") + + +from Studio_editing import ( + handle_edit_field_selection, + handle_field_input, + handle_save_all_changes, + handle_edit_cancel, + show_preview +) + + +@bot.message_handler(func=lambda message: message.text in ['Изменить название', 'Изменить описание', 'Изменить контакты']) +def handle_edit_field(message): + """Обработка выбора поля для редактирования""" + handle_edit_field_selection(message, bot, user_edit_states, create_edit_menu_markup) + + +@bot.message_handler(func=lambda message: user_edit_states.get(message.from_user.id, {}).get('awaiting_input')) +def handle_edit_input(message): + """Обработка ввода нового значения для поля""" + handle_field_input(message, bot, user_edit_states, create_edit_menu_markup) + + # Показываем предпросмотр после ввода + user_id = message.from_user.id + if user_id in user_edit_states and not user_edit_states[user_id].get('awaiting_input'): + show_preview(message.chat.id, user_id, bot, user_edit_states) + + +@bot.message_handler(func=lambda message: message.text == 'Сохранить все изменения') +def handle_save_changes(message): + """Сохранение всех изменений в БД""" + handle_save_all_changes(message, bot, user_edit_states, create_main_menu_markup) + + +@bot.message_handler(func=lambda message: message.text == 'Отмена') +def handle_cancel_edit(message): + """Отмена редактирования""" + handle_edit_cancel(message, bot, user_edit_states, create_main_menu_markup) + + +@bot.message_handler(content_types=['photo']) +def handle_photo_for_editing(message): + """ + Обработчик фото во время редактирования студии. + + Этот обработчик срабатывает, когда пользователь отправляет фото. + Проверяем, находится ли пользователь на этапе ожидания изображения. + """ + user_id = message.from_user.id + + # Проверяем, находится ли пользователь на этапе ожидания изображения + if studio_editor.get_user_step(user_id) != 'waiting_for_image': + # Если нет - игнорируем фото или сообщаем об ошибке + return + + # Получаем информацию о самом большом фото (Telegram отправляет несколько размеров) + file_id = message.photo[-1].file_id + + # Скачиваем фото с серверов Telegram + file_info = bot.get_file(file_id) + image_bytes = bot.download_file(file_info.file_path) + + # Передаем изображение в редактор студии + response, is_completed = studio_editor.handle_image(user_id, image_bytes) + + # Отправляем ответ пользователю + bot.send_message( + message.chat.id, + response, + parse_mode='html' + ) -# # ---------------------------ИЗМЕНЕНИЕ ИНФОРМАЦИИ О СТУДИИ--------------------------- -# -# @bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') -# # ==================== -# # РЕДАКТИРОВАНИЕ СТУДИИ -# # ==================== -# -# @bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') -# def edit_info(message): -# """ -# Обработчик кнопки 'Изменить информацию о студии'. -# Начинает процесс пошагового редактирования студии. -# """ -# user_id = message.from_user.id -# -# # Начинаем процесс редактирования через наш класс StudioEditor -# response = studio_editor.start_editing(user_id) -# -# # Отправляем первое сообщение с инструкцией -# bot.send_message( -# message.chat.id, -# response, -# parse_mode='html' -# ) -# -# -# @bot.message_handler(func=lambda message: studio_editor.get_user_step(message.from_user.id) in -# ['waiting_for_name', 'waiting_for_description']) -# def handle_text_for_editing(message): -# """ -# Обработчик текстовых сообщений во время редактирования студии. -# -# Этот обработчик срабатывает ТОЛЬКО когда пользователь находится -# на этапе 'waiting_for_name' или 'waiting_for_description'. -# """ -# user_id = message.from_user.id -# -# # Передаем текст сообщения в редактор студии -# response, is_completed = studio_editor.handle_message(user_id, message.text) -# -# # Отправляем ответ пользователю -# bot.send_message( -# message.chat.id, -# response, -# parse_mode='html' -# ) -# -# # Если процесс редактирования завершен, показываем главное меню -# if is_completed: -# # Показываем кнопки для дальнейших действий -# markup = types.ReplyKeyboardMarkup(resize_keyboard=True) -# show_main_menu(markup) -# -# bot.send_message( -# message.chat.id, -# "Что дальше?", -# reply_markup=markup -# ) -# -# -# @bot.message_handler(content_types=['photo']) -# def handle_photo_for_editing(message): -# """ -# Обработчик фото во время редактирования студии. -# -# Этот обработчик срабатывает, когда пользователь отправляет фото. -# Проверяем, находится ли пользователь на этапе ожидания изображения. -# """ -# user_id = message.from_user.id -# -# # Проверяем, находится ли пользователь на этапе ожидания изображения -# if studio_editor.get_user_step(user_id) != 'waiting_for_image': -# # Если нет - игнорируем фото или сообщаем об ошибке -# return -# -# # Получаем информацию о самом большом фото (Telegram отправляет несколько размеров) -# file_id = message.photo[-1].file_id -# -# # Скачиваем фото с серверов Telegram -# file_info = bot.get_file(file_id) -# image_bytes = bot.download_file(file_info.file_path) -# -# # Передаем изображение в редактор студии -# response, is_completed = studio_editor.handle_image(user_id, image_bytes) -# -# # Отправляем ответ пользователю -# bot.send_message( -# message.chat.id, -# response, -# parse_mode='html' -# ) -# # # @bot.message_handler(commands=['cancel']) # def cancel_editing(message): @@ -428,6 +633,6 @@ def handle_other_messages(message): # reply_markup=markup # ) -if __name__ == '__main__': - print("Бот запущен...") - bot.polling(none_stop=True) +# if __name__ == '__main__': +# print("Бот запущен...") +# bot.polling(none_stop=True) diff --git a/.venv/Studio_editing.py b/.venv/Studio_editing.py new file mode 100644 index 0000000..ec39012 --- /dev/null +++ b/.venv/Studio_editing.py @@ -0,0 +1,218 @@ +import sqlite3 +from typing import Dict, Any +import logging +from telebot import types + +logger = logging.getLogger(__name__) + +def handle_edit_field_selection(message, bot, user_edit_states, create_edit_menu_markup): + """Обработка выбора поля для редактирования""" + user_id = message.from_user.id + + if user_id not in user_edit_states: + bot.send_message(message.chat.id, "❌ Сессия редактирования не активна") + return + + edit_state = user_edit_states[user_id] + + # Определяем какое поле редактируем + field_map = { + 'Изменить название': ('name', 'введите новое название студии'), + 'Изменить описание': ('description', 'введите новое описание студии'), + 'Изменить контакты': ('contacts', 'введите новые контакты (телефон, email, соцсети)') + } + + field_name, prompt = field_map[message.text] + edit_state['awaiting_input'] = field_name + + # Сохраняем текущий текст для возможной отмены + if field_name not in edit_state['changes']: + conn = get_connection() + cursor = conn.cursor() + cursor.execute(f"SELECT {field_name} FROM studios WHERE id = ?", + (edit_state['studio_id'],)) + result = cursor.fetchone() + conn.close() + if result: + current_value = result[0] + edit_state['changes'][f'old_{field_name}'] = current_value + + bot.send_message( + message.chat.id, + f"✏️ Редактирование {field_name}\n\n" + f"Пожалуйста, {prompt}:\n\n" + "Для отмены ввода отправьте 'отмена'", + parse_mode='HTML' + ) + + +def handle_field_input(message, bot, user_edit_states, create_edit_menu_markup): + """Обработка ввода нового значения для поля""" + user_id = message.from_user.id + + if user_id not in user_edit_states: + return + + edit_state = user_edit_states[user_id] + field_name = edit_state.get('awaiting_input') + + if not field_name: + return + + # Проверяем отмену + if message.text.lower() == 'отмена': + if f'old_{field_name}' in edit_state['changes']: + # Восстанавливаем старое значение + old_value = edit_state['changes'][f'old_{field_name}'] + edit_state['changes'][field_name] = old_value + del edit_state['changes'][f'old_{field_name}'] + + edit_state['awaiting_input'] = None + + bot.send_message( + message.chat.id, + "⏪ Ввод отменен", + parse_mode='HTML', + reply_markup=create_edit_menu_markup() + ) + return + + # Сохраняем новое значение + edit_state['changes'][field_name] = message.text + edit_state['awaiting_input'] = None + + # Подтверждаем ввод + confirmation_text = { + 'name': 'Название', + 'description': 'Описание', + 'contacts': 'Контакты' + } + + bot.send_message( + message.chat.id, + f"✅ {confirmation_text.get(field_name, 'Поле')} обновлено\n\n" + f"Новое значение сохранено в черновике.\n\n" + f"Продолжайте редактирование или нажмите 'Сохранить все изменения'", + parse_mode='HTML', + reply_markup=create_edit_menu_markup() + ) + + # Показываем предпросмотр + show_preview(message.chat.id, user_id, bot, user_edit_states) + + +def show_preview(chat_id: int, user_id: int, bot, user_edit_states): + """Показывает предпросмотр изменений""" + if user_id not in user_edit_states: + return + + edit_state = user_edit_states[user_id] + studio_id = edit_state['studio_id'] + + # Получаем текущие данные + conn = get_connection() + cursor = conn.cursor() + cursor.execute("SELECT name, description, contacts FROM studios WHERE id = ?", (studio_id,)) + current_data = cursor.fetchone() + conn.close() + + if not current_data: + return + + current_name, current_description, current_contacts = current_data + + # Применяем изменения из черновика + preview_name = edit_state['changes'].get('name', current_name) + preview_description = edit_state['changes'].get('description', current_description) + preview_contacts = edit_state['changes'].get('contacts', current_contacts) + + message = ( + "Предпросмотр изменений:\n\n" + f"Название: {preview_name if preview_name else '❌ Не указано'}\n" + f"Описание:\n{preview_description if preview_description else '❌ Не указано'}\n" + f"Контакты:\n{preview_contacts if preview_contacts else '❌ Не указаны'}\n\n" + f"Изменено полей: {len([k for k in edit_state['changes'].keys() if not k.startswith('old_')])}" + ) + + bot.send_message(chat_id, message, parse_mode='HTML') + + +def handle_save_all_changes(message, bot, user_edit_states, create_main_menu_markup): + """Сохранение всех изменений в БД""" + user_id = message.from_user.id + + if user_id not in user_edit_states: + bot.send_message(message.chat.id, "❌ Нет изменений для сохранения") + return + + edit_state = user_edit_states[user_id] + + # Фильтруем только настоящие изменения (не старые значения) + real_changes = {k: v for k, v in edit_state['changes'].items() if not k.startswith('old_')} + + if not real_changes: + bot.send_message(message.chat.id, "❌ Нет изменений для сохранения") + return + + # Сохраняем изменения в БД + conn = get_connection() + cursor = conn.cursor() + + try: + # Формируем SQL запрос + set_clauses = [] + params = [] + + for field, value in real_changes.items(): + set_clauses.append(f"{field} = ?") + params.append(value) + + params.append(edit_state['studio_id']) + + update_query = f""" + UPDATE studios + SET {', '.join(set_clauses)} + WHERE id = ? + """ + + cursor.execute(update_query, params) + conn.commit() + + # Логируем изменения + logger.info(f"Студия {edit_state['studio_id']} обновлена пользователем {user_id}") + + bot.send_message( + message.chat.id, + "✅ Все изменения сохранены!", + parse_mode='HTML', + reply_markup=create_main_menu_markup(user_id) + ) + + except Exception as e: + conn.rollback() + logger.error(f"Ошибка при сохранении изменений студии: {e}") + bot.send_message( + message.chat.id, + f"❌ Ошибка при сохранении:\n{str(e)}", + parse_mode='HTML' + ) + finally: + conn.close() + + # Очищаем состояние редактирования + del user_edit_states[user_id] + + +def handle_edit_cancel(message, bot, user_edit_states, create_main_menu_markup): + """Отмена редактирования""" + user_id = message.from_user.id + + if user_id in user_edit_states: + del user_edit_states[user_id] + + bot.send_message( + message.chat.id, + "❌ Редактирование отменено", + parse_mode='HTML', + reply_markup=create_main_menu_markup(user_id) + ) \ No newline at end of file diff --git a/.venv/applications_manager.py b/.venv/applications_manager.py new file mode 100644 index 0000000..2b7b868 --- /dev/null +++ b/.venv/applications_manager.py @@ -0,0 +1,130 @@ +import sqlite3 +from datetime import datetime, timedelta +from typing import List, Dict +from telebot import types + + +class ApplicationsManager: + def __init__(self, bot): + self.bot = bot + self.db_path = "student_studios_bot (1).db" + + def get_connection(self): + return sqlite3.connect(self.db_path, check_same_thread=False) + + def get_user_applications(self, user_id: int) -> List[Dict]: + conn = self.get_connection() + cursor = conn.cursor() + + try: + # Проверим, есть ли таблица applications + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='applications'") + if not cursor.fetchone(): + print("Таблица applications не существует!") + return [] + + # Проверим структуру таблицы + cursor.execute("PRAGMA table_info(applications)") + columns = [col[1] for col in cursor.fetchall()] + print(f"Столбцы таблицы applications: {columns}") + + # Простой запрос для проверки + cursor.execute(""" + SELECT application_id, studio_id, status, created_at + FROM applications + WHERE user_id = ? + ORDER BY created_at DESC + """, (user_id,)) + + rows = cursor.fetchall() + print(f"Найдено заявок для пользователя {user_id}: {len(rows)}") + + applications = [] + + for row in rows: + studio_name = f"Студия #{row[1]}" + # Попробуем получить название студии + try: + cursor.execute("SELECT name FROM studios WHERE id = ?", (row[1],)) + studio_result = cursor.fetchone() + if studio_result: + studio_name = studio_result[0] + except: + pass + + applications.append({ + 'id': row[0], + 'studio_id': row[1], + 'status': row[2], + 'created_at': row[3], + 'studio_name': studio_name + }) + + return applications + + except Exception as e: + print(f"Ошибка при получении заявок: {e}") + return [] + finally: + conn.close() + + def show_user_applications(self, chat_id: int, user_id: int): + applications = self.get_user_applications(user_id) + + # Главное меню должно быть после сообщения, а не как кнопка над клавиатурой + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + markup.row(types.KeyboardButton('Каталог студий')) + markup.row(types.KeyboardButton('Мои заявки')) + + if not applications: + self.bot.send_message( + chat_id, + "У вас пока нет заявок", + reply_markup=markup + ) + return + + message = "📋 Ваши заявки:\n\n" + + for i, app in enumerate(applications, 1): + try: + created_date = datetime.strptime(app['created_at'], '%Y-%m-%d %H:%M:%S') + date_str = created_date.strftime('%d.%m.%Y %H:%M') + except: + date_str = app['created_at'] + + status_text = { + 'pending': '⏳ На рассмотрении', + 'approved': '✅ Принята', + 'rejected': '❌ Отклонена' + }.get(app['status'], app['status']) + + message += f"{i}. {app['studio_name']}\n" + message += f" {status_text}\n" + message += f" 📅 {date_str}\n\n" + + self.bot.send_message( + chat_id, + message, + parse_mode='HTML', + reply_markup=markup + ) + + def cleanup_old_applications(self): + conn = self.get_connection() + cursor = conn.cursor() + + try: + week_ago = datetime.now() - timedelta(days=7) + week_ago_str = week_ago.strftime('%Y-%m-%d %H:%M:%S') + + cursor.execute(""" + DELETE FROM applications + WHERE status IN ('approved', 'rejected') AND updated_at < ? + """, (week_ago_str,)) + + conn.commit() + except: + conn.rollback() + finally: + conn.close() \ No newline at end of file diff --git a/.venv/auth.py b/.venv/auth.py new file mode 100644 index 0000000..b4809a0 --- /dev/null +++ b/.venv/auth.py @@ -0,0 +1,40 @@ +import sqlite3 +import logging +from typing import Optional + +logger = logging.getLogger(__name__) + +def get_connection(db_path: str = "student_studios_bot (1).db") -> sqlite3.Connection: + """Возвращает соединение с БД""" + return sqlite3.connect(db_path, check_same_thread=False) + +def get_user_role(username: str) -> str: + """Получение роли пользователя из базы данных""" + if not username: + return 'student' + + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT role FROM users WHERE username = ?", (f"@{username}",)) + result = cursor.fetchone() + return result[0] if result else 'student' + except Exception as e: + logger.error(f"Database error getting role: {e}") + return 'student' + +def get_studio_id_for_head(user_id: int) -> Optional[int]: + """Получает ID студии, за которой закреплен руководитель""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT studio_id FROM studios WHERE head_user_id = ?", (user_id,)) + result = cursor.fetchone() + return result[0] if result else None + except Exception as e: + logger.error(f"Error getting studio for head {user_id}: {e}") + return None + + + + diff --git a/.venv/catalog.py b/.venv/catalog.py new file mode 100644 index 0000000..9483582 --- /dev/null +++ b/.venv/catalog.py @@ -0,0 +1,63 @@ +import telebot +from telebot import types +from typing import List, Tuple +import logging +from database import load_active_studios + +logger = logging.getLogger(__name__) + + +def show_studio_catalog(bot, chat_id: int): + """Показывает каталог студий""" + studios = load_active_studios() + + if not studios: + bot.send_message(chat_id, 'На данный момент активных студий нет.') + return + + markup = types.InlineKeyboardMarkup() + for studio_id, name in studios: + btn = types.InlineKeyboardButton(name, callback_data=f'info:{studio_id}') + markup.row(btn) + + bot.send_message(chat_id, 'Нажми на студию для просмотра:', reply_markup=markup) + + +def show_studio_info(bot, call): + """Показывает информацию о студии""" + studio_id = int(call.data.split(':')[1]) + + try: + from database import get_connection + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT name, description, contacts + FROM studios + WHERE studio_id = ? + """, (studio_id,)) + result = cursor.fetchone() + + if result: + name, description, contacts = result + text = f"{name}\n\n{description or 'Описание скоро появится.'}" + + if contacts: + text += f"\n\nКонтакты:\n{contacts}" + + markup = types.InlineKeyboardMarkup() + btn_apply = types.InlineKeyboardButton("📝 Подать заявку", callback_data=f'apply:{studio_id}') + markup.row(btn_apply) + + bot.edit_message_text( + text, + call.message.chat.id, + call.message.message_id, + parse_mode='html', + reply_markup=markup + ) + else: + bot.answer_callback_query(call.id, "Студия не найдена") + except Exception as e: + logger.error(f"Error showing studio info: {e}") + bot.answer_callback_query(call.id, "Ошибка при загрузке информации") \ No newline at end of file diff --git a/.venv/database.py b/.venv/database.py new file mode 100644 index 0000000..e3c4889 --- /dev/null +++ b/.venv/database.py @@ -0,0 +1,110 @@ +import sqlite3 +from datetime import datetime +from typing import Optional, Dict, Any +import logging + +logger = logging.getLogger(__name__) + + +def get_connection(db_path: str = "student_studios_bot (1).db") -> sqlite3.Connection: + """Возвращает соединение с БД""" + return sqlite3.connect(db_path, check_same_thread=False) + + +def get_user_by_telegram_id(telegram_id: int) -> Optional[Dict[str, Any]]: + """Получает данные пользователя по Telegram ID""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT full_name, student_group, phone_number, email + FROM users + WHERE telegram_id = ? + """, (telegram_id,)) + result = cursor.fetchone() + + if result: + return { + 'full_name': result[0] or '', + 'student_group': result[1] or '', + 'phone_number': result[2] or '', + 'email': result[3] or '' + } + except Exception as e: + logger.error(f"Error getting user data: {e}") + return None + + +def save_application_to_db(telegram_id: int, studio_id: int, answers: dict, + user_first_name: str = None, user_last_name: str = None) -> Optional[int]: + """Сохраняет заявку в БД""" + try: + with get_connection() as conn: + cursor = conn.cursor() + + # Получаем или создаем пользователя + cursor.execute("SELECT user_id FROM users WHERE telegram_id = ?", (telegram_id,)) + user_result = cursor.fetchone() + + if user_result: + user_id = user_result[0] + else: + # Получаем ФИО из ответов или Telegram + full_name = answers.get('full_name') + if not full_name: + full_name = f"{user_first_name or ''} {user_last_name or ''}".strip() + if not full_name: + full_name = "Не указано" + + cursor.execute(""" + INSERT INTO users (telegram_id, full_name, created_at) + VALUES (?, ?, ?) + """, (telegram_id, full_name, datetime.now())) + user_id = cursor.lastrowid + + # Генерируем сводку + from users_collector import QuestionnaireBuilder + summary = QuestionnaireBuilder.generate_summary(studio_id, answers) + + # Сохраняем заявку + cursor.execute(""" + INSERT INTO applications (user_id, studio_id, summary, status, created_at) + VALUES (?, ?, ?, ?, ?) + """, (user_id, studio_id, summary, 'pending', datetime.now())) + + application_id = cursor.lastrowid + + # Сохраняем ответы на вопросы + for question_id, answer in answers.items(): + from questions import get_question_obj + q_obj = get_question_obj(question_id) + question_text = q_obj.text if q_obj else question_id + + cursor.execute(""" + INSERT INTO application_answers + (application_id, question_id, question_text, answer_text) + VALUES (?, ?, ?, ?) + """, (application_id, question_id, question_text, answer)) + + conn.commit() + return application_id + + except Exception as e: + logger.error(f"Error saving application: {e}") + return None + + +def load_active_studios(): + """Загрузка списка активных студий""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT studio_id, name FROM studios WHERE is_active = 1 ORDER BY studio_id") + return cursor.fetchall() + except Exception as e: + logger.error(f"Error loading studios: {e}") + return [] + + + + diff --git a/.venv/questionnaire_flow.py b/.venv/questionnaire_flow.py new file mode 100644 index 0000000..6c6772c --- /dev/null +++ b/.venv/questionnaire_flow.py @@ -0,0 +1,405 @@ +import telebot +from telebot import types +from typing import Dict, Any +import logging + +from users_collector import QuestionnaireBuilder # Этот импорт должен быть +from questions import get_question_obj +from database import save_application_to_db + +logger = logging.getLogger(__name__) + + +class QuestionnaireFlow: + """Управление процессом заполнения анкеты""" + + def __init__(self, bot): + self.bot = bot + self.user_states = {} + + def start_questionnaire(self, call, studio_id: int): + """Начало анкеты для выбранной студии""" + user_id = call.from_user.id + + studio_config = QuestionnaireBuilder.get_questionnaire_for_studio(studio_id) + if not studio_config: + self.bot.answer_callback_query(call.id, "Анкета для этой студии не настроена") + return False + + self.user_states[user_id] = { + 'state': 'questionnaire', + 'studio_id': studio_id, + 'studio_name': studio_config.get('name', 'Студия'), + 'current_question_index': 0, + 'answers': {}, + 'questionnaire': studio_config + } + + # Получаем данные пользователя для автозаполнения + user_data = self._get_user_data(user_id) + self.user_states[user_id]['user_data'] = user_data + + if QuestionnaireBuilder.can_autofill(studio_id, user_data): + self._offer_autofill(call, studio_id, user_data) + else: + self._start_manual_filling(call, studio_id) + return True + + def _get_user_data(self, user_id: int) -> Dict[str, Any]: + """Получает данные пользователя из БД""" + from database import get_user_by_telegram_id + user = get_user_by_telegram_id(user_id) + + if user: + logger.info(f"Найдены данные пользователя {user_id}: {user}") + else: + logger.info(f"Данные пользователя {user_id} не найдены в БД") + + return user if user else {} + + def _offer_autofill(self, call, studio_id: int, user_data: Dict[str, Any]): + """Предлагает автозаполнение""" + autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) + autofill_info = [] + + # Логируем для отладки + logger.info(f"Поля для автозаполнения: {autofill_fields}") + logger.info(f"Данные пользователя: {user_data}") + + for field in autofill_fields: + value = user_data.get(field, '') + logger.info(f"Поле {field}: значение '{value}', тип {type(value)}") + if value and str(value).strip(): + field_name = { + 'full_name': 'ФИО', + 'student_group': 'Группа', + 'phone_number': 'Телефон', + 'email': 'Email' + }.get(field, field) + autofill_info.append(f"{field_name}: {value}") + + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + message = f"*Анкета для студии: {studio_name}*\n\n" + + if autofill_info: + message += "Найдены ваши данные из предыдущих анкет:\n" + for info in autofill_info: + message += f"• {info}\n" + message += "\nХотите использовать их для автозаполнения?" + else: + message += "К сожалению, данных для автозаполнения не найдено.\nХотите заполнить анкету вручную?" + + markup = types.InlineKeyboardMarkup() + if autofill_info: + markup.row( + types.InlineKeyboardButton("Использовать мои данные", callback_data='use_autofill'), + types.InlineKeyboardButton("Заполнить вручную", callback_data='manual_fill') + ) + else: + markup.row( + types.InlineKeyboardButton("Заполнить вручную", callback_data='manual_fill') + ) + + self.bot.edit_message_text( + message, + call.message.chat.id, + call.message.message_id, + parse_mode='Markdown', + reply_markup=markup + ) + + def _start_manual_filling(self, call, studio_id: int): + """Начинает ручное заполнение анкеты""" + user_id = call.from_user.id + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + + self.user_states[user_id]['state'] = 'answering' + + self.bot.edit_message_text( + f"Начинаем заполнение анкеты для студии *{studio_name}*.\n\n" + f"Пожалуйста, ответьте на несколько вопросов:", + call.message.chat.id, + call.message.message_id, + parse_mode='Markdown' + ) + + self._ask_next_question(call.message.chat.id, user_id) + + def _ask_next_question(self, chat_id: int, user_id: int): + """Задает следующий вопрос""" + if user_id not in self.user_states: + return + + state = self.user_states[user_id] + question_ids = state['questionnaire'].get('questions', []) + current_index = state.get('current_question_index', 0) + + # Пропускаем уже заполненные вопросы + while current_index < len(question_ids): + qid = question_ids[current_index] + # Если вопрос уже заполнен (например, через автозаполнение), пропускаем его + if qid in state['answers']: + logger.info(f"Вопрос {qid} уже заполнен, пропускаем") + current_index += 1 + state['current_question_index'] = current_index + else: + break + + # Проверяем, не заполнили ли мы все вопросы + if current_index >= len(question_ids): + self._show_summary(chat_id, user_id) + return + + # Получаем текущий вопрос + qid = question_ids[current_index] + q_obj = get_question_obj(qid) + + if not q_obj: + # Пропускаем несуществующий вопрос + state['current_question_index'] += 1 + self._ask_next_question(chat_id, user_id) + return + + # Сохраняем текущий вопрос + state['current_question_id'] = qid + + logger.info(f"Задаем вопрос {qid}, тип: {q_obj.type}") + + # Проверяем, может быть вопрос уже заполнен автозаполнением? + if qid in state['answers']: + logger.info(f"Вопрос {qid} уже имеет ответ: {state['answers'][qid]}") + state['current_question_index'] += 1 + self._ask_next_question(chat_id, user_id) + return + + # Отправляем вопрос с нужным типом клавиатуры + if q_obj.type == 'select' and q_obj.options: + # Для вопросов с выбором создаем INLINE клавиатуру + markup = types.InlineKeyboardMarkup(row_width=1) + for option in q_obj.options: + markup.add(types.InlineKeyboardButton( + option, + callback_data=f"select_answer:{qid}:{option}" + )) + + self.bot.send_message( + chat_id, + q_obj.text, + reply_markup=markup + ) + + elif q_obj.type == 'boolean': + # Для boolean вопросов используем reply-клавиатуру + markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) + markup.add("Да", "Нет") + + self.bot.send_message( + chat_id, + q_obj.text, + reply_markup=markup + ) + + else: + # Для обычных вопросов без клавиатуры + self.bot.send_message(chat_id, q_obj.text) + + def process_answer(self, message): + """Обработка текстового ответа на вопрос""" + user_id = message.from_user.id + + if user_id not in self.user_states: + return + + state = self.user_states[user_id] + if state.get('state') != 'answering': + return + + current_qid = state.get('current_question_id') + answer_text = message.text.strip() + + if not current_qid: + return + + # Получаем объект вопроса для валидации + q_obj = get_question_obj(current_qid) + + # Проверяем тип вопроса - если это select, то обрабатывается через callback + if q_obj and q_obj.type == 'select': + return # Пропускаем, select обрабатывается через callback + + if q_obj: + is_valid, error_msg = q_obj.validate(answer_text) + if not is_valid: + self.bot.send_message(message.chat.id, f"❌ {error_msg}\n\nПопробуйте еще раз:") + return + + # Сохраняем ответ + state['answers'][current_qid] = answer_text + + # Увеличиваем индекс и задаем следующий вопрос + state['current_question_index'] += 1 + + # Отправляем подтверждение для вопросов с reply-клавиатурой + if q_obj and q_obj.type == 'boolean': + # Сначала отправляем подтверждение с текстом + self.bot.send_message( + message.chat.id, + f"✅ Ответ принят: {answer_text}", + reply_markup=types.ReplyKeyboardRemove() + ) + + self._ask_next_question(message.chat.id, user_id) + + def process_select_answer(self, call): + """Обработка выбора из inline-клавиатуры для select вопросов""" + user_id = call.from_user.id + + if user_id not in self.user_states: + self.bot.answer_callback_query(call.id, "Сессия устарела") + return + + state = self.user_states[user_id] + if state.get('state') != 'answering': + self.bot.answer_callback_query(call.id, "Неверное состояние") + return + + # Разбираем callback_data: "select_answer:question_id:selected_option" + parts = call.data.split(':') + if len(parts) != 3: + self.bot.answer_callback_query(call.id, "Ошибка данных") + return + + qid = parts[1] + selected_option = parts[2] + + # Проверяем, что это текущий вопрос + current_qid = state.get('current_question_id') + if qid != current_qid: + self.bot.answer_callback_query(call.id, "Неверный вопрос") + return + + # Сохраняем ответ + state['answers'][qid] = selected_option + + # Увеличиваем индекс вопроса + state['current_question_index'] += 1 + + # Удаляем inline-клавиатуру и показываем выбранный вариант + self.bot.edit_message_text( + f"❓ {get_question_obj(qid).text if get_question_obj(qid) else 'Вопрос'}\n" + f"✅ Вы выбрали: {selected_option}", + call.message.chat.id, + call.message.message_id + ) + + # Задаем следующий вопрос + self._ask_next_question(call.message.chat.id, user_id) + + def _show_summary(self, chat_id: int, user_id: int): + """Показывает сводку по заполненной анкете""" + if user_id not in self.user_states: + return + + state = self.user_states[user_id] + studio_id = state['studio_id'] + answers = state['answers'] + + # Генерируем сводку + summary = "📋 *Сводка вашей заявки*\n\n" + + for qid, answer in answers.items(): + q_obj = get_question_obj(qid) + question_text = q_obj.text if q_obj else qid + summary += f"*{question_text}*\n" + summary += f"{answer}\n\n" + + # Отправляем сводку пользователю + self.bot.send_message( + chat_id, + summary, + parse_mode='Markdown' + ) + + # Подтверждение отправки + markup = types.InlineKeyboardMarkup() + markup.row( + types.InlineKeyboardButton("✅ Отправить заявку", callback_data='confirm_application'), + types.InlineKeyboardButton("✏️ Заполнить заново", callback_data='restart_questionnaire') + ) + + self.bot.send_message( + chat_id, + "Проверьте информацию выше. Все верно?", + reply_markup=markup + ) + + state['state'] = 'reviewing' + + def _confirm_application(self, call): + """Подтверждение и сохранение заявки""" + user_id = call.from_user.id + + if user_id not in self.user_states: + self.bot.answer_callback_query(call.id, "Сессия устарела") + return + + state = self.user_states[user_id] + + # Сохраняем заявку в БД + application_id = save_application_to_db( + telegram_id=user_id, + studio_id=state['studio_id'], + answers=state['answers'], + user_first_name=call.from_user.first_name, + user_last_name=call.from_user.last_name + ) + + if application_id: + # Удаляем состояние пользователя + if user_id in self.user_states: + del self.user_states[user_id] + + # Сообщаем об успехе + self.bot.edit_message_text( + "✅ *Ваша заявка успешно отправлена!*\n\n" + f"Номер заявки: #{application_id}\n" + "С вами свяжутся в ближайшее время.", + call.message.chat.id, + call.message.message_id, + parse_mode='Markdown' + ) + + # Показываем главное меню + self._show_main_menu(call.message.chat.id, call.from_user) + else: + self.bot.edit_message_text( + "❌ Произошла ошибка при сохранении заявки. Попробуйте позже.", + call.message.chat.id, + call.message.message_id + ) + + def _restart_questionnaire(self, call): + """Перезапуск заполнения анкеты""" + user_id = call.from_user.id + if user_id in self.user_states: + state = self.user_states[user_id] + studio_id = state['studio_id'] + + # Начинаем заново + self.start_questionnaire(call, studio_id) + + def _show_main_menu(self, chat_id: int, user): + """Показывает главное меню""" + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + btn3 = types.KeyboardButton('Подать заявку') + markup.row(btn2, btn3) + + self.bot.send_message( + chat_id, + 'Главное меню', + parse_mode='html', + reply_markup=markup + ) \ No newline at end of file diff --git a/.venv/questions.py b/.venv/questions.py index 2b4805e..2a0e45b 100644 --- a/.venv/questions.py +++ b/.venv/questions.py @@ -1,126 +1,177 @@ - -from typing import Dict, Any, Callable, Optional +from typing import Dict, Any, Callable, Optional import re + class Question: """Класс для хранения вопроса""" - def __init__(self, + + def __init__(self, question_id: str, - text: str, + text: str, # Полный текст вопроса должен передаваться question_type: str = 'text', is_required: bool = True, autofill_field: Optional[str] = None, validation_func: Optional[Callable] = None, options: Optional[list] = None): self.id = question_id - self.text = text + self.text = text # Используйте переданный текст self.type = question_type # text, phone, email, boolean, select self.is_required = is_required - self.autofill_field = autofill_field # Поле для автозаполнения из БД + self.autofill_field = autofill_field self.validation_func = validation_func self.options = options or [] - + def validate(self, answer: str) -> tuple[bool, str]: """Валидация ответа""" answer = answer.strip() - + if self.is_required and not answer: return False, "Это обязательный вопрос" - + if self.validation_func: return self.validation_func(answer) - + # Базовая валидация по типу if self.type == 'phone_number': phone_digits = re.sub(r'[^\d]', '', answer) if len(phone_digits) < 10: return False, "Введите корректный номер телефона" - + elif self.type == 'email': if '@' not in answer or '.' not in answer: return False, "Введите корректный email" - + elif self.type == 'boolean': if answer.lower() not in ['да', 'нет']: return False, "Ответьте 'да' или 'нет'" - + elif self.type == 'select': if answer not in self.options: return False, f"Выберите вариант из списка: {', '.join(self.options)}" - + return True, "" # ========== СЛОВАРЬ ВСЕХ ВОПРОСОВ ========== +# ИСПРАВЛЕНО: Теперь сразу создаем объекты Question с полным текстом ALL_QUESTIONS = { - # ID вопроса: (текст вопроса, объект Question) - 'full_name': ("Укажите ваше ФИО (полностью):", - Question('full_name', 'ФИО', autofill_field='full_name')), - - 'group': ("Укажите номер вашей группу:", - Question('group', 'Группа', autofill_field='student_group')), - - 'phone_number': ("Укажите ваш номер телефона:", - Question('phone_number', 'Телефон', 'phone_number', autofill_field='phone_number')), - - 'email': ("Укажите ваш email адрес:", - Question('email', 'Email', 'email', autofill_field='email')), - - 'experience': ("Расскажите о вашем опыте в данной деятельности:", - Question('experience', 'Опыт в деятельности')), - - 'motivation': ("Почему вы хотите вступить в эту студию?", - Question('motivation', 'Мотивация вступления')), - - 'expectations': ("Какие у вас ожидания от участия в студии?", - Question('expectations', 'Ожидания от студии')), - - 'activeness': ("Готовы ли вы участвовать в мероприятиях студии? (да/нет)", - Question('activeness', 'Участие в мероприятиях', 'boolean')), - - 'media': ("Какое направление вам интересно?\n" - "Варианты: Фотостуди, Видеостудия, Студия дизайна, Журналистика и СММ", - Question('dance_style', 'Танцевальный стиль', 'select', - options=['Фотостуди', 'Видеостудия', 'Студия дизайна', 'Журналистика и СММ'])), - - 'music_instrument': ("10. 🎵 На каком инструменте вы играете (если играете)?", - Question('music_instrument', 'Музыкальный инструмент')), - - 'singing_experience': ("Есть ли у вас опыт пения? Если да, опишите его:", - Question('singing_experience', 'Опыт пения')), - - 'improvisation': ("Как вы относитесь к импровизации?", - Question('improvisation', 'Импровизация')), - - 'tech_skills': ("Какими техническими навыками вы владеете?", - Question('tech_skills', 'Технические навыки')), - - 'organizational_exp': ("Был ли у вас опыт организации мероприятий? Опишите:", - Question('organizational_exp', 'Организационный опыт')), - 'quote': ("Девиз по жизни или цитата, которая вас вдохновляет:", - Question('quote', 'Цитата')), - - 'project_ideas': ("Есть ли у вас идеи для проектов в студии?", - Question('project_ideas', 'Идеи для проектов')), - 'leadership': ("Есть ли у вас лидерский опыт?", - Question('leadership', 'Лидерские качества')), - - 'why_choose_us': ("Почему вы выбрали именно нашу студию?", - Question('why_choose_us', 'Почему выбрали нас')), - - 'feedback_source': ("Откуда вы узнали о нашей студии?", - Question('feedback_source', 'Источник информации')), - - 'public_speaking': ("Как вы относитесь к публичным выступлениям??", - Question('public_speaking', 'Публичные выступления')), + 'full_name': Question( + 'full_name', + 'Укажите ваше ФИО (полностью):', + autofill_field='full_name' + ), + + 'group': Question( + 'group', + 'Укажите номер вашей группу:', + autofill_field='student_group' + ), + + 'phone_number': Question( + 'phone_number', + 'Укажите ваш номер телефона:', + 'phone_number', + autofill_field='phone_number' + ), + + 'email': Question( + 'email', + 'Укажите ваш email адрес:', + 'email', + autofill_field='email' + ), + + 'experience': Question( + 'experience', + 'Расскажите о вашем опыте в данной деятельности:' + ), + + 'motivation': Question( + 'motivation', + 'Почему вы хотите вступить в эту студию?' + ), + + 'expectations': Question( + 'expectations', + 'Какие у вас ожидания от участия в студии?' + ), + + 'activeness': Question( + 'activeness', + 'Готовы ли вы участвовать в мероприятиях студии? (да/нет)', + 'boolean' + ), + + 'media': Question( + 'media', + 'Какое направление вам интересно?\nВарианты: Фотостуди, Видеостудия, Студия дизайна, Журналистика и СММ', + 'select', + options=['Фотостуди', 'Видеостудия', 'Студия дизайна', 'Журналистика и СММ'] + ), + + 'music_instrument': Question( + 'music_instrument', + 'На каком инструменте вы играете (если играете)?' + ), + + 'singing_experience': Question( + 'singing_experience', + 'Есть ли у вас опыт пения? Если да, опишите его:' + ), + + 'improvisation': Question( + 'improvisation', + 'Как вы относитесь к импровизации?' + ), + + 'tech_skills': Question( + 'tech_skills', + 'Какими техническими навыками вы владеете?' + ), + + 'organizational_exp': Question( + 'organizational_exp', + 'Был ли у вас опыт организации мероприятий? Опишите:' + ), + + 'quote': Question( + 'quote', + 'Девиз по жизни или цитата, которая вас вдохновляет:' + ), + + 'project_ideas': Question( + 'project_ideas', + 'Есть ли у вас идеи для проектов в студии?' + ), + + 'leadership': Question( + 'leadership', + 'Есть ли у вас лидерский опыт?' + ), + + 'why_choose_us': Question( + 'why_choose_us', + 'Почему вы выбрали именно нашу студию?' + ), + + 'feedback_source': Question( + 'feedback_source', + 'Откуда вы узнали о нашей студии?' + ), + + 'public_speaking': Question( + 'public_speaking', + 'Как вы относитесь к публичным выступлениям?' + ), } + def get_question_handler(question_id: str) -> str: """Возвращает текст вопроса по ID""" - result = ALL_QUESTIONS.get(question_id) - return result[0] if result else "" + q_obj = ALL_QUESTIONS.get(question_id) + return q_obj.text if q_obj else "" + def get_question_obj(question_id: str) -> Optional[Question]: """Возвращает объект вопроса по ID""" - result = ALL_QUESTIONS.get(question_id) - return result[1] if result else None + return ALL_QUESTIONS.get(question_id) diff --git a/.venv/student_studios_bot (1).db b/.venv/student_studios_bot (1).db index f607c7674e915409f0962ddb01921ca8f12e9c9a..81d02b87fe20907df3bea1eb0b18eefcf44cc763 100644 GIT binary patch literal 57344 zcmeHQYiwIbcD@wfS0ZW0>t$V4@s+$5 zCaFAHq#H`>JnC!`G@ETuY@FBb0*gN2ICd?++aCei zerN83m(-Oh$#UX2Lt7MeXXc!l@0>Gt&T!7TM;||wE~KQB+1zxpAj!fep{`EYBT0fF zG~vgu{n+q3h#v=j^cP*L^=cBjjveqhw+kNI+k*Y&hIgDF_)a)J@yVW$?QNgzlO0p^ zuNIlFUirfG7I(0<)%H?TA$cN`O3uz^(qqX&Iy;j$e|GO1jt`8)rICTXhvJgCL~8em z5?-cjiS)QMI5ZOfc6?ZRYVDVEni=FgkK%a0rwR$A?Bb)n;?~RL)#~=*WmP zGtdK0s<)xAMlOxjp_#*?Oheo8f+0@Kx|T1O92r8#4#ffWzJa6r2KL9T=oqL}P*Shv+fAj*sCCSzZ0=w% zXgkxv*!E>w>II6osz~YF_Fv8&8q#A3aLzLBA1*d97xwXrF};Z9U476B&DX4>5O)*{JCRy zz!e;5v;rK(WV8)~}}E!6A9B*Z-oS{>u7=`O*BcwWdG}yV>UCc^@W*^Zq}yD zXT)_VA3@AS-a#}aZp+X<8H-BtZr1dyuTT$r`9?>m&$A6Q3qy} zOS^YVgZm}rg0iICEWMyCm5R!FNm*1bO9O`vB_1EeZE5hx&{3(~m^(WqHU)J`Y{Eoc zsWjiET=zO-0L$iYCm;JFrjeX!uXtf6yxF#XqT5Mza>?`6_93ORI{>Pg`D8 z2#75|KEFlLV@?;{`HnN;YB%eA3bs5mma6*GRWZX<6}Hn|rxPtZ{J#?R2OAr0XU0^T zY#E!^?)G+_hRd(sdUyM*77M#KSzWDOLWDJ<&iUv3ZpL!(RxH{2y+v!&f(hQM{eii# z#jCB_J8_kU(&DX{Rix3OLCjNBI-bgp<)v1-};#hX3kQ^)I%U5P|+39Qo(^rAk z@YUC!PLHQ5ucz|~5WvmV=!&rf3=s!f;UzSMF{)5Odgf_bVU1gt6Z+uF9uLbD+3 z$))p?_$n*&T=H--m!3)XnP0RU5$+C0cZb5e!yzdY>yO0xyL+RN?r2o*4)^u!anuVS z(>Or1ly_;@m;TZ23%*Oxn}+(NP;Y-Y+#l`figfpcqmgJR9O>(h#pG^4(;;luD1_Nw zkGZ8Rb@%p%y8Gqc1NM#UcF*HBx3E1S1ZDP3TH-{upuC}6RIX!Z_=d7bJIWXeZP+fA zUXYac)l1mDy$T!{s^7K^3VakvHNLe4M`<0X75 zDp%nweXJ}=`1KLHa1-w+VpU$3;E!BlE)^Z80RgmMRIW%Z$_L6@%1lk?zt9<?CLf2`W3Zy-N>VKzIM2`FXnfM!d61w z>k)=)hl;QSsZ~93`e3p#UnPDiGAK@3<(rUFQPsUO08;8Zz%65KUMyU z1`2+R`64i6{ZQ=&htKE))bF549I9+$d@;q=(I+GuYFG3mu)l{LuMz7xA(_J{1>wJGb}8>xHP zQM7x}e7*Y+%>NJkLJ0h8;1_}a3j8+k%X>n=d=MM~jsQo1Bft^h2yg^A0vrL307rl$ zz!A8I5pdWAo0HxgetWa6e0Ig&U~jVNhZySW?M+_&`+r~HH-bHISqOX@xE%PWdl(u% zG>!mAfFr;W;0SO8I0762jsQo1Bft^h2z>bv*yOOc+RW)vy+J3=G?=a=w9LADF}TfM zQ$fDoZf~^d*(seUwgo;T1R4ULiO+jJa$c+Z0sYBe9Dy$effv8w_6jy{Jo-VD?Tk7x-c1NLB3o5fB!i`u^9)gZ8Qrr~+m(UzBc`V`zaaEPr zD~k`S*i24MW=>6I=$}UaJr=V>k%cC(5n!m{O#xDro%^gN1>#4cTQqtM7lDB+%8=4j zzp4vES=0tGb!w9R(^$O=Sy8Owbv6vdN8wNxMPlg^)^z12U7$ciXwUaadn%Mj8^|dd zi7^NxqKksA2q*+_f&79Iy+(m^G$M_zDmXT<7r>3hN#dQt)^v3RWKc2;_5lnF$+;og#CoX(dw=Qxm7A zCQn(@S*>#S_Vo2wQ&6>=>4|D3q(Br0SZ!&b5bKHIt+)49l+*`FtZXNDQjaGXQj=Qu zHd2kJCMRGa8sWdh=nxD^s>|$x&26f6KrdksC`81fX6hI48S#k-fkY6dW)%};aT+yB zD}R-RchW@6%mMW=#i<4Y6Soey)JP#j7%`ccltAJ#BzHv=7Y3c0*1y>7Sf=$X+fyjg zFNeD#(MU`V8Cs8$o_`T4B06Tzi7FyFwa=pLcYUuMH&_la8izf%;&P#1_XP2VXh z@4=ieqQmn*Xi*fyRQ9Gm8!P8`igF~1nOLUlbmr4g#hqd_8tu_vy`+W_REpD6Om>Pi z5l5-bIdvAKX$gERr#iov09>|I2X+)N?Ddr}1065@cxfxX##a?Rq zww0hwb+3tHAFD_7(C#IorE16xGBMl^C>)h(2N+aKbKyRRcB4NNwO8HfLoq27>X#!( zQ`HsfjYXoy1cs9CKDA^GqO5((uNLOX4E@WT#JiOwBax6MNt2LBWh5H!v!RsM*riAeA*6n5Npw zl9fox##Jqn)!WU-SVTbmnQ&HbIWYSrSP@n~nv_sGct>zTj5P7~C^ zLHjcEQB%-`$$Y|nvF`4&`Je<=MJ1~i)cg8^WW&-yR2y5wN}jk#Go>NSTrQ_hi!^su z9mZ#=0qDkpyA0;_#hqedzS*V&^!Q2uQI5W@zECd`JQ$j+0z@Tu5kTRv6+&V5s)A{i z7{R%#cboiwU*NNV?EkI*ivJb=BmQl^fA+oQJK<|;__X1jhQ0Nl*1u8zWPPjnYw@x; zDSqAib8prg@!C9BJ#Tov<^Hw%hi=)WxSnwZod4*2(D8G}G5fFV&)dINcfD@b_K9sm z_~eW6C9Ap4g`g76#YUk~c28&VNDKB%J!NB-Q<^`ZD-!vtnnzl6bu+#X3Qe**_3Uga zm!^l9V7)-e8aCST(On*kj{NBZ=to;|yRbuc<0&9hcs>xGm61y3m5uW8Ox~RZ{{vkf z!4`Z<*bEqGZDZ+SFZW0HbXm}PVmHw`Eo_n9?14uI445Sbo{ zS53aQb5Y^ITQExO0;BD7!Zz7$JY33>OATjAjb5@~@XSs~dsopWJRrMHrc&c4l4GY5 z`RrT{s<-Z?`LbnPC1d7UW;&Rfxsj{Xm7a#8J)O?0mUcV-1@kN{xE0!&aWw)+*M=Ll z>K$2dEjrs6+Xpe1xbC=3Ywn*#KMs6TH!0Wcm=u%S$yQbMTV>a6s=mpiw&?S*Z&064 z3tMH^I!%|Hehb3M9jZw+LE-ghU9QKZ^qmPS7A;`0>lkdxS#48quVN*)viX!<)WK%3 zXnP1Z3umSEtgx)qJicp2Z_A{POCZ(!l+Y?WO{THBm96ebl1FGkdU!jL9;IHI)PEIA zZJn+K&G!32GZ@7E!O4`Ft^G{1e|cc>Ce&>05O&GV3RSIetE-%93x=UTAcn2bM5n5W zCU>3u35zDNMl{*JP3V9E7&^vw04n`|i>4#LkEYE@aC4{+)S5}<^UD%kt>~eV*TASkvSrh!*3J8C$~D~!1P>mbL} z#9^f-CT#X@MNOzlD;S$^<`rtv7c0LkIv@HPaU&gZEYlI%2C!(-vjt6{BRgbAm5!LU z6)f5%??XE_x!5;!axuB*hc|--CYM&(zPic93eB>8jcQ2S1kA~{pxW*SRi-<3OWi3CZJ!S{p*wA|eP!K|e~S4fRbCi17!nM{5azH-4J zR^A)fg*1nRQc$ilqXZY+qVj+Tl>yWaxo%~k2qx`iL88k|{938=%V3IF)bDhmJ}^T4 zDvYdZA8>jEFN0=n$UIZUnKlL59H7PY#|(@`#XWXZWT>nRit50t@7f~^LgoF3a2>H~ z#XP(W6RqL$Hk~%K0Wh28DqPT_qBdM(Z-_D(hTxnB{v~iO@Ez{|-`2N+-2cayK-~W~ zzFg$~KllH&a|O43n1TEMmFHTx|8IQrj-yWM*J9lNPvvO2TmD*|`~NIsG<^fV!uS3P z_y3g}-2cY|<5xQEK?g|mZ>$XuqUKdz;`o4dSaR(U2bC0$|JNguHzv|YMU}_@-)5E!{gnOfDL0S*r@;F4r?KGi|2Sn% z`AaBzqoh4^oX7w3_doLP&m9q=DRDM@XB*i=jck)v*?O1f5>mTh zh8S-gaD6TFeWu>#&=KV3Hx51CG`P(U>mYUkybhY$4uA37fV$+y~;3-$A8z}rC zay`Gmx`gb{L=NRwDZ4CEfxeF@njfi{SrgaB_IW3fCjWo4r%eb<2O9hz`Vad4&$rO< zzYX5{r|Y+he=QD+F7NYRm*)ql!e1N#jsQo1Bft^h2z<2=IJFnccuFK1l)cq?$=2;D zOp%XZ;o{B?fLUA7Eo_&)l^M!z$IUSu91D(fd%#i63hXr{G2584LYk*$!M5mnnDM2o zz+O{^vpchn)}2v{jvV+yO1?!21idV$m1V}v<*tP(FJpOw;>m}I)@jldHEC6qS6=(b z8gnTudO7heP-b~~y%h3gN5mgO;i|kpa;bUvV zqK^|1-RwPg#_aV0BxK#}JsWHGI)mb-uwnM@FWl_eL)1*mmr6axbT9~84r)G%k2V$iE9+?XMHV|60Bs{N{XLH!B*f z;Ipx2;%1PQl$ACHYGKGJ&CbkA%**#nEH0@`G`4`i;*!#o)VvZS10z#iLn9zkFfz6> zva~Xp{6)^0#e$ieVY0Qns<)nnp_#dbnW>3^sj0Dnk%bx1Kx?4U;&_ZsOv%m6!(#Ai z`7nNnZ@6^v{w{Co`je4BX;Uhz#_AT^oK{u@x}G6VnR%{&f=_$LOiZ@%pR zmEVh1oR!&_F|nW^Co?&*Br`uRJ~6MjJhiA;322WXCz3=lH#4&|BPcvF^NU${fzqrT z8yWcS@b2aj;;QA?2z29oj?Eia1haD~u^Th+U*ey~A2XS+zz!(!oNcpl`$dV(EEE3l zGjeTaoWLx=#UQ}Iz^}^}#cK{!pvf!Hq`=I}&=}qn3No6LL2$|z7wAu60a*nN3DwEs_AhyW QHh=&N5c6*4x$uP_05Tbpz5oCK diff --git a/.venv/studio_manager.py b/.venv/studio_manager.py index 7f1820d..3bb0ab0 100644 --- a/.venv/studio_manager.py +++ b/.venv/studio_manager.py @@ -28,18 +28,54 @@ def __init__(self, bot, db_path: str = "student_studios_bot (1).db"): def get_connection(self) -> sqlite3.Connection: """Возвращает соединение с БД""" return sqlite3.connect(self.db_path, check_same_thread=False) - + + def start_reviewing_applications_with_id(self, user_id: int, studio_id: int) -> bool: + """Начинает просмотр заявок для указанного пользователя и студии""" + try: + # Получаем новые заявки + applications = self.get_new_applications(studio_id) + + if not applications: + return False + + # Сохраняем состояние пользователя + self.user_states[user_id] = { + 'studio_id': studio_id, + 'applications': applications, + 'current_index': 0, + 'status': 'reviewing' + } + + return True + + except Exception as e: + logger.error(f"Error starting review for user {user_id}, studio {studio_id}: {e}") + return False + def get_studio_id_for_head(self, user_id: int) -> Optional[int]: - #Получает ID студии, за которой закреплен руководитель try: with self.get_connection() as conn: cursor = conn.cursor() + print(f"\n[DEBUG] Ищу студию для user_id: {user_id}") + cursor.execute(""" SELECT studio_id FROM studios WHERE head_user_id = ? """, (user_id,)) + result = cursor.fetchone() + print(f"[DEBUG] Результат запроса: {result}") + + if result: + print(f"[DEBUG] Найдена студия ID: {result[0]}") + else: + print(f"[DEBUG] Студия не найдена для user_id: {user_id}") + # Покажем все студии для отладки + cursor.execute("SELECT studio_id, name, head_user_id FROM studios") + all_studios = cursor.fetchall() + print(f"[DEBUG] Все студии в базе: {all_studios}") + return result[0] if result else None except Exception as e: logger.error(f"Error getting studio for head {user_id}: {e}") diff --git a/.venv/user.py b/.venv/user.py deleted file mode 100644 index b59201c..0000000 --- a/.venv/user.py +++ /dev/null @@ -1,691 +0,0 @@ -import telebot -from telebot import types -import sqlite3 -from datetime import datetime -from typing import Dict, Any, List, Tuple -import logging -import re - -from Admin import initialize as init_admin, admin_start, handle_admin_message -from users_collector import QuestionnaireBuilder -from questions import get_question_handler, get_question_obj - -# Настройка логирования -logging.basicConfig( - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - level=logging.INFO -) -logger = logging.getLogger(__name__) - -bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") -DB_PATH = "student_studios_bot (1).db" -#Инициализация модуля для функционала админа -init_admin(bot) - -def get_connection() -> sqlite3.Connection: - """Возвращает соединение с БД""" - return sqlite3.connect(DB_PATH, check_same_thread=False) - -def load_active_studios() -> List[Tuple[int, str]]: - """Загрузка списка активных студий""" - try: - with get_connection() as conn: - cursor = conn.cursor() - cursor.execute("SELECT studio_id, name FROM studios WHERE is_active = 1 ORDER BY studio_id") - return cursor.fetchall() - except Exception as e: - logger.error(f"Error loading studios: {e}") - return [] - -#===========ОПРЕДЕЛЕНИЕ РОЛИ ПО БД========================== -def get_user_role(username: str) -> str: - #Получение роли пользователя из базы данных - if not username: - return 'student' - - try: - with get_connection() as conn: - cursor = conn.cursor() - # Ищем username с @ (как хранится в БД) - cursor.execute("SELECT role FROM users WHERE username = ?", (f"{username}",)) - result = cursor.fetchone() - return result[0] if result else 'student' - except Exception as e: - logger.error(f"Database error getting role: {e}") - return 'student' - - -@bot.message_handler(commands=['start']) -def start(message): - """Главный обработчик команды /start с распределением по ролям""" - username = message.from_user.username - - # Получаем роль из базы данных - role = get_user_role(username) - print(username) - print("Role:",role) - # Распределение по ролям - if role == 'admin': - try: - from Admin import admin_start - admin_start(message) - return - except ImportError as e: - logger.error(f"Error importing Admin: {e}") - bot.send_message(message.chat.id, "Модуль администратора недоступен") - except Exception as e: - logger.error(f"Error in admin_start: {e}") - bot.send_message(message.chat.id, "Ошибка в модуле администратора") - - elif role == 'head': - try: - from Head_studio import head_start - head_start( message) - return - except ImportError as e: - logger.error(f"Error importing Head_of_studio: {e}") - bot.send_message(message.chat.id, "Модуль руководителя недоступен") - except Exception as e: - logger.error(f"Error in head_start: {e}") - bot.send_message(message.chat.id, "Ошибка в модуле руководителя") - - # Для user или если пользователя нет в базе - user_start(message) - - -#==============ФУНКЦИОНАЛ СТАНДАРТНОГО ПОЛЬЗОВАТЕЛЯ========================= -def user_start(message): - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn1 = types.KeyboardButton('Каталог студий') - markup.row(btn1) - btn2 = types.KeyboardButton('Мои заявки') - btn3 = types.KeyboardButton('Подать заявку') - markup.row(btn2, btn3) - - bot.send_message( - message.chat.id, - f'Привет, в этом боте ты можешь узнать про творческие студии' - f' ГУАП и стать частью активной студенческой жизни!', - parse_mode='html', - reply_markup=markup - ) - -# ВЕТКА КАТАЛОГ СТУДИЙ - -@bot.message_handler(func=lambda message: message.text == 'Каталог студий') -def inline_button_catalog(message): - studios = load_active_studios() - - if not studios: - bot.send_message(message.chat.id, 'На данный момент активных студий нет.') - return - - markup = types.InlineKeyboardMarkup() - for studio_id, name in studios: - btn = types.InlineKeyboardButton(name, callback_data=f'info:{studio_id}') - markup.row(btn) - - bot.send_message(message.chat.id, 'Нажми на студию для просмотра:', reply_markup=markup) - -@bot.callback_query_handler(func=lambda call: call.data.startswith('info:')) -def show_studio_info(call): - """Показывает информацию о студии""" - studio_id = int(call.data.split(':')[1]) - - try: - with get_connection() as conn: - cursor = conn.cursor() - cursor.execute(""" - SELECT name, description, contacts - FROM studios - WHERE studio_id = ? - """, (studio_id,)) - result = cursor.fetchone() - - if result: - name, description, contacts = result - text = f"{name}\n\n{description or 'Описание скоро появится.'}" - - # Добавляем кнопку подачи заявки - markup = types.InlineKeyboardMarkup() - btn_apply = types.InlineKeyboardButton("📝 Подать заявку", callback_data=f'apply:{studio_id}') - markup.row(btn_apply) - - bot.edit_message_text( - text, - - call.message.chat.id, - call.message.message_id, - parse_mode='html', - reply_markup=markup - ) - else: - bot.answer_callback_query(call.id, "Студия не найдена") - except Exception as e: - logger.error(f"Error showing studio info: {e}") - bot.answer_callback_query(call.id, "Ошибка при загрузке информации") - -# Хранение состояний пользователей -user_states = {} - -class QuestionnaireHandler: - """Обработчик анкет с автозаполнением""" - - def __init__(self, db_connection): - pass - - def start_questionnaire(self, call): - """Начало анкеты для выбранной студии""" - studio_id = int(call.data.split(':')[1]) - user_id = call.from_user.id - - # Проверяем, существует ли такая студия в конфигурации - studio_config = QuestionnaireBuilder.get_questionnaire_for_studio(studio_id) - if not studio_config: - bot.answer_callback_query(call.id, f"Анкета для этой студии не настроена") - return - - # Сохраняем данные в состоянии пользователя - user_states[user_id] = { - 'state': 'questionnaire', - 'studio_id': studio_id, - 'studio_name': studio_config.get('name', 'Студия'), - 'current_question_index': 0, - 'answers': {}, - 'questionnaire': studio_config # Сохраняем конфигурацию студии - } - - # Получаем данные пользователя для автозаполнения - user_data = self.get_user_data(user_id) - user_states[user_id]['user_data'] = user_data - - # Проверяем возможность автозаполнения - if QuestionnaireBuilder.can_autofill(studio_id, user_data): - self.offer_autofill(call, studio_id, user_data) - else: - self.start_manual_filling(call, studio_id) - - def get_user_data(self, user_id: int) -> Dict[str, Any]: - """Получает данные пользователя из БД для автозаполнения""" - try: - with get_connection() as conn: - cursor = conn.cursor() - cursor.execute(""" - SELECT full_name, student_group, phone_number, email - FROM users - WHERE telegram_id = ? - """, (user_id,)) - result = cursor.fetchone() - return { - 'full_name': result[0] or '', - 'student_group': result[1] or '', - 'phone_number': result[2] or '', - 'email': result[3] or '' - } if result else {} - except Exception as e: - logger.error(f"Error getting user data: {e}") - return {} - - def offer_autofill(self, call, studio_id: int, user_data: Dict[str, Any]): - """Предлагает автозаполнение""" - # Получаем поля для автозаполнения - autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) - autofill_info = [] - - for field in autofill_fields: - value = user_data.get(field, '') - if value: - field_name = { - 'full_name': 'ФИО', - 'student_group': 'Группа', - 'phone_number': 'Телефон', - 'email': 'Email' - }.get(field, field) - autofill_info.append(f"{field_name}: {value}") - - studio_name = QuestionnaireBuilder.get_studio_name(studio_id) - message = f"*Анкета для студии: {studio_name}*\n\n" - - if autofill_info: - message += "Найдены ваши данные из предыдущих анкет:\n" - for info in autofill_info: - message += f"• {info}\n" - message += "\nХотите использовать их для автозаполнения?" - else: - message += "Хотите использовать основную информацию из предыдущих анкет для автозаполнения?"#### - - markup = types.InlineKeyboardMarkup() - markup.row( - types.InlineKeyboardButton("✅ Использовать мои данные", callback_data='use_autofill'), - types.InlineKeyboardButton("✏️ Заполнить вручную", callback_data='manual_fill') - ) - - bot.edit_message_text( - message, - call.message.chat.id, - call.message.message_id, - parse_mode='Markdown', - reply_markup=markup - ) - - def start_manual_filling(self, call, studio_id: int): - """Начинает ручное заполнение анкеты""" - user_id = call.from_user.id - studio_name = QuestionnaireBuilder.get_studio_name(studio_id) - - # Обновляем состояние - user_states[user_id]['state'] = 'answering' - - bot.edit_message_text( - f"Начинаем заполнение анкеты для студии *{studio_name}*.\n\n" - f"Пожалуйста, ответьте на несколько вопросов:", - call.message.chat.id, - call.message.message_id, - parse_mode='Markdown' - ) - - # Задаем первый вопрос - self.ask_next_question(call.message.chat.id, user_id) - - def ask_next_question(self, chat_id: int, user_id: int): - """Задает следующий вопрос""" - if user_id not in user_states: - return - - state = user_states[user_id] - studio_id = state['studio_id'] - question_ids = QuestionnaireBuilder.get_question_sequence(studio_id) - current_index = state.get('current_question_index', 0) - answers = state.get('answers', {}) - - # Пропускаем уже заполненные вопросы - while current_index < len(question_ids): - qid = question_ids[current_index] - if qid not in answers: - break - current_index += 1 - - # Проверяем, не заполнили ли мы все вопросы - if current_index >= len(question_ids): - self.show_summary(chat_id, user_id) - return - - # Сохраняем текущий индекс - user_states[user_id]['current_question_index'] = current_index - user_states[user_id]['current_question_id'] = qid - - # Получаем текст вопроса - print(qid) - question_text = get_question_handler(qid) - - # Отправляем вопрос - bot.send_message(chat_id, question_text) - - def process_answer(self, message): - """Обработка ответа на вопрос""" - user_id = message.from_user.id - - if user_id not in user_states: - return - - state = user_states[user_id] - if state.get('state') != 'answering': - return - - current_qid = state.get('current_question_id') - answer_text = message.text.strip() - - if not current_qid: - return - - # Получаем объект вопроса для валидации - q_obj = get_question_obj(current_qid) - - if q_obj: - is_valid, error_msg = q_obj.validate(answer_text) - if not is_valid: - bot.send_message(message.chat.id, f"❌ {error_msg}\n\nПопробуйте еще раз:") - return - - # Сохраняем ответ - user_states[user_id]['answers'][current_qid] = answer_text - - # Переходим к следующему вопросу - user_states[user_id]['current_question_index'] += 1 - - # Задаем следующий вопрос - self.ask_next_question(message.chat.id, user_id) - - def show_summary(self, chat_id: int, user_id: int): - """Показывает сводку по ответам""" - if user_id not in user_states: - return - - state = user_states[user_id] - studio_id = state['studio_id'] - answers = state.get('answers', {}) - - # Валидируем ответы - is_valid, errors = QuestionnaireBuilder.validate_answers(studio_id, answers) - - if not is_valid: - error_text = "❌ *Ошибки в заполнении:*\n\n" - for qid, error in errors.items(): - q_obj = get_question_obj(qid) - error_text += f"• {q_obj.text if q_obj else qid}: {error}\n" - - error_text += "\nПожалуйста, заполните анкету заново." - - bot.send_message(chat_id, error_text, parse_mode='Markdown') - - # Сбрасываем анкету - state['current_question_index'] = 0 - state['answers'] = {} - - # Запускаем заново - self.ask_next_question(chat_id, user_id) - return - - # Генерируем сводку - summary = QuestionnaireBuilder.generate_summary(studio_id, answers) - - # Создаем клавиатуру - markup = types.InlineKeyboardMarkup() - markup.row( - types.InlineKeyboardButton("✅ Подтвердить и отправить", callback_data='confirm_application'), - types.InlineKeyboardButton("✏️ Заполнить заново", callback_data='restart_questionnaire') - ) - markup.row( - types.InlineKeyboardButton("🚫 Отменить", callback_data='cancel_to_catalog') - ) - - bot.send_message( - chat_id, - f"{summary}\n\n*Всё верно? Подтвердите отправку заявки или внесите изменения.*", - parse_mode='Markdown', - reply_markup=markup - ) - - # Обновляем состояние - user_states[user_id]['state'] = 'confirmation' - - -# Инициализируем обработчик -questionnaire_handler = QuestionnaireHandler(None) - -@bot.callback_query_handler(func=lambda call: call.data.startswith('apply:')) -def handle_apply(call): - """Обработка нажатия кнопки 'Подать заявку'""" - questionnaire_handler.start_questionnaire(call) - -@bot.callback_query_handler(func=lambda call: call.data in ['use_autofill', 'manual_fill']) -def handle_autofill_choice(call): - """Обработка выбора автозаполнения""" - user_id = call.from_user.id - studio_id = user_states[user_id]['studio_id'] - - if call.data == 'use_autofill': - # Автозаполняем данные - user_data = user_states[user_id]['user_data'] - autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) - - for field in autofill_fields: - value = user_data.get(field) - if value: - # Находим ID вопроса, соответствующего этому полю - questions = QuestionnaireBuilder.get_question_objects(studio_id) - for q in questions: - if hasattr(q, 'autofill_field') and q.autofill_field == field: - user_states[user_id]['answers'][q.id] = value - break - - bot.edit_message_text( - "✅ Данные автозаполнены.\nПродолжаем с оставшимися вопросами...", - call.message.chat.id, - call.message.message_id - ) - - # Начинаем опрос - user_states[user_id]['state'] = 'answering' - questionnaire_handler.ask_next_question(call.message.chat.id, user_id) - -@bot.callback_query_handler(func=lambda call: call.data in ['use_autofill', 'manual_fill']) -def handle_autofill_choice(call): - """Обработка выбора автозаполнения""" - user_id = call.from_user.id - studio_id = user_states[user_id]['studio_id'] - - if call.data == 'use_autofill': - # Автозаполняем данные - user_data = user_states[user_id]['user_data'] - autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) - - for field in autofill_fields: - value = user_data.get(field) - if value: - # Находим ID вопроса, соответствующего этому полю - questions = QuestionnaireBuilder.get_question_objects(studio_id) - for q in questions: - if hasattr(q, 'autofill_field') and q.autofill_field == field: - user_states[user_id]['answers'][q.id] = value - break - - bot.edit_message_text( - "✅ Данные автозаполнены.\nПродолжаем с оставшимися вопросами...", - call.message.chat.id, - call.message.message_id - ) - - # Начинаем опрос - user_states[user_id]['state'] = 'answering' - questionnaire_handler.ask_next_question(call.message.chat.id, user_id) - - -def save_application_to_db(telegram_id: int, studio_id: int, answers: dict, user_first_name: str = None, - user_last_name: str = None): - """Сохраняет заявку в БД""" - try: - with get_connection() as conn: - cursor = conn.cursor() - - # 1. Получаем или создаем пользователя - cursor.execute("SELECT user_id FROM users WHERE telegram_id = ?", (telegram_id,)) - user_result = cursor.fetchone() - - if user_result: - user_id = user_result[0] - else: - # Получаем ФИО из ответов - full_name = answers.get('full_name') - - # Если нет в ответах, используем имя из Telegram - if not full_name and user_first_name: - full_name = f"{user_first_name or ''} {user_last_name or ''}".strip() - if not full_name: - full_name = "Не указано" - elif not full_name: - full_name = "Не указано" - - # Создаем нового пользователя - cursor.execute(""" - INSERT INTO users (telegram_id, full_name, created_at) - VALUES (?, ?, ?) - """, (telegram_id, full_name, datetime.now())) - user_id = cursor.lastrowid - - # 2. Генерируем сводку - summary = QuestionnaireBuilder.generate_summary(studio_id, answers) - - # 3. Сохраняем заявку - cursor.execute(""" - INSERT INTO applications (user_id, studio_id, summary, status, created_at) - VALUES (?, ?, ?, ?, ?) - """, (user_id, studio_id, summary, 'pending', datetime.now())) - - application_id = cursor.lastrowid - - # 4. Сохраняем ответы на каждый вопрос (опционально) - for question_id, answer in answers.items(): - q_obj = get_question_obj(question_id) - question_text = q_obj.text if q_obj else question_id - - cursor.execute(""" - INSERT INTO application_answers - (application_id, question_id, question_text, answer_text) - VALUES (?, ?, ?, ?) - """, (application_id, question_id, question_text, answer)) - - conn.commit() - return application_id - - except Exception as e: - logger.error(f"Error saving application: {e}") - return None - -@bot.callback_query_handler(func=lambda call: call.data in [ - 'confirm_application', 'restart_questionnaire', 'cancel_to_catalog', 'go_to_main_menu' -]) -def handle_confirmation_actions(call): - """Обработка действий подтверждения""" - user_id = call.from_user.id - - if user_id not in user_states: - bot.answer_callback_query(call.id, "Сессия устарела") - return - - state = user_states[user_id] - studio_id = state['studio_id'] - answers = state.get('answers', {}) - - if call.data == 'confirm_application': - application_id = save_application_to_db( - telegram_id=user_id, - studio_id=studio_id, - answers=answers, - user_first_name=call.from_user.first_name, - user_last_name=call.from_user.last_name - ) - - if application_id: - bot.answer_callback_query(call.id, "✅ Заявка отправлена") - - markup = types.InlineKeyboardMarkup() - markup.row(types.InlineKeyboardButton("🏠 Главное меню", callback_data='go_to_main_menu')) - - bot.edit_message_text( - "✅ Ваша заявка отправлена успешно!", - call.message.chat.id, - call.message.message_id, - reply_markup=markup - ) - else: - bot.answer_callback_query(call.id, "❌ Ошибка при сохранении") - - bot.edit_message_text( - "❌ Произошла ошибка при сохранении заявки. Попробуйте позже.", - call.message.chat.id, - call.message.message_id - ) - - # Очищаем состояние - del user_states[user_id] - - elif call.data == 'restart_questionnaire': - state['current_question_index'] = 0 - state['answers'] = {} - state['state'] = 'answering' - - bot.edit_message_text( - "Начинаем заполнение анкеты заново...", - call.message.chat.id, - call.message.message_id - ) - - import time - time.sleep(1) - - questionnaire_handler.ask_next_question(call.message.chat.id, user_id) - - elif call.data == 'cancel_to_catalog': - if user_id in user_states: - del user_states[user_id] - - # Загружаем студии и показываем каталог - studios = load_active_studios() - if studios: - markup = types.InlineKeyboardMarkup() - for studio_id, name in studios: - btn = types.InlineKeyboardButton(name, callback_data=f'info:{studio_id}') - markup.row(btn) - - bot.edit_message_text( - "Заполнение анкеты отменено.\n\nВыберите студию:", - call.message.chat.id, - call.message.message_id, - reply_markup=markup - ) - else: - bot.edit_message_text( - "Заполнение анкеты отменено.", - call.message.chat.id, - call.message.message_id - ) - - elif call.data == 'go_to_main_menu': - if user_id in user_states: - del user_states[user_id] - - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn1 = types.KeyboardButton('Каталог студий') - markup.row(btn1) - btn2 = types.KeyboardButton('Мои заявки') - btn3 = types.KeyboardButton('Подать заявку') - markup.row(btn2, btn3) - - bot.edit_message_text( - "Вы вернулись в главное меню.", - call.message.chat.id, - call.message.message_id, - reply_markup=None - ) - - bot.send_message( - call.message.chat.id, - f'Привет! В этом боте ты можешь узнать про творческие студии' - f' ГУАП и стать частью активной студенческой жизни!', - parse_mode='html', - reply_markup=markup - ) - - -# Обработка текстовых сообщений -@bot.message_handler(func=lambda message: True, content_types=['text']) -def handle_all_text_messages(message): - """Обработка всех текстовых сообщений""" - user_id = message.from_user.id - username = message.from_user.username - # Проверяем, находится ли пользователь в процессе заполнения анкеты - if user_id in user_states and user_states[user_id].get('state') == 'answering': - questionnaire_handler.process_answer(message) - return - - if get_user_role(username) == "admin": - from Admin import handle_admin_message - handle_admin_message(message) - - # Обработка других команд - if message.text == 'Мои заявки': - bot.send_message(message.chat.id, "Функция 'Мои заявки' в разработке") - elif message.text == 'Подать заявку': - inline_button_catalog(message) - else: - bot.send_message(message.chat.id, "Используйте кнопки меню или команду /start") - - - -# Запуск бота -if __name__ == '__main__': - print("Бот запущен...") - bot.polling(none_stop=True) - - diff --git a/.venv/usernew.py b/.venv/usernew.py new file mode 100644 index 0000000..e5fd317 --- /dev/null +++ b/.venv/usernew.py @@ -0,0 +1,202 @@ +import telebot +from telebot import types +import logging + +from auth import get_user_role +from database import load_active_studios +from questionnaire_flow import QuestionnaireFlow +from catalog import show_studio_catalog, show_studio_info +from users_collector import QuestionnaireBuilder +from questions import get_question_obj + + + +# Настройка логирования +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +# Инициализация компонентов +questionnaire_flow = QuestionnaireFlow(bot) + +# Инициализация модуля администратора +from Admin import initialize as init_admin + +init_admin(bot) + + +@bot.message_handler(commands=['start']) +def start(message): + """Главный обработчик команды /start""" + username = message.from_user.username + role = get_user_role(username) + + if role == 'admin': + from Admin import admin_start + admin_start(message) + return + elif role == 'head': + from head_of_studio import handle_head_start + handle_head_start(message) + return + + # Для обычных пользователей + user_start(message) + + +def user_start(message): + """Старт для обычного пользователя""" + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + btn3 = types.KeyboardButton('Подать заявку') + markup.row(btn2, btn3) + + bot.send_message( + message.chat.id, + 'Привет, в этом боте ты можешь узнать про творческие студии' + ' ГУАП и стать частью активной студенческой жизни!', + parse_mode='html', + reply_markup=markup + ) + + +@bot.message_handler(func=lambda message: message.text == 'Каталог студий') +def handle_catalog(message): + """Показ каталога студий""" + show_studio_catalog(bot, message.chat.id) + + +@bot.message_handler(func=lambda message: message.text == 'Подать заявку') +def handle_apply_from_menu(message): + """Подать заявку из меню""" + show_studio_catalog(bot, message.chat.id) + + +@bot.callback_query_handler(func=lambda call: call.data.startswith('info:')) +def handle_studio_info(call): + """Показ информации о студии""" + show_studio_info(bot, call) + + +@bot.callback_query_handler(func=lambda call: call.data.startswith('apply:')) +def handle_apply(call): + """Начало заполнения анкеты""" + studio_id = int(call.data.split(':')[1]) + questionnaire_flow.start_questionnaire(call, studio_id) + + +@bot.callback_query_handler(func=lambda call: call.data in ['use_autofill', 'manual_fill']) +def handle_autofill_choice(call): + """Обработка выбора автозаполнения""" + user_id = call.from_user.id + + if user_id not in questionnaire_flow.user_states: + bot.answer_callback_query(call.id, "Сессия устарела") + return + + state = questionnaire_flow.user_states[user_id] + studio_id = state['studio_id'] + + if call.data == 'use_autofill': + user_data = state['user_data'] + logger.info(f"Данные пользователя для автозаполнения: {user_data}") + + # Получаем все вопросы для этой студии + questions = QuestionnaireBuilder.get_question_objects(studio_id) + + for q in questions: + if q.autofill_field and q.autofill_field in user_data: + value = user_data[q.autofill_field] + if value and str(value).strip(): + # Сохраняем значение в ответы + state['answers'][q.id] = str(value) + logger.info(f"Автозаполнение: {q.id} = {value}") + + bot.edit_message_text( + "✅ Данные автозаполнены.\nПродолжаем с оставшимися вопросами...", + call.message.chat.id, + call.message.message_id + ) + + # Начинаем опрос + state['state'] = 'answering' + questionnaire_flow._ask_next_question(call.message.chat.id, user_id) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('answer:')) +def handle_select_answer(call): + """Обработка выбора из инлайн-клавиатуры""" + if questionnaire_flow.user_states.get(call.from_user.id, {}).get('state') == 'answering': + questionnaire_flow._handle_select_answer(call) + +@bot.message_handler(func=lambda message: True, content_types=['text']) +def handle_all_text_messages(message): + """Обработка всех текстовых сообщений""" + user_id = message.from_user.id + username = message.from_user.username + + # Проверяем, находится ли пользователь в процессе заполнения анкеты + if user_id in questionnaire_flow.user_states and questionnaire_flow.user_states[user_id].get( + 'state') == 'answering': + questionnaire_flow.process_answer(message) + return + + role = get_user_role(username) + + if role == "admin": + from Admin import handle_admin_message + handle_admin_message(message) + return + + # Обработка других команд + if message.text == 'Мои заявки': + bot.send_message(message.chat.id, "Функция 'Мои заявки' в разработке") + else: + bot.send_message(message.chat.id, "Используйте кнопки меню или команду /start") + + +@bot.callback_query_handler(func=lambda call: call.data == 'confirm_application') +def handle_confirm_application(call): + """Обработка подтверждения заявки""" + if call.from_user.id in questionnaire_flow.user_states: + questionnaire_flow._confirm_application(call) + + +@bot.callback_query_handler(func=lambda call: call.data == 'restart_questionnaire') +def handle_restart_questionnaire(call): + """Обработка перезаполнения анкеты""" + user_id = call.from_user.id + if user_id in questionnaire_flow.user_states: + state = questionnaire_flow.user_states[user_id] + studio_id = state['studio_id'] + + # Начинаем заново + questionnaire_flow.start_questionnaire(call, studio_id) + +# В самом конце файла, перед запуском бота, добавьте: + +@bot.callback_query_handler(func=lambda call: call.data.startswith('select_answer:')) +def handle_select_answer(call): + """Обработка выбора из inline-клавиатуры для select вопросов""" + questionnaire_flow.process_select_answer(call) + +@bot.callback_query_handler(func=lambda call: call.data == 'confirm_application') +def handle_confirm_application(call): + """Обработка подтверждения заявки""" + questionnaire_flow._confirm_application(call) + +@bot.callback_query_handler(func=lambda call: call.data == 'restart_questionnaire') +def handle_restart_questionnaire(call): + """Обработка перезаполнения анкеты""" + questionnaire_flow._restart_questionnaire(call) + + +# Запуск бота +if __name__ == '__main__': + print("Бот запущен...") + bot.polling(none_stop=True) \ No newline at end of file diff --git a/.venv/userrrrrr.py b/.venv/userrrrrr.py new file mode 100644 index 0000000..2af70c7 --- /dev/null +++ b/.venv/userrrrrr.py @@ -0,0 +1,216 @@ +import telebot +from telebot import types +import logging + +from auth import get_user_role +from database import load_active_studios +from questionnaire_flow import QuestionnaireFlow +from catalog import show_studio_catalog, show_studio_info +from users_collector import QuestionnaireBuilder +from questions import get_question_obj +from applications_manager import ApplicationsManager + + +# Настройка логирования +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +# Инициализация компонентов +questionnaire_flow = QuestionnaireFlow(bot) +applications_manager = ApplicationsManager(bot) + +# Инициализация модуля администратора +from Admin import initialize as init_admin + +init_admin(bot) + + +@bot.message_handler(commands=['start']) +def start(message): + """Главный обработчик команды /start""" + username = message.from_user.username + role = get_user_role(username) + + if role == 'admin': + from Admin import admin_start + admin_start(message) + return + elif role == 'head': + from head_of_studio import handle_head_start + handle_head_start(message) + return + + # Для обычных пользователей + user_start(message) + + +def user_start(message): + """Старт для обычного пользователя""" + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + markup.row(btn2) + + bot.send_message( + message.chat.id, + 'Привет, в этом боте ты можешь узнать про творческие студии' + ' ГУАП и стать частью активной студенческой жизни!', + parse_mode='html', + reply_markup=markup + ) + +def create_main_user_menu(): + """Создает главное меню пользователя""" + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + markup.row(btn2) + return markup + + +@bot.message_handler(func=lambda message: message.text == 'Каталог студий') +def handle_catalog(message): + """Показ каталога студий""" + show_studio_catalog(bot, message.chat.id) + +@bot.message_handler(func=lambda message: message.text == 'Мои заявки') +def handle_my_applications(message): + """Показ заявок пользователя""" + user_id = message.from_user.id + applications_manager.show_user_applications(message.chat.id, user_id) + + +@bot.message_handler(func=lambda message: message.text == 'Главное меню') +def handle_main_menu_return(message): + """Возврат в главное меню""" + user_start(message) + + +@bot.callback_query_handler(func=lambda call: call.data.startswith('info:')) +def handle_studio_info(call): + """Показ информации о студии""" + show_studio_info(bot, call) + + +@bot.callback_query_handler(func=lambda call: call.data.startswith('apply:')) +def handle_apply(call): + """Начало заполнения анкеты""" + studio_id = int(call.data.split(':')[1]) + questionnaire_flow.start_questionnaire(call, studio_id) + + +@bot.callback_query_handler(func=lambda call: call.data in ['use_autofill', 'manual_fill']) +def handle_autofill_choice(call): + """Обработка выбора автозаполнения""" + user_id = call.from_user.id + + if user_id not in questionnaire_flow.user_states: + bot.answer_callback_query(call.id, "Сессия устарела") + return + + state = questionnaire_flow.user_states[user_id] + studio_id = state['studio_id'] + + if call.data == 'use_autofill': + user_data = state['user_data'] + logger.info(f"Данные пользователя для автозаполнения: {user_data}") + + # Получаем все вопросы для этой студии + questions = QuestionnaireBuilder.get_question_objects(studio_id) + + for q in questions: + if q.autofill_field and q.autofill_field in user_data: + value = user_data[q.autofill_field] + if value and str(value).strip(): + # Сохраняем значение в ответы + state['answers'][q.id] = str(value) + logger.info(f"Автозаполнение: {q.id} = {value}") + + bot.edit_message_text( + "✅ Данные автозаполнены.\nПродолжаем с оставшимися вопросами...", + call.message.chat.id, + call.message.message_id + ) + + # Начинаем опрос + state['state'] = 'answering' + questionnaire_flow._ask_next_question(call.message.chat.id, user_id) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('answer:')) +def handle_select_answer(call): + """Обработка выбора из инлайн-клавиатуры""" + if questionnaire_flow.user_states.get(call.from_user.id, {}).get('state') == 'answering': + questionnaire_flow._handle_select_answer(call) + +@bot.message_handler(func=lambda message: True, content_types=['text']) +def handle_all_text_messages(message): + """Обработка всех текстовых сообщений""" + user_id = message.from_user.id + username = message.from_user.username + + # Проверяем, находится ли пользователь в процессе заполнения анкеты + if user_id in questionnaire_flow.user_states and questionnaire_flow.user_states[user_id].get( + 'state') == 'answering': + questionnaire_flow.process_answer(message) + return + + role = get_user_role(username) + + if role == "admin": + from Admin import handle_admin_message + handle_admin_message(message) + return + + # Обработка других команд + if message.text == 'Мои заявки': + bot.send_message(message.chat.id, "Функция 'Мои заявки' в разработке") + else: + bot.send_message(message.chat.id, "Используйте кнопки меню или команду /start") + + +@bot.callback_query_handler(func=lambda call: call.data == 'confirm_application') +def handle_confirm_application(call): + """Обработка подтверждения заявки""" + if call.from_user.id in questionnaire_flow.user_states: + questionnaire_flow._confirm_application(call) + + # После отправки заявки показываем главное меню + markup = create_main_user_menu() + bot.send_message( + call.message.chat.id, + "✅ Ваша заявка успешно отправлена!\n\n" + "Вы можете:\n" + "• Просмотреть свои заявки в разделе 'Мои заявки'\n" + "• Изучить другие студии в каталоге", + parse_mode='HTML', + reply_markup=markup + ) + + +@bot.callback_query_handler(func=lambda call: call.data == 'restart_questionnaire') +def handle_restart_questionnaire(call): + """Обработка перезаполнения анкеты""" + user_id = call.from_user.id + if user_id in questionnaire_flow.user_states: + state = questionnaire_flow.user_states[user_id] + studio_id = state['studio_id'] + + questionnaire_flow.start_questionnaire(call, studio_id) + + +@bot.callback_query_handler(func=lambda call: call.data.startswith('select_answer:')) +def handle_select_answer_callback(call): + questionnaire_flow.process_select_answer(call) + + +# Запуск бота +if __name__ == '__main__': + print("Бот запущен...") + bot.polling(none_stop=True) \ No newline at end of file diff --git a/.venv/users_collector.py b/.venv/users_collector.py index 928f8dd..891ccec 100644 --- a/.venv/users_collector.py +++ b/.venv/users_collector.py @@ -1,214 +1,293 @@ from typing import Dict, List, Any, Optional, Callable -from questions import get_question_handler, get_question_obj, ALL_QUESTIONS +from questions import get_question_obj + class QuestionnaireBuilder: """Сборщик анкет для разных студий""" - + STUDIO_QUESTIONNAIRES = { - # ID студии: [список ID вопросов из ALL_QUESTIONS] - 1: { 'name': 'Танцевальная студия', + 1: { + 'name': 'Танцевальная студия', 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone_number', # 3. Телефон - 'email', # 4. Email - 'project_ideas', # 5. Стиль танца - 'experience', # 6. Опыт - 'motivation', # 7. Мотивация - 'activeness', # 8. Участие в мероприятиях + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'project_ideas', # 5. Идеи для проектов + 'experience', # 6. Опыт + 'motivation', + 'activeness', # 8. Участие в мероприятиях ] }, - 6: {'name': 'КВН', + 6: { + 'name': 'КВН', 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone_number', # 3. Телефон - 'email', # 4. Email - 'experience', - 'quot', - 'public_speakin', + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'experience', # 5. Опыт + 'quote', # 6. Цитата (ИСПРАВЛЕНО: было 'quot') + 'public_speaking', # 7. Публичные выступления (ИСПРАВЛЕНО: было 'public_speakin') ] }, - 2: {'name': 'Театральная студия', + 2: { + 'name': 'Театральная студия', 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone_number', # 3. Телефон - 'email', # 5. Театральные роли - 'experience', # 6. Опыт - 'motivation', # 7. Мотивация - 'improvisation', - 'feedback_source', + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'experience', # 5. Опыт + 'motivation', # 6. Мотивация + 'improvisation', # 7. Импровизация + 'feedback_source', # 8. Источник информации ] }, - 4: {'name': 'МУЗГУАП', + 4: { + 'name': 'МУЗГУАП', 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone_number', # 3. Телефон - 'email', # 4. Email - 'music_instrument', # 5. Музыкальный инструмент - 'singing_experience', # 6. Опыт пения - 'public_speakin', - 'activeness', + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'music_instrument', # 5. Музыкальный инструмент + 'singing_experience', # 6. Опыт пения + 'public_speaking', # 7. Публичные выступления (ИСПРАВЛЕНО) + 'activeness', # 8. Участие в мероприятиях ] }, - 5: {'name': 'Медиацентр', + 5: { + 'name': 'Медиацентр', 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone_number', # 3. Телефон - 'email', # 4. Email - 'media', - 'experience', - 'project_ideas', - 'time_commitment', + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'media', # 5. Направление медиа + 'experience', # 6. Опыт + 'project_ideas', # 7. Идеи для проектов ] }, - 3: {'name': 'Студия технического обеспечения мероприятий', + 3: { + 'name': 'Студия технического обеспечения мероприятий', 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone_number', # 3. Телефон - 'email', # 4. Email - 'tech_skills', # 5. Технические навыки - 'organizational_exp', - 'leadership', - 'feedback_source', + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'tech_skills', # 5. Технические навыки + 'organizational_exp', # 6. Организационный опыт + 'leadership', # 7. Лидерские качества + 'feedback_source', # 8. Источник информации ] }, - 7: {'name': 'Студия организации мероприятий', + 7: { + 'name': 'Студия организации мероприятий', 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone_number', # 3. Телефон - 'email', # 4. Email - 'organizational_exp', - 'motivation', - 'activeness', - 'tech_skills', + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'organizational_exp', # 5. Организационный опыт + 'motivation', # 6. Мотивация + 'activeness', # 7. Участие в мероприятиях + 'tech_skills', # 8. Технические навыки ] }, - 8: {'name': 'Студия ведущих', + 8: { + 'name': 'Студия ведущих', 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone_number', # 3. Телефон - 'email', # 4. Email - 'experience', # 6. Идеи для проектов - 'public_speakin', - 'quot', - 'improvisation', + 'full_name', # 1. ФИО + 'group', # 2. Группа + 'phone_number', # 3. Телефон + 'email', # 4. Email + 'experience', # 5. Опыт + 'public_speaking', # 6. Публичные выступления (ИСПРАВЛЕНО) + 'quote', # 7. Цитата (ИСПРАВЛЕНО: было 'quot') + 'improvisation', # 8. Импровизация ] }, } - + @classmethod def get_questionnaire_for_studio(cls, studio_id: int) -> Optional[Dict[str, Any]]: - """Получает конфигурацию анкеты для студии""" return cls.STUDIO_QUESTIONNAIRES.get(studio_id) - + @classmethod def get_studio_name(cls, studio_id: int) -> str: - """Получает название студии""" config = cls.get_questionnaire_for_studio(studio_id) return config.get('name', 'Неизвестная студия') if config else 'Неизвестная студия' - + @classmethod def get_question_sequence(cls, studio_id: int) -> List[str]: - """Получает последовательность вопросов для студии""" config = cls.get_questionnaire_for_studio(studio_id) return config.get('questions', []) if config else [] - - @classmethod - def build_questionnaire_handlers(cls, studio_id: int) -> List[Callable]: - """Собирает список обработчиков вопросов для студии""" - question_ids = cls.get_question_sequence(studio_id) - handlers = [] - - for qid in question_ids: - handler = get_question_handler(qid) - if handler: - handlers.append(handler) - - return handlers - + @classmethod def get_question_objects(cls, studio_id: int) -> List[Any]: - """Получает объекты вопросов для студии""" question_ids = cls.get_question_sequence(studio_id) questions = [] - + for qid in question_ids: q_obj = get_question_obj(qid) if q_obj: questions.append(q_obj) - + return questions - + @classmethod def get_autofill_fields(cls, studio_id: int) -> List[str]: - """Получает список полей для автозаполнения""" questions = cls.get_question_objects(studio_id) autofill_fields = [] - + for q in questions: if q.autofill_field: autofill_fields.append(q.autofill_field) - - # Убираем дубликаты + return list(set(autofill_fields)) - #У нас каждый раз новая анкета, зачем автозаполнение?? @classmethod def can_autofill(cls, studio_id: int, user_data: Dict[str, Any]) -> bool: - """Проверяет, можно ли автозаполнить анкету""" autofill_fields = cls.get_autofill_fields(studio_id) - + if not autofill_fields: return False - - # Проверяем, сколько полей можно автозаполнить - fillable_count = 0 + for field in autofill_fields: value = user_data.get(field, '') if value and str(value).strip(): - fillable_count += 1 - - return fillable_count == len(autofill_fields) - + return True + + return False + @classmethod def generate_summary(cls, studio_id: int, answers: Dict[str, str]) -> str: """Генерирует сводку по ответам""" - questions = cls.get_question_objects(studio_id) studio_name = cls.get_studio_name(studio_id) - - lines = [f"📋 *Анкета для студии: {studio_name}*\n"] - - for i, q in enumerate(questions, 1): - answer = answers.get(q.id, "❌ Не заполнено") - lines.append(f"\n{i}. *{q.text}*\n{answer}") - - return "\n".join(lines) - - @classmethod - def validate_answers(cls, studio_id: int, answers: Dict[str, str]) -> tuple[bool, Dict[str, str]]: - """Валидирует все ответы анкеты""" - questions = cls.get_question_objects(studio_id) - errors = {} - - for q in questions: - answer = answers.get(q.id, '').strip() - - if q.is_required and not answer: - errors[q.id] = "Обязательный вопрос не заполнен" - continue - - is_valid, error_msg = q.validate(answer) - if not is_valid: - errors[q.id] = error_msg - - return len(errors) == 0, errors + summary = f"Заявка в студию: {studio_name}\n\n" + + for qid, answer in answers.items(): + q_obj = get_question_obj(qid) + question_text = q_obj.text if q_obj else qid + summary += f"❓ {question_text}\n" + summary += f"📝 {answer}\n\n" + + return summary + + + + + + + + + # @classmethod + # def get_questionnaire_for_studio(cls, studio_id: int) -> Optional[Dict[str, Any]]: + # """Получает конфигурацию анкеты для студии""" + # return cls.STUDIO_QUESTIONNAIRES.get(studio_id) + # + # @classmethod + # def get_studio_name(cls, studio_id: int) -> str: + # """Получает название студии""" + # config = cls.get_questionnaire_for_studio(studio_id) + # return config.get('name', 'Неизвестная студия') if config else 'Неизвестная студия' + # + # @classmethod + # def get_question_sequence(cls, studio_id: int) -> List[str]: + # """Получает последовательность вопросов для студии""" + # config = cls.get_questionnaire_for_studio(studio_id) + # return config.get('questions', []) if config else [] + # + # @classmethod + # def build_questionnaire_handlers(cls, studio_id: int) -> List[Callable]: + # """Собирает список обработчиков вопросов для студии""" + # question_ids = cls.get_question_sequence(studio_id) + # handlers = [] + # + # for qid in question_ids: + # handler = get_question_handler(qid) + # if handler: + # handlers.append(handler) + # + # return handlers + # + # @classmethod + # def get_question_objects(cls, studio_id: int) -> List[Any]: + # """Получает объекты вопросов для студии""" + # question_ids = cls.get_question_sequence(studio_id) + # questions = [] + # + # for qid in question_ids: + # q_obj = get_question_obj(qid) + # if q_obj: + # questions.append(q_obj) + # + # return questions + # + # @classmethod + # def get_autofill_fields(cls, studio_id: int) -> List[str]: + # """Получает список полей для автозаполнения""" + # questions = cls.get_question_objects(studio_id) + # autofill_fields = [] + # + # for q in questions: + # if q.autofill_field: + # autofill_fields.append(q.autofill_field) + # + # # Убираем дубликаты + # return list(set(autofill_fields)) + # + # #У нас каждый раз новая анкета, зачем автозаполнение?? + # @classmethod + # def can_autofill(cls, studio_id: int, user_data: Dict[str, Any]) -> bool: + # """Проверяет, можно ли автозаполнить анкету""" + # autofill_fields = cls.get_autofill_fields(studio_id) + # + # if not autofill_fields: + # return False + # + # # Проверяем, сколько полей можно автозаполнить + # fillable_count = 0 + # for field in autofill_fields: + # value = user_data.get(field, '') + # if value and str(value).strip(): + # fillable_count += 1 + # + # return fillable_count == len(autofill_fields) + # + # @classmethod + # def generate_summary(cls, studio_id: int, answers: Dict[str, str]) -> str: + # """Генерирует сводку по ответам""" + # questions = cls.get_question_objects(studio_id) + # studio_name = cls.get_studio_name(studio_id) + # + # lines = [f"📋 *Анкета для студии: {studio_name}*\n"] + # + # for i, q in enumerate(questions, 1): + # answer = answers.get(q.id, "❌ Не заполнено") + # lines.append(f"\n{i}. *{q.text}*\n{answer}") + # + # return "\n".join(lines) + # + # @classmethod + # def validate_answers(cls, studio_id: int, answers: Dict[str, str]) -> tuple[bool, Dict[str, str]]: + # """Валидирует все ответы анкеты""" + # questions = cls.get_question_objects(studio_id) + # errors = {} + # + # for q in questions: + # answer = answers.get(q.id, '').strip() + # + # if q.is_required and not answer: + # errors[q.id] = "Обязательный вопрос не заполнен" + # continue + # + # is_valid, error_msg = q.validate(answer) + # if not is_valid: + # errors[q.id] = error_msg + # + # return len(errors) == 0, errors From 2c58de2511d2324c74225b79a43c74abe47df76c Mon Sep 17 00:00:00 2001 From: Anastasia Lukina Date: Mon, 22 Dec 2025 19:22:27 +0300 Subject: [PATCH 11/12] =?UTF-8?q?=D0=9E=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D1=83=D1=81=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .venv/applications_manager.py | 54 ++++++++++++++++++----------------- .venv/userrrrrr.py | 13 --------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/.venv/applications_manager.py b/.venv/applications_manager.py index 2b7b868..63751e8 100644 --- a/.venv/applications_manager.py +++ b/.venv/applications_manager.py @@ -12,23 +12,22 @@ def __init__(self, bot): def get_connection(self): return sqlite3.connect(self.db_path, check_same_thread=False) - def get_user_applications(self, user_id: int) -> List[Dict]: + def get_user_applications(self, telegram_id: int) -> List[Dict]: conn = self.get_connection() cursor = conn.cursor() try: - # Проверим, есть ли таблица applications - cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='applications'") - if not cursor.fetchone(): - print("Таблица applications не существует!") + # 1. Находим user_id по telegram_id + cursor.execute("SELECT user_id FROM users WHERE telegram_id = ?", (telegram_id,)) + user_result = cursor.fetchone() + + if not user_result: + print(f"Пользователь с telegram_id {telegram_id} не найден!") return [] - # Проверим структуру таблицы - cursor.execute("PRAGMA table_info(applications)") - columns = [col[1] for col in cursor.fetchall()] - print(f"Столбцы таблицы applications: {columns}") + user_id = user_result[0] - # Простой запрос для проверки + # 2. Ищем заявки по user_id cursor.execute(""" SELECT application_id, studio_id, status, created_at FROM applications @@ -37,15 +36,14 @@ def get_user_applications(self, user_id: int) -> List[Dict]: """, (user_id,)) rows = cursor.fetchall() - print(f"Найдено заявок для пользователя {user_id}: {len(rows)}") + print(f"Найдено заявок для пользователя с telegram_id {telegram_id} (user_id={user_id}): {len(rows)}") applications = [] for row in rows: studio_name = f"Студия #{row[1]}" - # Попробуем получить название студии try: - cursor.execute("SELECT name FROM studios WHERE id = ?", (row[1],)) + cursor.execute("SELECT name FROM studios WHERE studio_id = ?", (row[1],)) studio_result = cursor.fetchone() if studio_result: studio_name = studio_result[0] @@ -64,6 +62,8 @@ def get_user_applications(self, user_id: int) -> List[Dict]: except Exception as e: print(f"Ошибка при получении заявок: {e}") + import traceback + traceback.print_exc() return [] finally: conn.close() @@ -71,11 +71,6 @@ def get_user_applications(self, user_id: int) -> List[Dict]: def show_user_applications(self, chat_id: int, user_id: int): applications = self.get_user_applications(user_id) - # Главное меню должно быть после сообщения, а не как кнопка над клавиатурой - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - markup.row(types.KeyboardButton('Каталог студий')) - markup.row(types.KeyboardButton('Мои заявки')) - if not applications: self.bot.send_message( chat_id, @@ -84,13 +79,21 @@ def show_user_applications(self, chat_id: int, user_id: int): ) return - message = "📋 Ваши заявки:\n\n" + message = "Ваши заявки:\n\n" for i, app in enumerate(applications, 1): try: - created_date = datetime.strptime(app['created_at'], '%Y-%m-%d %H:%M:%S') - date_str = created_date.strftime('%d.%m.%Y %H:%M') - except: + created_str = app['created_at'] + + if ' ' in created_str: + date_part, time_part = created_str.split(' ') + time_without_seconds = ':'.join(time_part.split(':')[:2]) + date_str = f"{date_part} {time_without_seconds}" + else: + date_str = created_str + + except Exception as e: + print(f"Ошибка форматирования даты {app['created_at']}: {e}") date_str = app['created_at'] status_text = { @@ -100,14 +103,13 @@ def show_user_applications(self, chat_id: int, user_id: int): }.get(app['status'], app['status']) message += f"{i}. {app['studio_name']}\n" - message += f" {status_text}\n" - message += f" 📅 {date_str}\n\n" + message += f"Статус заявки: {status_text}\n" + message += f"Дата создания: {date_str}\n\n" self.bot.send_message( chat_id, message, parse_mode='HTML', - reply_markup=markup ) def cleanup_old_applications(self): @@ -116,7 +118,7 @@ def cleanup_old_applications(self): try: week_ago = datetime.now() - timedelta(days=7) - week_ago_str = week_ago.strftime('%Y-%m-%d %H:%M:%S') + week_ago_str = week_ago.strftime('%Y-%m-%d %H:%M:') cursor.execute(""" DELETE FROM applications diff --git a/.venv/userrrrrr.py b/.venv/userrrrrr.py index 2af70c7..4c06346 100644 --- a/.venv/userrrrrr.py +++ b/.venv/userrrrrr.py @@ -45,7 +45,6 @@ def start(message): handle_head_start(message) return - # Для обычных пользователей user_start(message) @@ -87,12 +86,6 @@ def handle_my_applications(message): applications_manager.show_user_applications(message.chat.id, user_id) -@bot.message_handler(func=lambda message: message.text == 'Главное меню') -def handle_main_menu_return(message): - """Возврат в главное меню""" - user_start(message) - - @bot.callback_query_handler(func=lambda call: call.data.startswith('info:')) def handle_studio_info(call): """Показ информации о студии""" @@ -122,7 +115,6 @@ def handle_autofill_choice(call): user_data = state['user_data'] logger.info(f"Данные пользователя для автозаполнения: {user_data}") - # Получаем все вопросы для этой студии questions = QuestionnaireBuilder.get_question_objects(studio_id) for q in questions: @@ -167,12 +159,8 @@ def handle_all_text_messages(message): from Admin import handle_admin_message handle_admin_message(message) return - - # Обработка других команд if message.text == 'Мои заявки': bot.send_message(message.chat.id, "Функция 'Мои заявки' в разработке") - else: - bot.send_message(message.chat.id, "Используйте кнопки меню или команду /start") @bot.callback_query_handler(func=lambda call: call.data == 'confirm_application') @@ -181,7 +169,6 @@ def handle_confirm_application(call): if call.from_user.id in questionnaire_flow.user_states: questionnaire_flow._confirm_application(call) - # После отправки заявки показываем главное меню markup = create_main_user_menu() bot.send_message( call.message.chat.id, From 87e21036d47fc12e3b1907add93094e3c184399b Mon Sep 17 00:00:00 2001 From: Marina Date: Wed, 24 Dec 2025 22:43:06 +0300 Subject: [PATCH 12/12] =?UTF-8?q?=D0=A0=D0=BE=D0=B1=D0=B8=D1=82=20=D0=B0?= =?UTF-8?q?=D0=BD=D0=BA=D0=B5=D1=82=D0=B0=20=D1=83=20=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .venv/Head_of_studio.py | 1670 ++++++++++++++--- .venv/auth.py | 36 + .venv/{student_studios_bot (1).db => bd2.db} | Bin 57344 -> 61440 bytes .venv/catalog.py | 63 + .venv/database.py | 106 ++ .venv/questionnaire_flow.py | 188 ++ .venv/questionnew.py | 123 ++ .venv/studio_manager.py | 1452 +++++++++----- .venv/userrrrr.py | 189 ++ .venv/users_collector.py | 8 + ...20\277\321\200\320\276\321\201\321\213.py" | 224 --- ...20\260\320\275\320\272\320\265\321\202.py" | 221 --- 12 files changed, 3094 insertions(+), 1186 deletions(-) create mode 100644 .venv/auth.py rename .venv/{student_studios_bot (1).db => bd2.db} (58%) create mode 100644 .venv/catalog.py create mode 100644 .venv/database.py create mode 100644 .venv/questionnaire_flow.py create mode 100644 .venv/questionnew.py create mode 100644 .venv/userrrrr.py delete mode 100644 ".venv/\320\262\320\276\320\277\321\200\320\276\321\201\321\213.py" delete mode 100644 ".venv/\321\201\320\261\320\276\321\200\321\211\320\270\320\272_\320\260\320\275\320\272\320\265\321\202.py" diff --git a/.venv/Head_of_studio.py b/.venv/Head_of_studio.py index 14fd07e..df5284e 100644 --- a/.venv/Head_of_studio.py +++ b/.venv/Head_of_studio.py @@ -1,107 +1,362 @@ -from gettext import textdomain - import telebot from telebot import types +import logging import sqlite3 -from typing import Optional, List, Tuple +from typing import Optional, List, Dict, Any from datetime import datetime -import logging - -from studio_manager import StudioManager -#В коде логгер нигде не используется??? logging.basicConfig( - # %(asctime)s — время события - # %(name)s — имя логгера (модуля) - # %(levelname)s — уровень важности (INFO, ERROR, WARNING) - # %(message)s — текст сообщения format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -# на уровне INFO: запуск/остановка процессов, критические действия пользователей, ошибки доступа,но не обычные действия пользователей level=logging.INFO ) logger = logging.getLogger(__name__) bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") -# Инициализируем менеджер студии -studio_manager = StudioManager(bot) -user_sessions = {} # Хранит активные сессии руководителей -studio_manager.user_states = {} # Хранит состояние просмотра заявок -#====================== -#====HEAD OF STUDIO==== -#====================== -def cleanup_stale_sessions(): - #Очистка зависших сессий более чем на час - пока не решила куда вставить - current_time = datetime.now() - stale_users = [] +def get_connection(db_path: str = "bd2.db") -> sqlite3.Connection: + """Возвращает соединение с БД""" + conn = sqlite3.connect(db_path, check_same_thread=False) + conn.execute("PRAGMA foreign_keys = ON") + return conn - for user_id, session in user_sessions.items(): - if (current_time - session['start_time']).seconds > 3600: # 1 час - stale_users.append(user_id) - for user_id in stale_users: - del user_sessions[user_id] - if user_id in studio_manager.user_states: - del studio_manager.user_states[user_id] - logger.warning(f"Очищена зависшая сессия пользователя {user_id}") +def get_user_by_telegram_id(telegram_id: int) -> Optional[Dict[str, Any]]: + """Получает данные пользователя по Telegram ID""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT user_id, + username, + full_name, + phone_number, + role, + student_group, + telegram_id, + email + FROM users + WHERE telegram_id = ? + """, (str(telegram_id),)) + result = cursor.fetchone() -def show_main_menu(markup): - btn1 = types.KeyboardButton('') - btn2=types.KeyboardButton('Новые заявки') - markup.row(btn1, btn2) - btn3 = types.KeyboardButton('Изменить информацию о студии') + if result: + return { + 'user_id': result[0], + 'username': result[1], + 'full_name': result[2], + 'phone_number': result[3], + 'role': result[4], + 'student_group': result[5], + 'telegram_id': result[6], + 'email': result[7] + } + except Exception as e: + logger.error(f"Ошибка получения данных пользователя: {e}") + return None -def get_connection() -> sqlite3.Connection: - """Возвращает соединение с БД""" - return sqlite3.connect("student_studios_bot (1).db", check_same_thread=False) +def get_studio_by_head_id(user_id: int) -> Optional[Dict[str, Any]]: + """Получает студию по ID руководителя""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT studio_id, + name, + description, + contacts, + promo_photo_id, + promo_video_id, + is_active, + head_user_id, + invite_code + FROM studios + WHERE head_user_id = ? + """, (user_id,)) + result = cursor.fetchone() + + if result: + return { + 'studio_id': result[0], + 'name': result[1], + 'description': result[2], + 'contacts': result[3], + 'promo_photo_id': result[4], + 'promo_video_id': result[5], + 'is_active': bool(result[6]), + 'head_user_id': result[7], + 'invite_code': result[8] + } + except Exception as e: + logger.error(f"Ошибка получения студии: {e}") + return None + + +def get_studio_statistics(studio_id: int) -> Dict[str, int]: + """Получает статистику по заявкам студии""" + try: + with get_connection() as conn: + cursor = conn.cursor() + + cursor.execute(""" + SELECT COUNT(*) as total, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, + SUM(CASE WHEN status = 'accepted' THEN 1 ELSE 0 END) as accepted, + SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected + FROM applications + WHERE studio_id = ? + """, (studio_id,)) + + stats_row = cursor.fetchone() + + return { + 'total': stats_row[0] if stats_row else 0, + 'pending': stats_row[1] if stats_row else 0, + 'accepted': stats_row[2] if stats_row else 0, + 'rejected': stats_row[3] if stats_row else 0 + } + except Exception as e: + logger.error(f"Ошибка получения статистики: {e}") + return {'total': 0, 'pending': 0, 'accepted': 0, 'rejected': 0} + + +def update_studio_info(studio_id: int, description: str = None, contacts: str = None) -> bool: + """Обновляет информацию о студии""" + try: + with get_connection() as conn: + cursor = conn.cursor() + + if description and contacts: + cursor.execute(""" + UPDATE studios + SET description = ?, + contacts = ? + WHERE studio_id = ? + """, (description, contacts, studio_id)) + elif description: + cursor.execute(""" + UPDATE studios + SET description = ? + WHERE studio_id = ? + """, (description, studio_id)) + elif contacts: + cursor.execute(""" + UPDATE studios + SET contacts = ? + WHERE studio_id = ? + """, (contacts, studio_id)) + + conn.commit() + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"Ошибка обновления информации студии: {e}") + return False + + +def get_pending_applications(studio_id: int) -> List[Dict[str, Any]]: + """Получает ожидающие заявки для студии""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT a.application_id, + a.summary, + a.created_at, + u.user_id, + u.full_name, + u.student_group, + u.username, + u.phone_number, + u.email, + u.telegram_id + FROM applications a + JOIN users u ON a.user_id = u.user_id + WHERE a.studio_id = ? + AND a.status = 'pending' + ORDER BY a.created_at ASC + """, (studio_id,)) + + rows = cursor.fetchall() + applications = [] + + for row in rows: + applications.append({ + 'application_id': row[0], + 'summary': row[1], + 'created_at': row[2], + 'user_id': row[3], + 'full_name': row[4], + 'student_group': row[5], + 'username': row[6], + 'phone_number': row[7], + 'email': row[8], + 'student_telegram_id': row[9] + }) + + return applications + except Exception as e: + logger.error(f"Ошибка получения заявок: {e}") + return [] + + +def get_application_details(application_id: int) -> Optional[Dict[str, Any]]: + """Получает детальную информацию о заявке""" + try: + with get_connection() as conn: + cursor = conn.cursor() + + cursor.execute(""" + SELECT a.application_id, + a.summary, + a.status, + a.created_at, + u.full_name, + u.student_group, + u.username, + u.phone_number, + u.email, + u.telegram_id, + s.name as studio_name, + s.studio_id + FROM applications a + JOIN users u ON a.user_id = u.user_id + JOIN studios s ON a.studio_id = s.studio_id + WHERE a.application_id = ? + """, (application_id,)) + + row = cursor.fetchone() + if not row: + return None + + cursor.execute(""" + SELECT question_text, answer_text + FROM application_answers + WHERE application_id = ? + ORDER BY created_at + """, (application_id,)) + + answers = cursor.fetchall() + answers_dict = {question: answer for question, answer in answers} + + return { + 'application_id': row[0], + 'summary': row[1], + 'status': row[2], + 'created_at': row[3], + 'full_name': row[4], + 'student_group': row[5], + 'username': row[6], + 'phone_number': row[7], + 'email': row[8], + 'student_telegram_id': row[9], + 'studio_name': row[10], + 'studio_id': row[11], + 'answers': answers_dict + } + except Exception as e: + logger.error(f"Ошибка получения деталей заявки: {e}") + return None + + +def update_application_status(application_id: int, status: str) -> bool: + """Обновляет статус заявки""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + UPDATE applications + SET status = ?, + updated_at = ? + WHERE application_id = ? + """, (status, datetime.now(), application_id)) + + conn.commit() + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"Ошибка обновления статуса заявки: {e}") + return False + -def create_main_menu_markup(user_id: int = None) -> types.ReplyKeyboardMarkup: - """Создает главное меню руководителя""" + +def create_head_menu() -> types.ReplyKeyboardMarkup: + """Создает меню для руководителя""" markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) - - btn_statistics = types.KeyboardButton('Отчет по заявкам') - btn_applications = types.KeyboardButton("Новые заявки") - markup.row(btn_statistics, btn_applications) - + btn_new_applications = types.KeyboardButton('Новые заявки') + btn_statistics = types.KeyboardButton('Статистика') + btn_edit_studio = types.KeyboardButton('Изменить информацию о студии') btn_refresh = types.KeyboardButton('Обновить') + + markup.row(btn_new_applications, btn_statistics) + markup.row(btn_edit_studio) markup.row(btn_refresh) - + + return markup + + +def create_student_menu() -> types.ReplyKeyboardMarkup: + """Создает меню для студента""" + markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) + + btn_catalog = types.KeyboardButton('Каталог студий') + btn_my_applications = types.KeyboardButton('Мои заявки') + btn_new_application = types.KeyboardButton('Подать заявку') + + markup.row(btn_catalog) + markup.row(btn_my_applications, btn_new_application) + + return markup + + +def create_head_inline_menu() -> types.InlineKeyboardMarkup: + """Создает inline-меню для руководителя""" + markup = types.InlineKeyboardMarkup(row_width=2) + + btn_new_applications = types.InlineKeyboardButton('Новые заявки', callback_data='head_new_applications') + btn_statistics = types.InlineKeyboardButton('Статистика', callback_data='head_statistics') + btn_edit_studio = types.InlineKeyboardButton('Изменить информацию', callback_data='head_edit_studio') + + markup.add(btn_new_applications, btn_statistics) + markup.add(btn_edit_studio) + return markup -@bot.message_handler(commands=['head']) -def handle_head_start(message): - """Обработчик команды для входа руководителя""" - user_id = message.from_user.id +review_states = {} +edit_states = {} + + + +def handle_head_start(message: types.Message) -> bool: + """Обработка входа руководителя""" + telegram_id = message.from_user.id first_name = message.from_user.first_name - - studio_id = studio_manager.get_studio_id_for_head(user_id) - - if not studio_id: - bot.send_message( - message.chat.id, - "❌ Доступ запрещен\n\n" - "Вы не зарегистрированы как руководитель студии.", - parse_mode='HTML' - ) - return - - # Получаем название студии - studio_name = studio_manager.get_studio_name(studio_id) - - # Получаем количество ожидающих заявок - applications = studio_manager.get_new_applications(studio_id) - pending_count = len(applications) - + + logger.info(f"=== ВХОД РУКОВОДИТЕЛЯ: telegram_id={telegram_id} ({first_name}) ===") + + # Проверяем, является ли руководителем + studio_data = get_head_studio_data(telegram_id) + + if not studio_data: + return False + + # Показать приветственное сообщение + studio_name = studio_data['name'] + stats = get_studio_statistics(studio_data['studio_id']) + welcome_message = ( - f"👋 Добро пожаловать, {first_name}!\n\n" + f"Добро пожаловать, {first_name}!\n\n" + f"Студия: {studio_name}\n\n" + f"Статистика заявок:\n" + f"Всего: {stats.get('total', 0)}\n" + f"Ожидают: {stats.get('pending', 0)}\n" + f"✅ Принято: {stats.get('accepted', 0)}\n" + f"❌ Отклонено: {stats.get('rejected', 0)}\n\n" ) - - # Отправляем главное меню - markup = create_main_menu_markup(user_id) + + markup = create_head_menu() + bot.send_message( message.chat.id, welcome_message, @@ -109,210 +364,1153 @@ def handle_head_start(message): reply_markup=markup ) -@bot.message_handler(func=lambda message: message.text.startswith('📋 Новые заявки')) -def handle_applications_start(message): - """Начало просмотра заявок""" - user_id = message.from_user.id - - # Проверяем, не идет ли уже просмотр - if user_id in studio_manager.user_states: + logger.info(f"✅ Показано меню руководителя для студии '{studio_name}'") + return True + + +def get_head_studio_data(telegram_id: int) -> Optional[dict]: + """Получает данные студии для руководителя""" + user_data = get_user_by_telegram_id(telegram_id) + + if not user_data or user_data['role'] != 'head': + logger.info(f"❌ Пользователь {telegram_id} не является руководителем") + return None + + studio_data = get_studio_by_head_id(user_data['user_id']) + + if not studio_data: + logger.warning(f"У руководителя {telegram_id} не назначена студия") + return None + + return studio_data + + +def handle_statistics(message: types.Message): + """Обработка кнопки статистики""" + telegram_id = message.from_user.id + + logger.info(f"=== СТАТИСТИКА ОТ telegram_id={telegram_id} ===") + + # Проверяем, является ли руководителем + studio_data = get_head_studio_data(telegram_id) + if not studio_data: + send_access_denied(message.chat.id) + return + + # Получаем статистику + stats = get_studio_statistics(studio_data['studio_id']) + studio_name = studio_data['name'] + + message_text = ( + f"Статистика по заявкам\n\n" + f"Студия: {studio_name}\n\n" + f"Общая статистика:\n" + f"📋 Всего заявок: {stats.get('total', 0)}\n" + f"Ожидают рассмотрения: {stats.get('pending', 0)}\n" + f"✅ Принято: {stats.get('accepted', 0)}\n" + f"❌ Отклонено: {stats.get('rejected', 0)}\n\n" + ) + + bot.send_message( + message.chat.id, + message_text, + parse_mode='HTML' + ) + + +def handle_edit_studio_info(message: types.Message): + """Обработка кнопки изменения информации о студии""" + telegram_id = message.from_user.id + + logger.info(f"=== ИЗМЕНЕНИЕ ИНФОРМАЦИИ ОТ telegram_id={telegram_id} ===") + + # Проверяем, является ли руководителем + studio_data = get_head_studio_data(telegram_id) + if not studio_data: + send_access_denied(message.chat.id) + return + + # Сохраняем состояние редактирования + edit_states[telegram_id] = { + 'action': 'editing_studio', + 'studio_id': studio_data['studio_id'], + 'step': 'description' + } + + studio_name = studio_data['name'] + current_description = studio_data.get('description', 'Не указано') + current_contacts = studio_data.get('contacts', 'Не указаны') + + edit_message = ( + f"Изменение информации о студии\n\n" + f"Студия: {studio_name}\n\n" + f"Текущее описание:\n{current_description}\n\n" + f"Текущие контакты:\n{current_contacts}\n\n" + f"Шаг 1 из 2: Введите новое описание студии (или напишите 'пропустить' чтобы оставить текущее):" + ) + + bot.send_message( + message.chat.id, + edit_message, + parse_mode='HTML' + ) + + +def handle_edit_description(message: types.Message): + """Обработка ввода нового описания""" + telegram_id = message.from_user.id + + if telegram_id not in edit_states or edit_states[telegram_id]['action'] != 'editing_studio': + return + + studio_id = edit_states[telegram_id]['studio_id'] + new_description = message.text.strip() + + if new_description.lower() == 'пропустить': + new_description = None + edit_states[telegram_id]['new_description'] = None + else: + edit_states[telegram_id]['new_description'] = new_description + + edit_states[telegram_id]['step'] = 'contacts' + + next_message = ( + f"Шаг 2 из 2: Введите новые контакты студии\n" + f"(или напишите 'пропустить' чтобы оставить текущие):" + ) + + bot.send_message( + message.chat.id, + next_message, + parse_mode='HTML' + ) + + +def handle_edit_contacts(message: types.Message): + """Обработка ввода новых контактов""" + telegram_id = message.from_user.id + + if telegram_id not in edit_states or edit_states[telegram_id]['action'] != 'editing_studio': + return + + new_contacts = message.text.strip() + studio_id = edit_states[telegram_id]['studio_id'] + + if new_contacts.lower() == 'пропустить': + new_contacts = None + + new_description = edit_states[telegram_id].get('new_description') + + # Обновляем информацию в БД + success = update_studio_info(studio_id, new_description, new_contacts) + + if success: + result_message = "✅ Информация о студии успешно обновлена!" + logger.info(f"✅ Обновлена информация о студии {studio_id}") + else: + result_message = "❌ Ошибка при обновлении информации" + logger.error(f"❌ Ошибка обновления информации студии {studio_id}") + + # Очищаем состояние + if telegram_id in edit_states: + del edit_states[telegram_id] + + bot.send_message( + message.chat.id, + result_message, + parse_mode='HTML' + ) + + # Возвращаем в главное меню с inline кнопками + return_to_head_menu_with_inline(message.chat.id) + + +def send_access_denied(chat_id: int): + """Отправляет сообщение об отказе в доступе""" + bot.send_message( + chat_id, + "❌ Доступ запрещен\n\n" + "Эта функция доступна только руководителям студий.", + parse_mode='HTML' + ) + + +def return_to_head_menu_with_inline(chat_id: int): + """Возвращает в главное меню руководителя с inline кнопками""" + markup = create_head_menu() + inline_markup = create_head_inline_menu() + + bot.send_message( + chat_id, + "Вы вернулись в главное меню руководителя\n\n", + parse_mode='HTML', + reply_markup=markup + ) + + bot.send_message( + chat_id, + "Быстрые действия:", + parse_mode='HTML', + reply_markup=inline_markup + ) + + +def format_application_message(app_data: Dict[str, Any], current_num: int, total: int) -> str: + """Форматирует сообщение с информацией о заявке""" + message = f"Заявка #{app_data['application_id']}\n\n" + message += f"Студент: {app_data['full_name']}\n" + + if app_data.get('student_group'): + message += f"Группа: {app_data['student_group']}\n" + + if app_data.get('username'): + message += f"Telegram: @{app_data['username']}\n" + + if app_data.get('phone_number'): + message += f"Телефон: {app_data['phone_number']}\n" + + if app_data.get('email'): + message += f"Email: {app_data['email']}\n" + + message += f"\nСводка:\n{app_data.get('summary', 'Нет сводки')}\n" + + message += f"\nПрогресс: {current_num} из {total}" + + return message + + +def start_reviewing_applications(telegram_id: int, studio_id: int, chat_id: int) -> bool: + """Начинает просмотр заявок руководителем""" + try: + applications = get_pending_applications(studio_id) + + if not applications: + logger.info(f"ℹНет новых заявок для студии {studio_id}") + return False + + review_states[telegram_id] = { + 'studio_id': studio_id, + 'applications': applications, + 'current_index': 0, + 'chat_id': chat_id, + 'status': 'reviewing' + } + + # Убираем навигационное сообщение, сразу показываем первую заявку + show_next_application(chat_id, telegram_id) + return True + + except Exception as e: + logger.error(f"Ошибка начала просмотра для руководителя {telegram_id}: {e}") + return False + + +def show_next_application(chat_id: int, telegram_id: int): + """Показывает следующую заявку в очереди""" + if telegram_id not in review_states: + return + + state = review_states[telegram_id] + applications = state['applications'] + current_index = state['current_index'] + + if current_index >= len(applications): bot.send_message( - message.chat.id, - "⚠️ Просмотр уже начат\n\n" - "Завершите текущий просмотр или нажмите 'Прекратить просмотр'.", + chat_id, + "🎉 Все заявки рассмотрены!\n\n" + "Новых заявок в очереди нет.", parse_mode='HTML' ) + + del review_states[telegram_id] + return_to_head_menu_with_inline(chat_id) return - - studio_id = studio_manager.get_studio_id_for_head(user_id) - if not studio_id: + + app_data = applications[current_index] + app_id = app_data['application_id'] + + details = get_application_details(app_id) + + if not details: bot.send_message( - message.chat.id, - "❌ 1Ошибка доступа", + chat_id, + f"Ошибка загрузки заявки #{app_id}. Пропускаем...", + parse_mode='HTML' + ) + state['current_index'] += 1 + show_next_application(chat_id, telegram_id) + return + + state['current_application_id'] = app_id + + message_text = format_application_message(details, current_index + 1, len(applications)) + + markup = types.InlineKeyboardMarkup(row_width=2) + + markup.add( + types.InlineKeyboardButton("✅ Принять", callback_data=f"accept:{app_id}"), + types.InlineKeyboardButton("❌ Отклонить", callback_data=f"reject:{app_id}") + ) + + # Изменено с "Связаться" на "Пропустить" + markup.add( + types.InlineKeyboardButton("Пропустить", callback_data=f"skip:{app_id}"), + types.InlineKeyboardButton("Прекратить", callback_data="stop_review") + ) + + bot.send_message( + chat_id, + message_text, + parse_mode='HTML', + reply_markup=markup + ) + + +def send_notification_to_student(student_telegram_id: str, student_name: str, studio_name: str, action: str): + """Отправляет уведомление студенту""" + try: + if not student_telegram_id: + return + + if action == 'accepted': + message = ( + f"🎉 Поздравляем, {student_name}!\n\n" + f"Ваша заявка в студию '{studio_name}' одобрена!\n\n" + f"Руководитель свяжется с вами для дальнейших инструкций." + ) + elif action == 'rejected': + message = ( + f"Уважаемый(ая) {student_name}!\n\n" + f"К сожалению, ваша заявка в студию '{studio_name}' отклонена.\n\n" + f"Не расстраивайтесь! Вы можете подать заявку в другие студии." + ) + else: + return + + bot.send_message( + student_telegram_id, + message, parse_mode='HTML' ) + + logger.info(f"✅ Уведомление отправлено студенту {student_telegram_id}") + + except Exception as e: + logger.error(f"Ошибка отправки уведомления студенту: {e}") + + +def remove_processed_application(telegram_id: int, application_id: int): + """Удаляет обработанную заявку из состояния пользователя""" + if telegram_id not in review_states: + return + + state = review_states[telegram_id] + applications = state['applications'] + + new_applications = [ + app for app in applications + if app['application_id'] != application_id + ] + + state['applications'] = new_applications + + if state['current_index'] >= len(new_applications): + state['current_index'] = 0 + + +def move_application_to_end(telegram_id: int, application_id: int): + """Перемещает заявку в конец очереди""" + if telegram_id not in review_states: + return + + state = review_states[telegram_id] + applications = state['applications'] + + target_app = None + other_apps = [] + + for app in applications: + if app['application_id'] == application_id: + target_app = app + else: + other_apps.append(app) + + if target_app: + state['applications'] = other_apps + [target_app] + state['current_index'] = 0 + + +def handle_application_action(call): + """Обрабатывает действия руководителя с заявками""" + telegram_id = call.from_user.id + + if telegram_id not in review_states: + bot.answer_callback_query(call.id, "Сессия устарела. Начните просмотр заново.") + return + + action_data = call.data + state = review_states[telegram_id] + + if action_data == "stop_review": + bot.answer_callback_query(call.id, "Просмотр прекращен") + + try: + bot.delete_message(call.message.chat.id, call.message.message_id) + except: + pass + + del review_states[telegram_id] + return_to_head_menu_with_inline(call.message.chat.id) + return + + parts = action_data.split(":") + if len(parts) != 2: + bot.answer_callback_query(call.id, "Ошибка обработки") + return + + action, app_id_str = parts + app_id = int(app_id_str) + + if state.get('current_application_id') != app_id: + bot.answer_callback_query(call.id, "Заявка уже обработана") + return + + try: + bot.delete_message(call.message.chat.id, call.message.message_id) + except: + pass + + if action == "accept": + accept_application(call, app_id, telegram_id) + elif action == "reject": + reject_application(call, app_id, telegram_id) + elif action == "skip": # Изменено с "contact" на "skip" + skip_application(call, app_id, telegram_id) + + +def accept_application(call, application_id: int, telegram_id: int): + """Принять заявку""" + try: + app_data = get_application_details(application_id) + if not app_data: + bot.answer_callback_query(call.id, "❌ Ошибка при принятии") + return + + success = update_application_status(application_id, 'accepted') + + if success: + send_notification_to_student( + app_data['student_telegram_id'], + app_data['full_name'], + app_data['studio_name'], + 'accepted' + ) + + bot.answer_callback_query(call.id, "✅ Заявка принята") + + remove_processed_application(telegram_id, application_id) + state = review_states[telegram_id] + show_next_application(state['chat_id'], telegram_id) + else: + bot.answer_callback_query(call.id, "❌ Ошибка при принятии") + + except Exception as e: + logger.error(f"Ошибка принятия заявки {application_id}: {e}") + bot.answer_callback_query(call.id, "❌ Ошибка при принятии") + + +def reject_application(call, application_id: int, telegram_id: int): + """Отклонить заявку""" + try: + app_data = get_application_details(application_id) + if not app_data: + bot.answer_callback_query(call.id, "❌ Ошибка при отклонении") + return + + success = update_application_status(application_id, 'rejected') + + if success: + send_notification_to_student( + app_data['student_telegram_id'], + app_data['full_name'], + app_data['studio_name'], + 'rejected' + ) + + bot.answer_callback_query(call.id, "❌ Заявка отклонена") + + remove_processed_application(telegram_id, application_id) + state = review_states[telegram_id] + show_next_application(state['chat_id'], telegram_id) + else: + bot.answer_callback_query(call.id, "❌ Ошибка при отклонении") + + except Exception as e: + logger.error(f"Ошибка отклонения заявки {application_id}: {e}") + bot.answer_callback_query(call.id, "❌ Ошибка при отклонении") + + +def skip_application(call, application_id: int, telegram_id: int): + """Пропустить заявку (переместить в конец)""" + try: + bot.answer_callback_query(call.id, "Заявка пропущена") + + move_application_to_end(telegram_id, application_id) + state = review_states[telegram_id] + show_next_application(state['chat_id'], telegram_id) + + except Exception as e: + logger.error(f"Ошибка пропуска заявки {application_id}: {e}") + bot.answer_callback_query(call.id, "❌ Ошибка") + + +# ==================== ОБРАБОТЧИКИ INLINE КНОПОК ==================== + +@bot.callback_query_handler(func=lambda call: call.data.startswith('head_')) +def handle_head_inline_callbacks(call): + """Обработка inline кнопок руководителя""" + action = call.data + + if action == 'head_new_applications': + handle_new_applications_inline(call) + elif action == 'head_statistics': + handle_statistics_inline(call) + elif action == 'head_edit_studio': + handle_edit_studio_inline(call) + + +def handle_new_applications_inline(call): + """Обработка inline кнопки 'Новые заявки'""" + telegram_id = call.from_user.id + + user_data = get_user_by_telegram_id(telegram_id) + if not user_data or user_data['role'] != 'head': + bot.answer_callback_query(call.id, "❌ Доступ запрещен") + return + + studio_data = get_studio_by_head_id(user_data['user_id']) + if not studio_data: + bot.answer_callback_query(call.id, "❌ У вас не назначена студия") + return + + if telegram_id in review_states: + bot.answer_callback_query(call.id, "Просмотр уже начат") return - - # Получаем название студии - studio_name = studio_manager.get_studio_name(studio_id) - success = studio_manager.start_reviewing_applications_with_id(user_id, studio_id) - + + success = start_reviewing_applications( + telegram_id, + studio_data['studio_id'], + call.message.chat.id + ) + if not success: + bot.answer_callback_query(call.id, "✅ Нет новых заявок") + bot.send_message( + call.message.chat.id, + "✅ Нет новых заявок\n\n" + "Все заявки уже рассмотрены или новых пока нет.", + parse_mode='HTML' + ) + else: + bot.answer_callback_query(call.id, "Начинаем просмотр заявок") + + +def handle_statistics_inline(call): + """Обработка inline кнопки 'Статистика'""" + telegram_id = call.from_user.id + + studio_data = get_head_studio_data(telegram_id) + if not studio_data: + bot.answer_callback_query(call.id, "❌ Доступ запрещен") + return + + stats = get_studio_statistics(studio_data['studio_id']) + studio_name = studio_data['name'] + + message_text = ( + f"Статистика по заявкам\n\n" + f"Студия: {studio_name}\n\n" + f"Общая статистика:\n" + f"Всего заявок: {stats.get('total', 0)}\n" + f"Ожидают рассмотрения: {stats.get('pending', 0)}\n" + f"✅ Принято: {stats.get('accepted', 0)}\n" + f"❌ Отклонено: {stats.get('rejected', 0)}" + ) + + bot.answer_callback_query(call.id, "Статистика загружена") + bot.send_message( + call.message.chat.id, + message_text, + parse_mode='HTML' + ) + + +def handle_edit_studio_inline(call): + """Обработка inline кнопки 'Изменить информацию'""" + telegram_id = call.from_user.id + + studio_data = get_head_studio_data(telegram_id) + if not studio_data: + bot.answer_callback_query(call.id, "❌ Доступ запрещен") return - - # Запускаем сессию - user_sessions[user_id] = { - 'start_time': datetime.now(), - 'studio_name': studio_name, - 'total_applications': len(studio_manager.user_states[user_id]['applications']), - 'processed': 0, - 'accepted': 0, - 'rejected': 0 + + edit_states[telegram_id] = { + 'action': 'editing_studio', + 'studio_id': studio_data['studio_id'], + 'step': 'description' } - - # Информационное сообщение - state = studio_manager.user_states[user_id] - total_apps = len(state['applications']) - + + studio_name = studio_data['name'] + current_description = studio_data.get('description', 'Не указано') + current_contacts = studio_data.get('contacts', 'Не указаны') + + edit_message = ( + f"Изменение информации о студии\n\n" + f"Студия: {studio_name}\n\n" + f"Текущее описание:\n{current_description}\n\n" + f"Текущие контакты:\n{current_contacts}\n\n" + f"Шаг 1 из 2: Введите новое описание студии (или напишите 'пропустить' чтобы оставить текущее):" + ) + + bot.answer_callback_query(call.id, "Режим редактирования") bot.send_message( - message.chat.id, - f"Начинаем просмотр заявок\n\n" - f"Студия: {studio_name}\n" - f"Всего заявок: {total_apps}\n", + call.message.chat.id, + edit_message, parse_mode='HTML' ) - - # Показываем первую заявку - studio_manager.show_next_application(message.chat.id, user_id) - -@bot.message_handler(func=lambda message: message.text == 'отчет по заявкам') -def handle_statistics(message): - user_id = message.from_user.id - - studio_id = studio_manager.get_studio_id_for_head(user_id) - if not studio_id: + + +@bot.message_handler(commands=['start']) +def handle_start(message): + """Главная команда запуска бота""" + telegram_id = message.from_user.id + first_name = message.from_user.first_name + + logger.info(f"=== КОМАНДА /start ОТ telegram_id={telegram_id} ({first_name}) ===") + + is_head = handle_head_start(message) + + if not is_head: + markup = create_student_menu() + bot.send_message( message.chat.id, - "❌ 2Ошибка доступа", - parse_mode='HTML' + f"👨‍🎓 Добро пожаловать, {first_name}!\n\n" + f"Роль: Студент\n\n" + f"Выберите действие в меню ниже:", + parse_mode='HTML', + reply_markup=markup ) - return - - stats = studio_manager.get_statistics(studio_id) - studio_name = studio_manager.get_studio_name(studio_id) - - # Форматируем сообщение - message_text = studio_manager.format_statistics_message(stats, studio_name) - + logger.info(f"✅ Показано меню студента для {telegram_id}") -@bot.message_handler(func=lambda message: message.text == 'Обновить') -def handle_refresh(message): - """Обновление списка заявок""" - user_id = message.from_user.id - print(user_id) - - studio_id = studio_manager.get_studio_id_for_head(user_id) - if not studio_id: +@bot.message_handler(func=lambda message: message.text == 'Новые заявки') +def handle_new_applications(message): + """Начало просмотра заявок руководителем""" + telegram_id = message.from_user.id + + logger.info(f"=== КНОПКА 'НОВЫЕ ЗАЯВКИ' ОТ telegram_id={telegram_id} ===") + + user_data = get_user_by_telegram_id(telegram_id) + if not user_data or user_data['role'] != 'head': bot.send_message( message.chat.id, - "❌ 3Ошибка доступа", + "❌ Доступ запрещен\n\n" + "Эта функция доступна только руководителям студий.", parse_mode='HTML' ) return - - # Получаем актуальный список заявок - applications = studio_manager.get_new_applications(studio_id) - studio_name = studio_manager.get_studio_name(studio_id) - - # Обновляем меню - markup = create_main_menu_markup(user_id) - - if applications: - message_text = ( - f"Список заявок обновлен\n\n" - f"Заявок в очереди: {len(applications)}\n\n" - f"Нажмите 'Новые заявки' для начала просмотра" + + studio_data = get_studio_by_head_id(user_data['user_id']) + if not studio_data: + bot.send_message( + message.chat.id, + "❌ Ошибка\n\n" + "У вас не назначена студия.", + parse_mode='HTML' ) - else: - message_text = ( - f"Список заявок обновлен\n\n" - f"Заявок в очереди: 0\n\n" - f"Новых заявок пока нет" + return + + if telegram_id in review_states: + bot.send_message( + message.chat.id, + "⚠️ Просмотр уже начат\n\n" + "Завершите текущий просмотр перед началом нового.", + parse_mode='HTML' ) - - bot.send_message( - message.chat.id, - message_text, - parse_mode='HTML', - reply_markup=markup - ) + return -@bot.message_handler(func=lambda message: message.text == 'Главное меню') -def handle_main_menu(message): - """Возврат в главное меню""" - user_id = message.from_user.id - - # Очищаем состояние просмотра - if user_id in studio_manager.user_states: - del studio_manager.user_states[user_id] - - - del user_sessions[user_id] - - # Возвращаем главное меню - markup = create_main_menu_markup(user_id) - bot.send_message( - message.chat.id, - "🏠 Главное меню руководителя", - parse_mode='HTML', - reply_markup=markup - ) + logger.info(f"✅ Запуск просмотра заявок для студии {studio_data['studio_id']}") -@bot.callback_query_handler(func=lambda call: call.data.startswith(('accept:', 'reject:', 'skip:')) or - call.data == 'stop_review') -def handle_review_callbacks(call): - """Обработка callback-ов от просмотра заявок""" - user_id = call.from_user.id - - # Обновляем статистику сессии - if user_id in user_sessions: - session = user_sessions[user_id] - - if call.data.startswith('accept:'): - session['accepted'] += 1 - session['processed'] += 1 - elif call.data.startswith('reject:'): - session['rejected'] += 1 - session['processed'] += 1 - elif call.data.startswith('skip:'): - session['processed'] += 1 - - # Передаем обработку в менеджер студии - studio_manager.handle_application_action(call) + success = start_reviewing_applications( + telegram_id, + studio_data['studio_id'], + message.chat.id + ) -@bot.message_handler(func=lambda message: True) -def handle_other_messages(message): - """Обработчик всех других сообщений""" - user_id = message.from_user.id - - # Если пользователь в режиме просмотра - if user_id in studio_manager.user_states: + if not success: bot.send_message( message.chat.id, - "⚠️ Вы в режиме просмотра заявок\n\n" - "Используйте кнопки под заявкой для действий.", + "✅ Нет новых заявок\n\n" + "Все заявки уже рассмотрены или новых пока нет.", parse_mode='HTML' ) - else: - # Проверяем, является ли руководителем - # studio_id = studio_manager.get_studio_id_for_head(user_id) - studio_id = True - - if studio_id: - # Показываем меню руководителя - markup = create_main_menu_markup(user_id) + + +@bot.message_handler(func=lambda message: message.text == 'Статистика') +def handle_statistics_button(message): + """Обработка кнопки статистики""" + handle_statistics(message) + + +@bot.message_handler(func=lambda message: message.text == 'Изменить информацию о студии') +def handle_edit_studio_button(message): + """Обработка кнопки изменения информации о студии""" + handle_edit_studio_info(message) + + +@bot.message_handler(func=lambda message: message.text == 'Обновить') +def handle_refresh(message): + """Обновление информации""" + telegram_id = message.from_user.id + + logger.info(f"=== КНОПКА 'ОБНОВИТЬ' ОТ telegram_id={telegram_id} ===") + + user_data = get_user_by_telegram_id(telegram_id) + + if user_data and user_data['role'] == 'head': + studio_data = get_studio_by_head_id(user_data['user_id']) + + if studio_data: + stats = get_studio_statistics(studio_data['studio_id']) + studio_name = studio_data['name'] + + markup = create_head_menu() + inline_markup = create_head_inline_menu() + + response = ( + f"✅ Информация обновлена\n\n" + f"Студия: {studio_name}\n" + f"Новых заявок: {stats.get('pending', 0)}\n" + f"Всего заявок: {stats.get('total', 0)}\n\n" + ) + bot.send_message( message.chat.id, - "Главное меню руководителя", + response, parse_mode='HTML', reply_markup=markup ) + + bot.send_message( + message.chat.id, + "Выберите действие:", + parse_mode='HTML', + reply_markup=inline_markup + ) + + logger.info(f"✅ Обновлено меню руководителя") else: - # Показываем обычное меню - markup = types.ReplyKeyboardMarkup(resize_keyboard=True) - btn1 = types.KeyboardButton('Каталог студий') - markup.row(btn1) - btn2 = types.KeyboardButton('Мои заявки') - btn3 = types.KeyboardButton('Подать заявку') - markup.row(btn2, btn3) - + markup = create_head_menu() + response = "❌ У вас не назначена студия" + bot.send_message( message.chat.id, - "👋 Добро пожаловать в бот студенческих студий!", + response, parse_mode='HTML', reply_markup=markup ) + else: + markup = create_student_menu() + response = "✅ Информация обновлена" + + bot.send_message( + message.chat.id, + response, + parse_mode='HTML', + reply_markup=markup + ) + + +@bot.callback_query_handler(func=lambda call: call.data.startswith(('accept:', 'reject:', 'skip:')) or + call.data == 'stop_review') +def handle_review_callbacks(call): + """Обработка callback-ов от просмотра заявок""" + logger.info(f"=== CALLBACK: {call.data} от telegram_id={call.from_user.id} ===") + handle_application_action(call) + + +@bot.message_handler(func=lambda message: True) +def handle_all_messages(message): + """Обработка всех остальных сообщений""" + telegram_id = message.from_user.id + + if telegram_id in review_states: + bot.send_message( + message.chat.id, + "⚠️ Вы в режиме просмотра заявок\n\n" + "Используйте кнопки под заявкой для действий.", + parse_mode='HTML' + ) + return + + if telegram_id in edit_states: + state = edit_states[telegram_id] + + if state['action'] == 'editing_studio': + if state['step'] == 'description': + handle_edit_description(message) + elif state['step'] == 'contacts': + handle_edit_contacts(message) + return + + user_data = get_user_by_telegram_id(telegram_id) + + if user_data and user_data['role'] == 'head': + markup = create_head_menu() + response = "Главное меню руководителя" + else: + markup = create_student_menu() + response = "Главное меню студента" + + bot.send_message( + message.chat.id, + response, + parse_mode='HTML', + reply_markup=markup + ) + + +if __name__ == "__main__": + logger.info("=" * 50) + logger.info("БОТ ЗАПУЩЕН") + logger.info("=" * 50) + + bot.polling(none_stop=True) + + + + + + + + +# +# from gettext import textdomain +# +# import telebot +# from telebot import types +# import sqlite3 +# from typing import Optional, List, Tuple +# from datetime import datetime +# import logging +# +# from studio_manager import StudioManager +# +# #В коде логгер нигде не используется??? +# logging.basicConfig( +# # %(asctime)s — время события +# # %(name)s — имя логгера (модуля) +# # %(levelname)s — уровень важности (INFO, ERROR, WARNING) +# # %(message)s — текст сообщения +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# # на уровне INFO: запуск/остановка процессов, критические действия пользователей, ошибки доступа,но не обычные действия пользователей +# level=logging.INFO +# ) +# logger = logging.getLogger(__name__) +# +# bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") +# +# # Инициализируем менеджер студии +# studio_manager = StudioManager(bot) +# user_sessions = {} # Хранит активные сессии руководителей +# +# studio_manager.user_states = {} # Хранит состояние просмотра заявок +# +# #====================== +# #====HEAD OF STUDIO==== +# #====================== +# def cleanup_stale_sessions(): +# #Очистка зависших сессий более чем на час - пока не решила куда вставить +# current_time = datetime.now() +# stale_users = [] +# +# for user_id, session in user_sessions.items(): +# if (current_time - session['start_time']).seconds > 3600: # 1 час +# stale_users.append(user_id) +# +# for user_id in stale_users: +# del user_sessions[user_id] +# if user_id in studio_manager.user_states: +# del studio_manager.user_states[user_id] +# logger.warning(f"Очищена зависшая сессия пользователя {user_id}") +# +# def show_main_menu(markup): +# btn1 = types.KeyboardButton('') +# btn2=types.KeyboardButton('Новые заявки') +# markup.row(btn1, btn2) +# btn3 = types.KeyboardButton('Изменить информацию о студии') +# +# +# def get_connection() -> sqlite3.Connection: +# """Возвращает соединение с БД""" +# return sqlite3.connect("student_studios_bot.db", check_same_thread=False) +# +# def create_main_menu_markup(user_id: int = None) -> types.ReplyKeyboardMarkup: +# """Создает главное меню руководителя""" +# markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) +# +# +# btn_statistics = types.KeyboardButton('Отчет по заявкам') +# btn_applications = types.KeyboardButton("Новые заявки") +# markup.row(btn_statistics, btn_applications) +# +# btn_refresh = types.KeyboardButton('Обновить') +# markup.row(btn_refresh) +# +# return markup +# +# @bot.message_handler(commands=['head']) +# def handle_head_start(message): +# """Обработчик команды для входа руководителя""" +# user_id = message.from_user.id +# first_name = message.from_user.first_name +# +# studio_id = studio_manager.get_studio_id_for_head(user_id) +# +# if not studio_id: +# bot.send_message( +# message.chat.id, +# "❌ Доступ запрещен\n\n" +# "Вы не зарегистрированы как руководитель студии.", +# parse_mode='HTML' +# ) +# return +# +# # Получаем название студии +# studio_name = studio_manager.get_studio_name(studio_id) +# +# # Получаем количество ожидающих заявок +# applications = studio_manager.get_new_applications(studio_id) +# pending_count = len(applications) +# +# welcome_message = ( +# f"👋 Добро пожаловать, {first_name}!\n\n" +# ) +# +# # Отправляем главное меню +# markup = create_main_menu_markup(user_id) +# bot.send_message( +# message.chat.id, +# welcome_message, +# parse_mode='HTML', +# reply_markup=markup +# ) +# +# @bot.message_handler(func=lambda message: message.text.startswith('📋 Новые заявки')) +# def handle_applications_start(message): +# """Начало просмотра заявок""" +# user_id = message.from_user.id +# +# # Проверяем, не идет ли уже просмотр +# if user_id in studio_manager.user_states: +# bot.send_message( +# message.chat.id, +# "⚠️ Просмотр уже начат\n\n" +# "Завершите текущий просмотр или нажмите 'Прекратить просмотр'.", +# parse_mode='HTML' +# ) +# return +# +# studio_id = studio_manager.get_studio_id_for_head(user_id) +# if not studio_id: +# bot.send_message( +# message.chat.id, +# "❌ 1Ошибка доступа", +# parse_mode='HTML' +# ) +# return +# +# # Получаем название студии +# studio_name = studio_manager.get_studio_name(studio_id) +# success = studio_manager.start_reviewing_applications_with_id(user_id, studio_id) +# +# if not success: +# return +# +# # Запускаем сессию +# user_sessions[user_id] = { +# 'start_time': datetime.now(), +# 'studio_name': studio_name, +# 'total_applications': len(studio_manager.user_states[user_id]['applications']), +# 'processed': 0, +# 'accepted': 0, +# 'rejected': 0 +# } +# +# # Информационное сообщение +# state = studio_manager.user_states[user_id] +# total_apps = len(state['applications']) +# +# bot.send_message( +# message.chat.id, +# f"Начинаем просмотр заявок\n\n" +# f"Студия: {studio_name}\n" +# f"Всего заявок: {total_apps}\n", +# parse_mode='HTML' +# ) +# +# # Показываем первую заявку +# studio_manager.show_next_application(message.chat.id, user_id) +# +# @bot.message_handler(func=lambda message: message.text == 'отчет по заявкам') +# def handle_statistics(message): +# user_id = message.from_user.id +# +# studio_id = studio_manager.get_studio_id_for_head(user_id) +# if not studio_id: +# bot.send_message( +# message.chat.id, +# "❌ 2Ошибка доступа", +# parse_mode='HTML' +# ) +# return +# +# stats = studio_manager.get_statistics(studio_id) +# studio_name = studio_manager.get_studio_name(studio_id) +# +# # Форматируем сообщение +# message_text = studio_manager.format_statistics_message(stats, studio_name) +# +# +# +# @bot.message_handler(func=lambda message: message.text == 'Обновить') +# def handle_refresh(message): +# """Обновление списка заявок""" +# user_id = message.from_user.id +# print(user_id) +# +# studio_id = studio_manager.get_studio_id_for_head(user_id) +# if not studio_id: +# bot.send_message( +# message.chat.id, +# "❌ 3Ошибка доступа", +# parse_mode='HTML' +# ) +# return +# +# # Получаем актуальный список заявок +# applications = studio_manager.get_new_applications(studio_id) +# studio_name = studio_manager.get_studio_name(studio_id) +# +# # Обновляем меню +# markup = create_main_menu_markup(user_id) +# +# if applications: +# message_text = ( +# f"Список заявок обновлен\n\n" +# f"Заявок в очереди: {len(applications)}\n\n" +# f"Нажмите 'Новые заявки' для начала просмотра" +# ) +# else: +# message_text = ( +# f"Список заявок обновлен\n\n" +# f"Заявок в очереди: 0\n\n" +# f"Новых заявок пока нет" +# ) +# +# bot.send_message( +# message.chat.id, +# message_text, +# parse_mode='HTML', +# reply_markup=markup +# ) +# +# @bot.message_handler(func=lambda message: message.text == 'Главное меню') +# def handle_main_menu(message): +# """Возврат в главное меню""" +# user_id = message.from_user.id +# +# # Очищаем состояние просмотра +# if user_id in studio_manager.user_states: +# del studio_manager.user_states[user_id] +# +# +# del user_sessions[user_id] +# +# # Возвращаем главное меню +# markup = create_main_menu_markup(user_id) +# bot.send_message( +# message.chat.id, +# "🏠 Главное меню руководителя", +# parse_mode='HTML', +# reply_markup=markup +# ) +# +# @bot.callback_query_handler(func=lambda call: call.data.startswith(('accept:', 'reject:', 'skip:')) or +# call.data == 'stop_review') +# def handle_review_callbacks(call): +# """Обработка callback-ов от просмотра заявок""" +# user_id = call.from_user.id +# +# # Обновляем статистику сессии +# if user_id in user_sessions: +# session = user_sessions[user_id] +# +# if call.data.startswith('accept:'): +# session['accepted'] += 1 +# session['processed'] += 1 +# elif call.data.startswith('reject:'): +# session['rejected'] += 1 +# session['processed'] += 1 +# elif call.data.startswith('skip:'): +# session['processed'] += 1 +# +# # Передаем обработку в менеджер студии +# studio_manager.handle_application_action(call) +# +# @bot.message_handler(func=lambda message: True) +# def handle_other_messages(message): +# """Обработчик всех других сообщений""" +# user_id = message.from_user.id +# +# # Если пользователь в режиме просмотра +# if user_id in studio_manager.user_states: +# bot.send_message( +# message.chat.id, +# "⚠️ Вы в режиме просмотра заявок\n\n" +# "Используйте кнопки под заявкой для действий.", +# parse_mode='HTML' +# ) +# else: +# # Проверяем, является ли руководителем +# # studio_id = studio_manager.get_studio_id_for_head(user_id) +# studio_id = True +# +# if studio_id: +# # Показываем меню руководителя +# markup = create_main_menu_markup(user_id) +# bot.send_message( +# message.chat.id, +# "Главное меню руководителя", +# parse_mode='HTML', +# reply_markup=markup +# ) +# else: +# # Показываем обычное меню +# markup = types.ReplyKeyboardMarkup(resize_keyboard=True) +# btn1 = types.KeyboardButton('Каталог студий') +# markup.row(btn1) +# btn2 = types.KeyboardButton('Мои заявки') +# btn3 = types.KeyboardButton('Подать заявку') +# markup.row(btn2, btn3) +# +# bot.send_message( +# message.chat.id, +# "👋 Добро пожаловать в бот студенческих студий!", +# parse_mode='HTML', +# reply_markup=markup +# ) @@ -428,6 +1626,4 @@ def handle_other_messages(message): # reply_markup=markup # ) -if __name__ == '__main__': - print("Бот запущен...") - bot.polling(none_stop=True) + diff --git a/.venv/auth.py b/.venv/auth.py new file mode 100644 index 0000000..b29885b --- /dev/null +++ b/.venv/auth.py @@ -0,0 +1,36 @@ +import sqlite3 +import logging +from typing import Optional + +logger = logging.getLogger(__name__) + +def get_connection(db_path: str = "student_studios_bot (1).db") -> sqlite3.Connection: + """Возвращает соединение с БД""" + return sqlite3.connect(db_path, check_same_thread=False) + +def get_user_role(username: str) -> str: + """Получение роли пользователя из базы данных""" + if not username: + return 'student' + + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT role FROM users WHERE username = ?", (f"@{username}",)) + result = cursor.fetchone() + return result[0] if result else 'student' + except Exception as e: + logger.error(f"Database error getting role: {e}") + return 'student' + +def get_studio_id_for_head(user_id: int) -> Optional[int]: + """Получает ID студии, за которой закреплен руководитель""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT studio_id FROM studios WHERE head_user_id = ?", (user_id,)) + result = cursor.fetchone() + return result[0] if result else None + except Exception as e: + logger.error(f"Error getting studio for head {user_id}: {e}") + return None \ No newline at end of file diff --git a/.venv/student_studios_bot (1).db b/.venv/bd2.db similarity index 58% rename from .venv/student_studios_bot (1).db rename to .venv/bd2.db index f607c7674e915409f0962ddb01921ca8f12e9c9a..8a157f7299702e56423d5dbe1cb7df96f8140b55 100644 GIT binary patch literal 61440 zcmeHQYj9h~b-o|~g18_6*^0s{tgK5Z3`vQid2s=ffa1!uMA?)jN|s2=ik%=334tKN zhX~S-bdngPB|DK5yK$0wI`Jck+dp+%SrQ}aMRnRqr|ony7tNn^+DRt)lV6>BCawFU z)283q```t5N$>&6wz8l_6L)vd?tXjD?m2t+oIUZ_;q+uuIx{vgl9-efVUti%Av`Ea zf*?5XFNlA}hrYXvFZe%wFZFQgydZZktS;&Lk7#HU8khZzTTtVJ)m@?tEabFD>ju)PFVBz z9PO2Q`VJq~le3dk1L?74GbARbvQlr?6TQ-bu7jO@hkK=l@#N@0dNkG0ENPYWPb3qQ z$$>aHuebY1*NNWFBgf3#`}>X`M|$Vg)1Io2AT%E6<@ySg9l zVf}B^JKH23?>g9pO80l2kmy-`mmM7wl>$oI(`=(9brH3u z>7>I`Q&ZzO*Tlw~)y{rZ6pS!?BsH>;rN6BsU>VY8Gs9?NB&qeAJvuX$$;8cPk~yj@ zaeQ!WG#MYA8abVuu#}k?%V;Fb?B*CXP@j^clMT}TN4oYuDmAja-91vHnbIsZ3?>r; z81sh2z({(up{Yr0-RcHkY?k!8;;D(Tsd24+(quA|Oid(42nXV|W@-P?!-xBh^hn8( zL^`8CTll+aPq4D4v(_P`M+cJ6X3u6Y`Nb1clVj|ATpy~qtpDNiJTA=CC{ahQj%7#> zJR8r9rQ#N$viynS#PpQIr;l29&w;KdB%5LqSirt$qyD&Zb0wShW@N9YX7_H#`K^pI z#>XZm&x~atd}q>`WVYz5Simhsi8|jN?l}|6Mw*~V; zVb=?U+LDwtMvP`>0NW%siSxq0)-k)Mx~SIT;)XOdQyWog zPQ6`JB8Ca@*-5=2&DxyTES(<1jGY+8jEUK?e^SRuLw2b)y_uf|1hdqp+d$$*{$=$l z{(nimh=f^ue?=p+x>?(9k+iZ0gvGk%T9OtC6-}6hE3jW|s{RkhFGx!||A)oQXyw8z z*OFGIS`|!wx_VJCKurC(b4t-McO|WSr`X`?i*+^$Ssv|A7JaQOm|=<%8)?;PM#-kY z&xHduwY83O{TfZC#%A@^-f7UN3}{R59>1NkuxFFq(_$4Oq!V>dKNs+@D!Z4flIia| z^fEgz!F%;Dm49Y7^K>>20$5zlrkEK_|0 z8f~`KvRpM<`dYYjL09xzxU#XZrUKsF!I*PLBl|z~3hOY8)E%T%zo1!BEC>6=wQ4pa zh)w>$e+lluh$HwHfp>#n4Auoke)nu`-eZmcM}Q;15#R`L1ULd50geDifFr;WC^G^N zxP8LTt-=;Xs1t+-C(_wLypGB|mpGD`NRK9H6CuN)BthGHNhr%7L z(Wuh;aD`9UwzWYko|zg(nb9$=Y=`wzKuDJ4Xe=BhLgCg(L}?9mw4qzgXhLl#wL^W2 zc75rGc3fTPPA`hwy^w^~Os(MAeiJjqF>KyGTqbsywJD-1EQopY~!tU+O z`~_A*>zTYqjz}RTCbz~SVZT2nxPA8v{`0>3t3Cc*{{JrXm~(O*0geDifFr;W;0SO8 zI077jk1qnbKG+~%o!*Qdc-`A@CS#WLZPL2gk%?RQ_KL(5@fj%QbNI4DeNFu#)bzLL z1jl#q#}CxEcc?$izlf|**rB@5;!wxmyp2+@L9U|IMN;PJP|5S4emOrwhcEE=t^9@j zIm&e#kFTTTMfJLR1Akwm6DV|QgVCeXbZ%o_y)NMq&T~+sUSjzc8m7((4*BVUf9%ES z(c|Gr+qC4Y7IG)2`>11%I`#YLl+goJHJd-jEQ}ZNR}R(AtC!VzkXPSh56~j?XO?Ao zMS?vti}q^AX#fH1dj^i=t~S;6g8eR5U;YQH!yHt({fkzK0@bf zkGFS311?e6LY(WZ5_)h@GVqje*n~`2A7o<=pOAC5?itQURB>v z|4jY!9qJ|E1bYwcfS$}zzw^@_(P+3;4o5m<*>#{3hg;>4((e`pCEGw;EEEj3W=+<^~-U?A{8uqOD|!D(V~{^1C41ULd50geDifFr;W z;0SO8I0762j=F2=N1R;?Z78Jcd{@C8QUAN3+8;l`BjIy){Vr9 zeXg9-i{fkDF=Fr@@~D|1uZ3@4aZ;41?kF!<3}e97|PjFBH+>C-Zz z_^UTwli;}r66%lO5QgJF6nvSv(n;z&>g(#8QX_NKxk27L8GQ1XdmUfif6uA>#&_{vlBR@y#J-Q3WK;~sgVX9v? z21A|GJ25mo$bR&y-ojXsui{PC4LC^QP#3vk86(VkI)j1519CJ3_k4q#Aq$vD@5nIq z#O#FW(nVfZ1Qa~DKz_z_^(4nR>XA;@&T$d_ARkYv5Z!_o=sqwRI(t951y{Z%4M2J# zis!PG5EBQ)oU1ubDJg|;7bSu65+C_#mWaeBTznA`aI)RjXP5$aXK{$D}SBwGEKxx z9FQMVoF*U;aeMD%UBKWX3Hc1Z0lu%1J5{@de z$@NHSi?I~U&ljX!@Wj4_`36EoYlJp`Sp|=+6Q%fnmne#I3y3jJ-zBPVL&UG3!P7u! zP836w_L?yps~2~PN;rauv7L&(Za2`Dyf;L# zgXJT7DEA`K(m3Q6EoN9v$RCw)2P!BY&6#^$`a*v$;w)O|<)|dfF(r&JRV{LRG#oJ} zFr>70Xep~0%F;`I@nD|G&`;(Je%p;?I4tWUX&EFMGoVJU>PxPQ0D{fms~Au=`LA|3 zFX@9=9BaVVC72c-tzk2taq0qg?#V?0wm8(8Az+jejoSg^`qUZ2|~PEOZ341gY63ZT}Q(%#Y`wfcnqQT>qbH@;VWPx$J^Uy4`7 zLGhE`AA85VVXvd=dey5{pY{CG^IeZpsa8Hy8Fc@xd#~%qu9ME6JHPDwM8(aDamV|P zl<@wCV@np-IwgZ8>TPIO!<827PCd0e>XdE|7>q==s&0|yDqC5-dxdR^C;9An zaw1Kam_T2kXbt5yd<>JvrXgRsAMI$!Z5MVb9^3*lg!_STS4J|CRm)}LS*$yo`VX~K z367d4g?j)4={B|&_F{YV!4?}@kMAK`XNAp*hh4~2Oslm00JGSJZmJn{>(2@efXH$s zUNQO7#zlbvZ&OixH!9jbC2UnZ=H*hhwUjWXH18#w3ZB`8(cYbN2=^APkx%( zd={aAE7vHxWc1q*PVCe~Y8x2dxa)E)B4zAM*j2FuRqQ?qnQ|A))axtItZx!_EA9eTEikK#jB1+-<=>|Y>%oa` zjT0^AI@uF8MWVGRvVE)21O_m9jO_pv+W$61dq0JubqUnw(k|2*O=h!;qQNa`p^@Qe zP_>xBN4FAHT2x)DALC0l1dsh55$wZ!>RRQJOV3T@OY+cL?*aRWc%#Tgu9pr{XH& z5zDrMO}WIqD90ujXIUo~i+O(J9#nzJrCxEaY;v(!tY(WBto>7)K#ol?&eby?^lrm& zwjtdcB+`9Co#I?28`3rbGqP={HU>bI@s87$cNV&~-;bQ&od(6ZH18-%7#~bDw0~z) zom@@T5%V|~F^_eEv5Zi}hazAe^t*_8jG0rNx2fq-5yf_%5Q0j@WU_xSo*hnSGT9Z_ z%9AFsg}s5@2y;j%)hHEKlwg9}WbUj&W&pKQsaP5)f=U0_kZAExef8A%MKFbJ@;6r^ zKQKc6B8=>EA97a-UIxwTka?!4X4({JaDf)%A1g3686R{aBSU3rP&5Nx`>o%7ATR7c zges`2dd$O%FwqMxF4OEl831#SQiKai7`b5;`&p<@nL-)LFwTSj9DFnQR8Zmezy6+L zEpG+6{f`%x-2OLT<8%9;+yDBxf^{Ee;P!vvxfX8!n{VE6)Jc0S#_j*)1gX0VuhqHz z&q7Ai8~7#O`>WjkS8s9q9~Z_ib=rdtkQi^QO$%bdNaprG8MWO0=k`ClxzJ+0e{}8t zg&hX6|AkHTi+?x*90861M}Q;15#R`L1ULd50geDifFr;WxLXk*``=UXf)MyO|BdQ< z-Iv9Ko@4mRKOBKOhQRq|=I_6)vlIUQiT?iN_+)ax*lAwI;Xf>eT4Rw=Op#kcax@xJ zO8PUGb^=_+`?tg)eD3$p{r>NW-~Y^~T<|;aLR{cAJCP(8o&|AP+HE#uhzmK?;*EtI zU9orDN&;pSJM>wOx!O6d-L2lUJSc-6c7q3HplAncIYp=~Bs?esj{Z|n8FHz~>(Nkn zP=-4Yl!5#IbN~M$q2vDlB|^n)JV0gj|2JbKJVW-sTXpW2xxdnVgKQDf! zN>_ZpaipSH@zv}^7JsBgmX+49>Fq#@uN;GWE9^u$9F1Ts7B& zGm!XQ1dw~4H3=c*h#b-pCJ*gEK^)Edp4Jn4;(Fiicx1XnH3XGKrhCjTGMyZWg~G8& zM@uv;cZAz5TRjL%!%|90XsN(K%W(6@aUW;U6bHMAlqKbJBV}lK7||ScxXTkpMdRqx zMAry+x1OjGCOM;(YmR)OQKD#)h`yNg89aV!2_8q3vT(E|(%K%9I|_N6rEnfET!Xau zhDJ!xk0PI&LRd~dCw9sdtU{UxB3nRKAAOO?m5b3CpJzma&r`O1j)}Y#+Eq(5)YcI; zeQ%+TA$%W8;e1}i=M-CnewJCIKGYdqZ{G&Y=_%}Lm5I%jnd8^nm58}SF7&hCZmZq?;Ka+5mAAz0Gjp)$|wLknu#`V|<~PD`-l zb@Th+FxCmO|358^3&D|Kb>N4ANBzI`&s6_kwb%EwZ;SY@cwDUXe%V`D_3f$#&)<1I zU-{e0%ayy`KXM;*{fp~aSI~LcnX33@#jxXl9p7@u!ry~XiO=JG6y||qwpD8(4T@Nz zbj&tjTBuAhgCW&R5hy5TTXjLOg(C86kJ)BeCh-##cYu&bqnnXxEln^8gf zI3^&9*|u5n6^DRX^O!?PF9eNgpqa-|RYQ)VwD}4{!K{Z}nFeB0+vG9SMxhtCDn3i3 znDWS+rbXG*nsXjy)$XNETB68oj7bEXi`kPQeW-`no+32)SkMsLkYtM&eM{gg=u@E> zqxHEXnh4b@zBP)Fu4&t#l{##Qk#EDqh;<_Vs3AtavWpR`VEB+OMm369HZfwWg%vhk z#U1U|1*ujM%O^;>eq&Q#_7Po_(7CdU5?c{%D$Thc)+GtO+;Bp?!gQQ9CMtr{^O=6j)Nv~k|Ayb;XWt1kUAce3o z4SKv&m!|ECw~W$6dLHRdHZ@K?sLNBW;w`&8VQC^o(5Bj4|cghI0|98wN6)b z=mLmwxFZ5+(}*v%qYV_2yH=?xjEz~gklfk}HJiE*wHbn6wPwLLz&T;nX~LxJ33FKUy^1$?R?Fm3*qj%Pz1NyeIX4MrEUslLm-n(3?wJA zgX!@luWrGKA!z%vxPOjrn3XS zI{oDo+O#v*fQtqK*6_`meL|NJ;!;vjSac&4C9tDY+IdGD-=YU}4Iw+(h3ifcE0Asg zoZSb6E-g|0p);h{(%x-g%{sDyZz^f%vVryKvU(tG?SD3yogAP>(*+xWH7euQ!1nYu zU+2K#eh|j{4;cbsUVgBK_aE&3MY{Zmct1zs32k~Yd|DU38pU5W@xyV8!Hl&sco0(O zh7+ia-iB;Z{N>wrqSGBVm1Vz(%IIYXdbR;OPWC;_IpVC=y?D3e-*9sm`(8F3KlBAd zn*3{*CVj*n|GdWJ=*=3LJXN>>snbn81-k5IqJMo-x>NvYyvkuL7k^R{rmbM_^$3&Q z%S2Ye(C2gk+Je`W<+=ch-nA8uEPDkze9Dxc>hj4CjsX_25vy$S30;2h9=7cAqa8i8 z>0-`#QWu~arF!E9h)zJ-wBpd?h6q)!M}!!TWdB!qe=7Lz!!Q2f2yg^A0vrL307rl$ zz!CU3B5?kZ0F+enzIu6kMe)7hWXVUhy`+}5HXPTtzHu{b{1Qi)t^R5@`z{nL9@m}B ztM6e$C5J7Z+5C&xXThEjT|saYUvW)3bFjk$l*hi*GAH3n9HZ2|-(IC{9_wa;J;l_KnfbqB0F$!k*a`zyiGV@}_HL zIXAhq&KX->daE{aOHUsiwy~#Ix70iBW%ep{D!BVnr?KW^ux5K!wEqfkfVf{5rLP~c z^#;J6ENvaNw71Giw6)MyV<{y>i7qR-2$xZEHocL5nQ^f-aKEJKBM zNJ}tt!8V3HsRbJ3CyPDZS=w#BjIHNFZvbq#*b`WpG;GYNU5zcU(V#c2JV%lZs@RLu X>8|HgwMLD06Ln(i6auyjDvJLP6i%`6 delta 693 zcmaKqPlysx9LIm-j61H*yf@mWt!C;*ux;z+&N%97^CtLx*nB-a2d$RP-i=grN7{k9TX8%f_C%<-h^7A<_%<&4~_xL{EzzUI?r>Nxfl&Rc(cgW)#V5A6=_KTyWdg7k1LSg-ZQ)cK0 s3g<5!X_Ag*Ic9T=__ANZG>)}6T2_ldi~bA>7J#>I)7>3l@}V^U8!Zl}?f?J) diff --git a/.venv/catalog.py b/.venv/catalog.py new file mode 100644 index 0000000..9483582 --- /dev/null +++ b/.venv/catalog.py @@ -0,0 +1,63 @@ +import telebot +from telebot import types +from typing import List, Tuple +import logging +from database import load_active_studios + +logger = logging.getLogger(__name__) + + +def show_studio_catalog(bot, chat_id: int): + """Показывает каталог студий""" + studios = load_active_studios() + + if not studios: + bot.send_message(chat_id, 'На данный момент активных студий нет.') + return + + markup = types.InlineKeyboardMarkup() + for studio_id, name in studios: + btn = types.InlineKeyboardButton(name, callback_data=f'info:{studio_id}') + markup.row(btn) + + bot.send_message(chat_id, 'Нажми на студию для просмотра:', reply_markup=markup) + + +def show_studio_info(bot, call): + """Показывает информацию о студии""" + studio_id = int(call.data.split(':')[1]) + + try: + from database import get_connection + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT name, description, contacts + FROM studios + WHERE studio_id = ? + """, (studio_id,)) + result = cursor.fetchone() + + if result: + name, description, contacts = result + text = f"{name}\n\n{description or 'Описание скоро появится.'}" + + if contacts: + text += f"\n\nКонтакты:\n{contacts}" + + markup = types.InlineKeyboardMarkup() + btn_apply = types.InlineKeyboardButton("📝 Подать заявку", callback_data=f'apply:{studio_id}') + markup.row(btn_apply) + + bot.edit_message_text( + text, + call.message.chat.id, + call.message.message_id, + parse_mode='html', + reply_markup=markup + ) + else: + bot.answer_callback_query(call.id, "Студия не найдена") + except Exception as e: + logger.error(f"Error showing studio info: {e}") + bot.answer_callback_query(call.id, "Ошибка при загрузке информации") \ No newline at end of file diff --git a/.venv/database.py b/.venv/database.py new file mode 100644 index 0000000..4b74817 --- /dev/null +++ b/.venv/database.py @@ -0,0 +1,106 @@ +import sqlite3 +from datetime import datetime +from typing import Optional, Dict, Any +import logging + +logger = logging.getLogger(__name__) + + +def get_connection(db_path: str = "student_studios_bot (1).db") -> sqlite3.Connection: + """Возвращает соединение с БД""" + return sqlite3.connect(db_path, check_same_thread=False) + + +def get_user_by_telegram_id(telegram_id: int) -> Optional[Dict[str, Any]]: + """Получает данные пользователя по Telegram ID""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT full_name, student_group, phone_number, email + FROM users + WHERE telegram_id = ? + """, (telegram_id,)) + result = cursor.fetchone() + + if result: + return { + 'full_name': result[0] or '', + 'student_group': result[1] or '', + 'phone_number': result[2] or '', + 'email': result[3] or '' + } + except Exception as e: + logger.error(f"Error getting user data: {e}") + return None + + +def save_application_to_db(telegram_id: int, studio_id: int, answers: dict, + user_first_name: str = None, user_last_name: str = None) -> Optional[int]: + """Сохраняет заявку в БД""" + try: + with get_connection() as conn: + cursor = conn.cursor() + + # Получаем или создаем пользователя + cursor.execute("SELECT user_id FROM users WHERE telegram_id = ?", (telegram_id,)) + user_result = cursor.fetchone() + + if user_result: + user_id = user_result[0] + else: + # Получаем ФИО из ответов или Telegram + full_name = answers.get('full_name') + if not full_name: + full_name = f"{user_first_name or ''} {user_last_name or ''}".strip() + if not full_name: + full_name = "Не указано" + + cursor.execute(""" + INSERT INTO users (telegram_id, full_name, created_at) + VALUES (?, ?, ?) + """, (telegram_id, full_name, datetime.now())) + user_id = cursor.lastrowid + + # Генерируем сводку + from users_collector import QuestionnaireBuilder + summary = QuestionnaireBuilder.generate_summary(studio_id, answers) + + # Сохраняем заявку + cursor.execute(""" + INSERT INTO applications (user_id, studio_id, summary, status, created_at) + VALUES (?, ?, ?, ?, ?) + """, (user_id, studio_id, summary, 'pending', datetime.now())) + + application_id = cursor.lastrowid + + # Сохраняем ответы на вопросы + for question_id, answer in answers.items(): + from questions import get_question_obj + q_obj = get_question_obj(question_id) + question_text = q_obj.text if q_obj else question_id + + cursor.execute(""" + INSERT INTO application_answers + (application_id, question_id, question_text, answer_text) + VALUES (?, ?, ?, ?) + """, (application_id, question_id, question_text, answer)) + + conn.commit() + return application_id + + except Exception as e: + logger.error(f"Error saving application: {e}") + return None + + +def load_active_studios(): + """Загрузка списка активных студий""" + try: + with get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT studio_id, name FROM studios WHERE is_active = 1 ORDER BY studio_id") + return cursor.fetchall() + except Exception as e: + logger.error(f"Error loading studios: {e}") + return [] \ No newline at end of file diff --git a/.venv/questionnaire_flow.py b/.venv/questionnaire_flow.py new file mode 100644 index 0000000..9e7b1cd --- /dev/null +++ b/.venv/questionnaire_flow.py @@ -0,0 +1,188 @@ +import telebot +from telebot import types +from typing import Dict, Any +import logging +from datetime import datetime + +from users_collector import QuestionnaireBuilder +from questions import get_question_obj +from database import save_application_to_db + +logger = logging.getLogger(__name__) + + +class QuestionnaireFlow: + """Управление процессом заполнения анкеты""" + + def __init__(self, bot): + self.bot = bot + self.user_states = {} + + def start_questionnaire(self, call, studio_id: int): + """Начало анкеты для выбранной студии""" + user_id = call.from_user.id + + studio_config = QuestionnaireBuilder.get_questionnaire_for_studio(studio_id) + if not studio_config: + self.bot.answer_callback_query(call.id, "Анкета для этой студии не настроена") + return False + + self.user_states[user_id] = { + 'state': 'questionnaire', + 'studio_id': studio_id, + 'studio_name': studio_config.get('name', 'Студия'), + 'current_question_index': 0, + 'answers': {}, + 'questionnaire': studio_config + } + + # Получаем данные пользователя для автозаполнения + user_data = self._get_user_data(user_id) + self.user_states[user_id]['user_data'] = user_data + + if QuestionnaireBuilder.can_autofill(studio_id, user_data): + self._offer_autofill(call, studio_id, user_data) + else: + self._start_manual_filling(call, studio_id) + return True + + def _get_user_data(self, user_id: int) -> Dict[str, Any]: + """Получает данные пользователя из БД""" + from database import get_user_by_telegram_id + user = get_user_by_telegram_id(user_id) + return user if user else {} + + def _offer_autofill(self, call, studio_id: int, user_data: Dict[str, Any]): + """Предлагает автозаполнение""" + autofill_fields = QuestionnaireBuilder.get_autofill_fields(studio_id) + autofill_info = [] + + for field in autofill_fields: + value = user_data.get(field, '') + if value: + field_name = { + 'full_name': 'ФИО', + 'student_group': 'Группа', + 'phone_number': 'Телефон', + 'email': 'Email' + }.get(field, field) + autofill_info.append(f"{field_name}: {value}") + + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + message = f"*Анкета для студии: {studio_name}*\n\n" + + if autofill_info: + message += "Найдены ваши данные из предыдущих анкет:\n" + for info in autofill_info: + message += f"• {info}\n" + message += "\nХотите использовать их для автозаполнения?" + else: + message += "Хотите использовать основную информацию для автозаполнения?" + + markup = types.InlineKeyboardMarkup() + markup.row( + types.InlineKeyboardButton("Использовать мои данные", callback_data='use_autofill'), + types.InlineKeyboardButton("Заполнить вручную", callback_data='manual_fill') + ) + + self.bot.edit_message_text( + message, + call.message.chat.id, + call.message.message_id, + parse_mode='Markdown', + reply_markup=markup + ) + + def _start_manual_filling(self, call, studio_id: int): + """Начинает ручное заполнение анкеты""" + user_id = call.from_user.id + studio_name = QuestionnaireBuilder.get_studio_name(studio_id) + + self.user_states[user_id]['state'] = 'answering' + + self.bot.edit_message_text( + f"Начинаем заполнение анкеты для студии *{studio_name}*.\n\n" + f"Пожалуйста, ответьте на несколько вопросов:", + call.message.chat.id, + call.message.message_id, + parse_mode='Markdown' + ) + + self._ask_next_question(call.message.chat.id, user_id) + + def _ask_next_question(self, chat_id: int, user_id: int): + """Задает следующий вопрос""" + if user_id not in self.user_states: + return + + state = self.user_states[user_id] + studio_id = state['studio_id'] + question_ids = QuestionnaireBuilder.get_question_sequence(studio_id) + current_index = state.get('current_question_index', 0) + + # Проверяем, не заполнили ли мы все вопросы + if current_index >= len(question_ids): + self._show_summary(chat_id, user_id) + return + + # Получаем текущий вопрос + qid = question_ids[current_index] + q_obj = get_question_obj(qid) + + if not q_obj: + # Пропускаем несуществующий вопрос + state['current_question_index'] += 1 + self._ask_next_question(chat_id, user_id) + return + + # Сохраняем текущий вопрос + state['current_question_id'] = qid + + # Отправляем вопрос + if q_obj.type == 'select' and q_obj.options: + # Для вопросов с выбором создаем клавиатуру + markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) + for option in q_obj.options: + markup.add(types.KeyboardButton(option)) + self.bot.send_message(chat_id, q_obj.text, reply_markup=markup) + else: + # Для обычных вопросов без клавиатуры + self.bot.send_message(chat_id, q_obj.text) + + def process_answer(self, message): + """Обработка ответа на вопрос""" + user_id = message.from_user.id + + if user_id not in self.user_states: + return + + state = self.user_states[user_id] + if state.get('state') != 'answering': + return + + current_qid = state.get('current_question_id') + answer_text = message.text.strip() + + if not current_qid: + return + + # Получаем объект вопроса для валидации + q_obj = get_question_obj(current_qid) + + if q_obj: + is_valid, error_msg = q_obj.validate(answer_text) + if not is_valid: + self.bot.send_message(message.chat.id, f"❌ {error_msg}\n\nПопробуйте еще раз:") + return + + # Сохраняем ответ + state['answers'][current_qid] = answer_text + + # Увеличиваем индекс и задаем следующий вопрос + state['current_question_index'] += 1 + + # Убираем клавиатуру для следующего вопроса + remove_markup = types.ReplyKeyboardRemove() + self.bot.send_message(message.chat.id, "Отлично! Следующий вопрос:", reply_markup=remove_markup) + + self._ask_next_question(message.chat.id, user_id) \ No newline at end of file diff --git a/.venv/questionnew.py b/.venv/questionnew.py new file mode 100644 index 0000000..2b34ebd --- /dev/null +++ b/.venv/questionnew.py @@ -0,0 +1,123 @@ +from typing import Dict, Any, Callable, Optional +import re + + +class Question: + """Класс для хранения вопроса""" + + def __init__(self, + question_id: str, + text: str, + question_type: str = 'text', + is_required: bool = True, + autofill_field: Optional[str] = None, + validation_func: Optional[Callable] = None, + options: Optional[list] = None): + self.id = question_id + self.text = text + self.type = question_type # text, phone, email, boolean, select + self.is_required = is_required + self.autofill_field = autofill_field + self.validation_func = validation_func + self.options = options or [] + + def validate(self, answer: str) -> tuple[bool, str]: + """Валидация ответа""" + answer = answer.strip() + + if self.is_required and not answer: + return False, "Это обязательный вопрос" + + if self.validation_func: + return self.validation_func(answer) + + # Базовая валидация по типу + if self.type == 'phone_number': + phone_digits = re.sub(r'[^\d]', '', answer) + if len(phone_digits) < 10: + return False, "Введите корректный номер телефона" + + elif self.type == 'email': + if '@' not in answer or '.' not in answer: + return False, "Введите корректный email" + + elif self.type == 'boolean': + if answer.lower() not in ['да', 'нет']: + return False, "Ответьте 'да' или 'нет'" + + elif self.type == 'select': + if answer not in self.options: + return False, f"Выберите вариант из списка: {', '.join(self.options)}" + + return True, "" + + +# ========== СЛОВАРЬ ВСЕХ ВОПРОСОВ ========== +ALL_QUESTIONS = { + 'full_name': ("Укажите ваше ФИО (полностью):", + Question('full_name', 'ФИО', autofill_field='full_name')), + + 'group': ("Укажите номер вашей группу:", + Question('group', 'Группа', autofill_field='student_group')), + + 'phone_number': ("Укажите ваш номер телефона:", + Question('phone_number', 'Телефон', 'phone_number', autofill_field='phone_number')), + + 'email': ("Укажите ваш email адрес:", + Question('email', 'Email', 'email', autofill_field='email')), + + 'experience': ("Расскажите о вашем опыте в данной деятельности:", + Question('experience', 'Опыт в деятельности')), + + 'motivation': ("Почему вы хотите вступить в эту студию?", + Question('motivation', 'Мотивация вступления')), + + 'activeness': ("Готовы ли вы участвовать в мероприятиях студии? (да/нет)", + Question('activeness', 'Участие в мероприятиях', 'boolean')), + + 'media': ("Какое направление вам интересно?\nВарианты: Фотостудия, Видеостудия, Студия дизайна, Журналистика и СММ", + Question('media', 'Направление медиацентра', 'select', + options=['Фотостудия', 'Видеостудия', 'Студия дизайна', 'Журналистика и СММ'])), + + 'music_instrument': ("На каком инструменте вы играете (если играете)?", + Question('music_instrument', 'Музыкальный инструмент')), + + 'singing_experience': ("Есть ли у вас опыт пения? Если да, опишите его:", + Question('singing_experience', 'Опыт пения')), + + 'improvisation': ("Как вы относитесь к импровизации?", + Question('improvisation', 'Импровизация')), + + 'tech_skills': ("Какими техническими навыками вы владеете?", + Question('tech_skills', 'Технические навыки')), + + 'organizational_exp': ("Был ли у вас опыт организации мероприятий? Опишите:", + Question('organizational_exp', 'Организационный опыт')), + + 'quote': ("Девиз по жизни или цитата, которая вас вдохновляет:", + Question('quote', 'Цитата')), + + 'project_ideas': ("Есть ли у вас идеи для проектов в студии?", + Question('project_ideas', 'Идеи для проектов')), + + 'leadership': ("Есть ли у вас лидерский опыт?", + Question('leadership', 'Лидерские качества')), + + 'public_speaking': ("Как вы относитесь к публичным выступлениям?", + Question('public_speaking', 'Публичные выступления')), + + 'feedback_source': ("Откуда вы узнали о нашей студии?", + Question('feedback_source', 'Источник информации')), +} + + +def get_question_handler(question_id: str) -> str: + """Возвращает текст вопроса по ID""" + result = ALL_QUESTIONS.get(question_id) + return result[0] if result else "" + + +def get_question_obj(question_id: str) -> Optional[Question]: + """Возвращает объект вопроса по ID""" + result = ALL_QUESTIONS.get(question_id) + return result[1] if result else None \ No newline at end of file diff --git a/.venv/studio_manager.py b/.venv/studio_manager.py index 7f1820d..b9159fc 100644 --- a/.venv/studio_manager.py +++ b/.venv/studio_manager.py @@ -1,9 +1,8 @@ import telebot from telebot import types -import sqlite3 -from typing import Dict, Any, Optional, List, Tuple import logging -from datetime import datetime +from typing import Dict, Any, List +from database import get_pending_applications, get_application_details, update_application_status, get_student_by_user_id logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', @@ -11,282 +10,108 @@ ) logger = logging.getLogger(__name__) + class StudioManager: - """Класс для управления анкетами руководителем студии""" - # user_states: состояние каждого руководителя(какие заявки просматривает, текущая позиция) - def __init__(self, bot, db_path: str = "student_studios_bot (1).db"): - + def __init__(self, bot): self.bot = bot - self.db_path = db_path - - # Состояния пользователей - self.user_states = {} - - # Кэш заявок для быстрого доступа - self.applications_cache = {} - - def get_connection(self) -> sqlite3.Connection: - """Возвращает соединение с БД""" - return sqlite3.connect(self.db_path, check_same_thread=False) - - def get_studio_id_for_head(self, user_id: int) -> Optional[int]: - #Получает ID студии, за которой закреплен руководитель - try: - with self.get_connection() as conn: - cursor = conn.cursor() - cursor.execute(""" - SELECT studio_id - FROM studios - WHERE head_user_id = ? - """, (user_id,)) - result = cursor.fetchone() - return result[0] if result else None - except Exception as e: - logger.error(f"Error getting studio for head {user_id}: {e}") - return None - - def get_new_applications(self, studio_id: int) -> List[Tuple[int, str, str, str]]: - #Получает ВСЕ новые заявки - #Возвращает:Список кортежей (application_id, summary, full_name, created_at) + self.user_states = {} # {telegram_id: state_dict} - try: - with self.get_connection() as conn: - cursor = conn.cursor() - cursor.execute(""" - SELECT - a.application_id, - a.summary, - u.full_name, - a.created_at, - u.username, - u.phone_number, - u.email - FROM applications a - JOIN users u ON a.user_id = u.user_id - WHERE a.studio_id = ? - AND a.status = 'pending' - ORDER BY a.created_at ASC - """, (studio_id,)) - - return cursor.fetchall() - except Exception as e: - logger.error(f"Error getting applications for studio {studio_id}: {e}") - return [] - - def get_application_details(self, application_id: int) -> Optional[Dict[str, Any]]: - #Получает детальную информацию о заявке по ее айди - try: - with self.get_connection() as conn: - cursor = conn.cursor() - - # Получаем основную информацию о заявке - cursor.execute(""" - SELECT - a.application_id, a.summary, a.status, a.created_at, - u.user_id, u.telegram_id, u.full_name, u.student_group, - u.phone_number, u.email, u.username, - s.name as studio_name - FROM applications a - JOIN users u ON a.user_id = u.user_id - JOIN studios s ON a.studio_id = s.studio_id - WHERE a.application_id = ? - """, (application_id,)) - - result = cursor.fetchone() - if not result: - return None - - # Получаем ответы на вопросы - cursor.execute(""" - SELECT question_text, answer_text - FROM application_answers - WHERE application_id = ? - ORDER BY answer_id - """, (application_id,)) - - answers = cursor.fetchall() - - # Формируем словарь с данными - app_data = { - 'application_id': result[0], - 'summary': result[1], - 'status': result[2], - 'created_at': result[3], - 'user_id': result[4], - 'user_telegram_id': result[5], - 'full_name': result[6], - 'student_group': result[7], - 'phone_number': result[8], - 'email': result[9], - 'username': result[10], - 'studio_name': result[11], - 'answers': dict(answers) if answers else {} - } - - # Сохраняем в кэш - self.applications_cache[application_id] = app_data - return app_data - - except Exception as e: - logger.error(f"Error getting application details {application_id}: {e}") - return None - - def format_application_message(self, app_data: Dict[str, Any]) -> str: - """Форматирует сообщение с информацией о заявке""" - if not app_data: - return "❌ Ошибка загрузки заявки" - - message = f"Заявка #{app_data['application_id']}\n" - message += f"Студент: {app_data['full_name']}\n" - message += f"Группа: {app_data['student_group'] or 'Не указана'}\n" - - if app_data.get('username'): - message += f"Telegram: @{app_data['username']}\n" - - if app_data.get('phone_number'): - message += f"Телефон: {app_data['phone_number']}\n" - - if app_data.get('email'): - message += f"Email: {app_data['email']}\n\n" - else: - message += "\n" - - # Добавляем ответы на вопросы - if app_data.get('answers'): - message += "Ответы на вопросы:\n" - for i, (question, answer) in enumerate(app_data['answers'].items(), 1): - message += f"{i}. {question}\n{answer}\n\n" - elif app_data.get('summary'): - message += "Сводка анкеты:\n" - # Преобразуем Markdown в HTML для лучшего отображения - summary = app_data['summary'].replace('*', '').replace('_', '') - message += f"{summary}\n\n" - - # Показываем прогресс - if 'user_id' in self.user_states: - user_id = None - for uid, state in self.user_states.items(): - if state.get('current_application_id') == app_data['application_id']: - user_id = uid - break - - if user_id: - state = self.user_states[user_id] - current_idx = state.get('current_index', 0) + 1 - total = len(state.get('applications', [])) - message += f"Прогресс: {current_idx} из {total}" - - return message - - def start_reviewing_applications(self, message): + + def start_reviewing_applications(self, telegram_id: int, studio_id: int, chat_id: int) -> bool: """Начинает просмотр заявок руководителем""" - user_id = message.from_user.id - - # Получаем студию руководителя - studio_id = self.get_studio_id_for_head(user_id) - if not studio_id: - self.bot.send_message( - message.chat.id, - "❌ Вы не закреплены ни за одной студией.", - parse_mode='HTML' - ) - return - - # Получаем новые заявки - applications = self.get_new_applications(studio_id) - - if not applications: + try: + # Получаем ожидающие заявки + applications = get_pending_applications(studio_id) + + if not applications: + logger.info(f"Нет новых заявок для студии {studio_id}") + return False + + # Сохраняем состояние пользователя + self.user_states[telegram_id] = { + 'studio_id': studio_id, + 'applications': applications, + 'current_index': 0, + 'chat_id': chat_id, + 'status': 'reviewing' + } + + # Информационное сообщение self.bot.send_message( - message.chat.id, - "Нет новых заявок для рассмотрения.", + chat_id, + f"Начинаем просмотр заявок\n\n" + f"Всего заявок: {len(applications)}\n\n" + f"Заявки показываются по одной. Используйте кнопки для действий:\n", parse_mode='HTML' ) - return - - # Получаем название студии для сообщения - studio_name = self.get_studio_name(studio_id) - - # Показываем информацию о количестве заявок - self.bot.send_message( - message.chat.id, - f"{studio_name}\n" - f"Найдено заявок: {len(applications)}\n\n", - parse_mode='HTML' - ) - - # Сохраняем состояние пользователя - self.user_states[user_id] = { - 'studio_id': studio_id, - 'applications': applications, - 'current_index': 0, - 'status': 'reviewing' - } - - # Показываем первую заявку - self.show_next_application(message.chat.id, user_id) - - def show_next_application(self, chat_id: int, user_id: int): + + self.show_next_application(chat_id, telegram_id) + return True + + except Exception as e: + logger.error(f"Ошибка начала просмотра для руководителя {telegram_id}: {e}") + return False + + + def show_next_application(self, chat_id: int, telegram_id: int): """Показывает следующую заявку в очереди""" - if user_id not in self.user_states: + if telegram_id not in self.user_states: return - - state = self.user_states[user_id] + + state = self.user_states[telegram_id] applications = state['applications'] current_index = state['current_index'] - + if current_index >= len(applications): # Все заявки обработаны self.bot.send_message( chat_id, - "Все заявки рассмотрены!\n\n" + "🎉 Все заявки рассмотрены!\n\n" "Новых заявок в очереди нет.", parse_mode='HTML' ) - + # Очищаем состояние - del self.user_states[user_id] + del self.user_states[telegram_id] return - + # Получаем текущую заявку - app_id, summary, full_name, created_at, username, phone, email = applications[current_index] + app_data = applications[current_index] + app_id = app_data['application_id'] - # Получаем данные заявки - app_data = self.get_application_details(app_id) - - if not app_data: - # Если не удалось загрузить, пропускаем эту заявку + # Получаем детальную информацию + details = get_application_details(app_id) + + if not details: + # Если ошибка, пропускаем эту заявку self.bot.send_message( chat_id, - f"⚠️ Ошибка загрузки заявки #{app_id}.", + f"⚠️ Ошибка загрузки заявки #{app_id}. Пропускаем...", parse_mode='HTML' ) - - # Увеличиваем индекс и показываем следующую state['current_index'] += 1 - self.show_next_application(chat_id, user_id) + self.show_next_application(chat_id, telegram_id) return - + # Сохраняем ID текущей заявки state['current_application_id'] = app_id - + # Форматируем сообщение - message_text = self.format_application_message(app_data) - - markup = types.InlineKeyboardMarkup() - - markup.row( + message_text = self.format_application_message(details, current_index + 1, len(applications)) + + # Создаем inline-клавиатуру + markup = types.InlineKeyboardMarkup(row_width=2) + + markup.add( types.InlineKeyboardButton("✅ Принять", callback_data=f"accept:{app_id}"), types.InlineKeyboardButton("❌ Отклонить", callback_data=f"reject:{app_id}") ) - - markup.row( - types.InlineKeyboardButton("⏭ Пропустить", callback_data=f"skip:{app_id}") - ) - - markup.row( - types.InlineKeyboardButton("⏹ Прекратить просмотр", callback_data="stop_review") + + markup.add( + types.InlineKeyboardButton("📞 Связаться", callback_data=f"contact:{app_id}"), + types.InlineKeyboardButton("Прекратить", callback_data="stop_review") ) - + # Отправляем сообщение self.bot.send_message( chat_id, @@ -294,302 +119,921 @@ def show_next_application(self, chat_id: int, user_id: int): parse_mode='HTML', reply_markup=markup ) - + + def format_application_message(self, app_data: Dict[str, Any], current_num: int, total: int) -> str: + """Форматирует сообщение с информацией о заявке""" + message = f"Заявка #{app_data['application_id']}\n\n" + message += f"Студент: {app_data['full_name']}\n" + + if app_data.get('student_group'): + message += f"Группа: {app_data['student_group']}\n" + + if app_data.get('username'): + message += f"Telegram: @{app_data['username']}\n" + + if app_data.get('phone_number'): + message += f"Телефон: {app_data['phone_number']}\n" + + if app_data.get('email'): + message += f"Email: {app_data['email']}\n" + + message += f"\nСводка:\n{app_data.get('summary', 'Нет сводки')}\n" + + if app_data.get('answers'): + message += f"\nОтветы на вопросы:\n" + for i, (question, answer) in enumerate(app_data['answers'].items(), 1): + message += f"\n{i}. {question}\n{answer}\n" + + message += f"\nПрогресс: {current_num} из {total}" + + return message + def handle_application_action(self, call): - - user_id = call.from_user.id - - if user_id not in self.user_states: - self.bot.answer_callback_query(call.id, "Сессия устарела") + """Обрабатывает действия руководителя с заявками""" + telegram_id = call.from_user.id + + if telegram_id not in self.user_states: + self.bot.answer_callback_query(call.id, "Сессия устарела. Начните просмотр заново.") return - + action_data = call.data - state = self.user_states[user_id] - + state = self.user_states[telegram_id] + if action_data == "stop_review": # Прекращаем просмотр self.bot.answer_callback_query(call.id, "Просмотр прекращен") - - # Показываем сообщение о прекращении - self.bot.edit_message_text( - "Просмотр заявок прекращен\n\n" - "Вы можете возобновить в любой момент.", - call.message.chat.id, - call.message.message_id, - parse_mode='HTML' - ) - + + # Удаляем последнее сообщение с заявкой + try: + self.bot.delete_message(call.message.chat.id, call.message.message_id) + except: + pass + # Очищаем состояние - del self.user_states[user_id] + del self.user_states[telegram_id] + + # Возвращаемся в главное меню + self.return_to_main_menu(call.message.chat.id) return - - + # Извлекаем ID заявки и действие parts = action_data.split(":") if len(parts) != 2: - self.bot.answer_callback_query(call.id, "Ошибка") + self.bot.answer_callback_query(call.id, "Ошибка обработки") return - + action, app_id_str = parts app_id = int(app_id_str) - + # Проверяем, что это текущая заявка if state.get('current_application_id') != app_id: self.bot.answer_callback_query(call.id, "Заявка уже обработана") return - + + # Удаляем текущее сообщение + try: + self.bot.delete_message(call.message.chat.id, call.message.message_id) + except: + pass + # Обрабатываем действие if action == "accept": - self.accept_application(call, app_id) + self.accept_application(call, app_id, telegram_id) elif action == "reject": - self.reject_application(call, app_id) - elif action == "skip": - self.skip_application(call, app_id) - - def accept_application(self, call, application_id: int): - """Принять заявку """ + self.reject_application(call, app_id, telegram_id) + elif action == "contact": + self.contact_student(call, app_id, telegram_id) + + def accept_application(self, call, application_id: int, telegram_id: int): + """Принять заявку""" try: - with self.get_connection() as conn: - cursor = conn.cursor() - - # Обновляем статус заявки - cursor.execute(""" - UPDATE applications - SET status = 'accepted', - processed_at = ? - WHERE application_id = ? - """, (datetime.now(), application_id)) - - conn.commit() - - # Получаем данные студента для уведомления - cursor.execute(""" - SELECT u.telegram_id, s.name, u.full_name - FROM applications a - JOIN users u ON a.user_id = u.user_id - JOIN studios s ON a.studio_id = s.studio_id - WHERE a.application_id = ? - """, (application_id,)) - - result = cursor.fetchone() - if result: - student_telegram_id, studio_name, student_name = result - # Отправляем уведомление студенту - try: - self.bot.send_message( - student_telegram_id, - f"🎉 Поздравляем, {student_name}!\n\n" - f"Ваша заявка в студию '{studio_name}' одобрена!\n\n", - parse_mode='HTML' - ) - except Exception as e: - logger.error(f"Error notifying student {student_telegram_id}: {e}") - + # Получаем данные заявки + app_data = get_application_details(application_id) + if not app_data: + self.bot.answer_callback_query(call.id, "❌ Ошибка при принятии") + return + + # Обновляем статус + success = update_application_status(application_id, 'accepted') + + if success: + # Отправляем уведомление студенту + self.send_notification_to_student( + app_data['student_telegram_id'], + app_data['full_name'], + app_data['studio_name'], + 'accepted' + ) + self.bot.answer_callback_query(call.id, "✅ Заявка принята") - - # Удаляем заявку из состояния пользователя - self.remove_application_from_state(call.from_user.id, application_id) - - # Удаляем сообщение с заявкой - self.bot.delete_message(call.message.chat.id, call.message.message_id) - - # Показываем следующую заявку (если есть) - self.show_next_application(call.message.chat.id, call.from_user.id) - + + # Удаляем заявку из состояния и показываем следующую + self.remove_processed_application(telegram_id, application_id) + state = self.user_states[telegram_id] + self.show_next_application(state['chat_id'], telegram_id) + else: + self.bot.answer_callback_query(call.id, "❌ Ошибка при принятии") + except Exception as e: - logger.error(f"Error accepting application {application_id}: {e}") + logger.error(f"Ошибка принятия заявки {application_id}: {e}") self.bot.answer_callback_query(call.id, "❌ Ошибка при принятии") - - def reject_application(self, call, application_id: int): - """ Отклонить заявку""" + + def reject_application(self, call, application_id: int, telegram_id: int): + """Отклонить заявку""" try: - with self.get_connection() as conn: - cursor = conn.cursor() - - # Обновляем статус заявки - cursor.execute(""" - UPDATE applications - SET status = 'rejected', - processed_at = ? - WHERE application_id = ? - """, (datetime.now(), application_id)) - - conn.commit() - - # Получаем данные студента для уведомления - cursor.execute(""" - SELECT u.telegram_id, s.name, u.full_name - FROM applications a - JOIN users u ON a.user_id = u.user_id - JOIN studios s ON a.studio_id = s.studio_id - WHERE a.application_id = ? - """, (application_id,)) - - result = cursor.fetchone() - if result: - student_telegram_id, studio_name, student_name = result - try: - self.bot.send_message( - student_telegram_id, - f"Уважаемый(ая) {student_name}!\n\n" - f"К сожалению, ваша заявка в студию '{studio_name}' не может быть принята.\n\n" - f"Не расстраивайтесь! Вы можете подать заявку в другие студии " - f"или попробовать снова в следующем семестре.", - parse_mode='HTML' - ) - except Exception as e: - logger.error(f"Error notifying student {student_telegram_id}: {e}") - + + app_data = get_application_details(application_id) + if not app_data: + self.bot.answer_callback_query(call.id, "❌ Ошибка при отклонении") + return + + success = update_application_status(application_id, 'rejected') + + if success: + self.send_notification_to_student( + app_data['student_telegram_id'], + app_data['full_name'], + app_data['studio_name'], + 'rejected' + ) + self.bot.answer_callback_query(call.id, "❌ Заявка отклонена") - - # Удаляем заявку из состояния пользователя - self.remove_application_from_state(call.from_user.id, application_id) - - # Удаляем сообщение с заявкой - self.bot.delete_message(call.message.chat.id, call.message.message_id) - - # Показываем следующую заявку - self.show_next_application(call.message.chat.id, call.from_user.id) - + + # Удаляем заявку из состояния и показываем следующую + self.remove_processed_application(telegram_id, application_id) + state = self.user_states[telegram_id] + self.show_next_application(state['chat_id'], telegram_id) + else: + self.bot.answer_callback_query(call.id, "❌ Ошибка при отклонении") + except Exception as e: - logger.error(f"Error rejecting application {application_id}: {e}") + logger.error(f"Ошибка отклонения заявки {application_id}: {e}") self.bot.answer_callback_query(call.id, "❌ Ошибка при отклонении") - - - def skip_application(self, call, application_id: int): - """Пропустить заявку (оставить в очереди)""" - # Перемещаем заявку в конец очереди - self.move_application_to_end(call.from_user.id, application_id) - - self.bot.answer_callback_query(call.id, "Заявка перемещена в конец очереди") - - # Удаляем текущее сообщение и показываем следующую заявку - self.bot.delete_message(call.message.chat.id, call.message.message_id) - self.show_next_application(call.message.chat.id, call.from_user.id) - - def remove_application_from_state(self, user_id: int, application_id: int): - """Удаляет заявку из состояния пользователя""" - if user_id not in self.user_states: + + def contact_student(self, call, application_id: int, telegram_id: int): + """Показать контакты студента и переместить заявку в конец""" + try: + # Получаем данные заявки + app_data = get_application_details(application_id) + if not app_data: + self.bot.answer_callback_query(call.id, "❌ Ошибка") + return + + # Формируем контактную информацию + contact_info = f"Контакты студента:\n\n" + contact_info += f"Имя: {app_data['full_name']}\n" + + if app_data.get('username'): + contact_info += f"Telegram: @{app_data['username']}\n" + + if app_data.get('phone_number'): + contact_info += f"Телефон: {app_data['phone_number']}\n" + + if app_data.get('email'): + contact_info += f"Email: {app_data['email']}\n" + + if app_data.get('student_group'): + contact_info += f"Группа: {app_data['student_group']}\n" + + + # Перемещаем заявку в конец очереди + self.move_application_to_end(telegram_id, application_id) + state = self.user_states[telegram_id] + self.show_next_application(state['chat_id'], telegram_id) + + except Exception as e: + logger.error(f"Ошибка получения контактов для заявки {application_id}: {e}") + self.bot.answer_callback_query(call.id, "❌ Ошибка") + + def send_notification_to_student(self, student_telegram_id: str, student_name: str, studio_name: str, action: str): + """Отправляет уведомление студенту""" + try: + if not student_telegram_id: + return + + if action == 'accepted': + message = ( + f"🎉 Поздравляем, {student_name}!\n\n" + f"Ваша заявка в студию '{studio_name}' одобрена!\n\n" + f"Руководитель свяжется с вами для дальнейших инструкций." + ) + elif action == 'rejected': + message = ( + f"Уважаемый(ая) {student_name}!\n\n" + f"К сожалению, ваша заявка в студию '{studio_name}' отклонена.\n\n" + f"Не расстраивайтесь! Вы можете подать заявку в другие студии." + ) + else: + return + + self.bot.send_message( + student_telegram_id, + message, + parse_mode='HTML' + ) + + logger.info(f"✅ Уведомление отправлено студенту {student_telegram_id}") + + except Exception as e: + logger.error(f"Ошибка отправки уведомления студенту: {e}") + + + def remove_processed_application(self, telegram_id: int, application_id: int): + """Удаляет обработанную заявку из состояния пользователя""" + if telegram_id not in self.user_states: return - - state = self.user_states[user_id] + + state = self.user_states[telegram_id] applications = state['applications'] - - # Удаляем заявку из списка + new_applications = [ - app for app in applications - if app[0] != application_id + app for app in applications + if app['application_id'] != application_id ] - - # Обновляем список заявок + state['applications'] = new_applications - - # Если текущая заявка была удалена, корректируем индекс - if state.get('current_index') >= len(new_applications): - state['current_index'] = max(0, len(new_applications) - 1) - - def move_application_to_end(self, user_id: int, application_id: int): - """Перемещает заявку в конец очереди """ - if user_id not in self.user_states: + + if state['current_index'] >= len(new_applications): + state['current_index'] = 0 + + def move_application_to_end(self, telegram_id: int, application_id: int): + """Перемещает заявку в конец очереди""" + if telegram_id not in self.user_states: return - - state = self.user_states[user_id] + + state = self.user_states[telegram_id] applications = state['applications'] - + # Находим заявку - target_application = None - new_applications = [] - + target_app = None + other_apps = [] + for app in applications: - if app[0] == application_id: - target_application = app + if app['application_id'] == application_id: + target_app = app else: - new_applications.append(app) - - # Если нашли заявку, добавляем ее в конец - if target_application: - new_applications.append(target_application) - - # Обновляем список заявок - state['applications'] = new_applications - - # Корректируем текущий индекс - # Текущая заявка будет удалена из сообщения, - # поэтому показываем следующую по порядку - # Индекс не меняем, так как список сдвинулся - if state.get('current_index') >= len(new_applications): - state['current_index'] = len(new_applications) - 1 - - def get_studio_name(self, studio_id: int) -> str: - """Получает название студии по ID""" - try: - with self.get_connection() as conn: - cursor = conn.cursor() - cursor.execute( - "SELECT name FROM studios WHERE studio_id = ?", - (studio_id,) - ) - result = cursor.fetchone() - return result[0] if result else "Неизвестная студия" - except Exception as e: - logger.error(f"Error getting studio name: {e}") - return "Неизвестная студия" - - def get_statistics(self, studio_id: int) -> Dict[str, Any]: - """Получает отчет по заявкам студии""" - try: - with self.get_connection() as conn: - cursor = conn.cursor() - - cursor.execute(""" - SELECT - COUNT(*) as total, - SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, - SUM(CASE WHEN status = 'accepted' THEN 1 ELSE 0 END) as accepted, - SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected - FROM applications - WHERE studio_id = ? - """, (studio_id,)) - - stats = cursor.fetchone() - - # Статистика по дням (последние 7 дней) - cursor.execute(""" - SELECT - DATE(created_at) as date, - COUNT(*) as count - FROM applications - WHERE studio_id = ? - AND created_at >= DATE('now', '-7 days') - GROUP BY DATE(created_at) - ORDER BY date DESC - """, (studio_id,)) - - daily_stats = cursor.fetchall() - - - - except Exception as e: - logger.error(f"Error getting statistics for studio {studio_id}: {e}") - return {} - - def format_statistics_message(self, stats: Dict[str, Any], studio_name: str) -> str: - """Форматирует сообщение с отчетом""" - if not stats: - return "❌ Не удалось получить отчет" - - message = f"отчет по заявкам студии '{studio_name}'\n\n" - - message += f"Общая статистика:\n" - message += f"📋 Всего заявок: {stats.get('total', 0)}\n" - message += f"⏳ Ожидают рассмотрения: {stats.get('pending', 0)}\n" - message += f"✅ Принято: {stats.get('accepted', 0)}\n" - message += f"❌ Отклонено: {stats.get('rejected', 0)}\n\n" - - if stats.get('daily'): - message += "За последние 7 дней:\n" - for date, count in stats['daily'][:7]: - message += f"{date}: {count} заявок\n" - message += "\n" - - return message \ No newline at end of file + other_apps.append(app) + + if target_app: + state['applications'] = other_apps + [target_app] + # Переходим к следующей заявке + state['current_index'] = 0 + + def return_to_main_menu(self, chat_id: int): + """Возвращает в главное меню руководителя""" + from head_of_studio import HeadOfStudio + from telebot import types + + # Создаем меню руководителя + markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) + + btn_new_applications = types.KeyboardButton('Новые заявки') + btn_statistics = types.KeyboardButton('Статистика') + btn_edit_studio = types.KeyboardButton('Изменить информацию о студии') + btn_refresh = types.KeyboardButton('Обновить') + + markup.row(btn_new_applications, btn_statistics) + markup.row(btn_edit_studio) + markup.row(btn_refresh) + + self.bot.send_message( + chat_id, + parse_mode='HTML', + reply_markup=markup + ) + + + + + +# import telebot +# from telebot import types +# import sqlite3 +# from typing import Dict, Any, Optional, List, Tuple +# import logging +# from datetime import datetime +# +# logging.basicConfig( +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +# level=logging.INFO +# ) +# logger = logging.getLogger(__name__) +# +# class StudioManager: +# """Класс для управления анкетами руководителем студии""" +# # user_states: состояние каждого руководителя(какие заявки просматривает, текущая позиция) +# def __init__(self, bot, db_path: str = "student_studios_bot (1).db"): +# +# self.bot = bot +# self.db_path = db_path +# +# # Состояния пользователей +# self.user_states = {} +# +# # Кэш заявок для быстрого доступа +# self.applications_cache = {} +# +# def get_connection(self) -> sqlite3.Connection: +# """Возвращает соединение с БД""" +# return sqlite3.connect(self.db_path, check_same_thread=False) +# +# def start_reviewing_applications_with_id(self, user_id: int, studio_id: int) -> bool: +# """Начинает просмотр заявок для указанного пользователя и студии""" +# try: +# # Получаем новые заявки +# applications = self.get_new_applications(studio_id) +# +# if not applications: +# return False +# +# # Сохраняем состояние пользователя +# self.user_states[user_id] = { +# 'studio_id': studio_id, +# 'applications': applications, +# 'current_index': 0, +# 'status': 'reviewing' +# } +# +# return True +# +# except Exception as e: +# logger.error(f"Error starting review for user {user_id}, studio {studio_id}: {e}") +# return False +# +# def get_studio_id_for_head(self, user_id: int) -> Optional[int]: +# try: +# with self.get_connection() as conn: +# cursor = conn.cursor() +# print(f"\n[DEBUG] Ищу студию для user_id: {user_id}") +# +# cursor.execute(""" +# SELECT studio_id +# FROM studios +# WHERE head_user_id = ? +# """, (user_id,)) +# +# result = cursor.fetchone() +# print(f"[DEBUG] Результат запроса: {result}") +# +# if result: +# print(f"[DEBUG] Найдена студия ID: {result[0]}") +# else: +# print(f"[DEBUG] Студия не найдена для user_id: {user_id}") +# # Покажем все студии для отладки +# cursor.execute("SELECT studio_id, name, head_user_id FROM studios") +# all_studios = cursor.fetchall() +# print(f"[DEBUG] Все студии в базе: {all_studios}") +# +# return result[0] if result else None +# except Exception as e: +# logger.error(f"Error getting studio for head {user_id}: {e}") +# return None +# +# def get_new_applications(self, studio_id: int) -> List[Tuple[int, str, str, str]]: +# #Получает ВСЕ новые заявки +# #Возвращает:Список кортежей (application_id, summary, full_name, created_at) +# +# try: +# with self.get_connection() as conn: +# cursor = conn.cursor() +# cursor.execute(""" +# SELECT +# a.application_id, +# a.summary, +# u.full_name, +# a.created_at, +# u.username, +# u.phone_number, +# u.email +# FROM applications a +# JOIN users u ON a.user_id = u.user_id +# WHERE a.studio_id = ? +# AND a.status = 'pending' +# ORDER BY a.created_at ASC +# """, (studio_id,)) +# +# return cursor.fetchall() +# except Exception as e: +# logger.error(f"Error getting applications for studio {studio_id}: {e}") +# return [] +# +# def get_application_details(self, application_id: int) -> Optional[Dict[str, Any]]: +# #Получает детальную информацию о заявке по ее айди +# try: +# with self.get_connection() as conn: +# cursor = conn.cursor() +# +# # Получаем основную информацию о заявке +# cursor.execute(""" +# SELECT +# a.application_id, a.summary, a.status, a.created_at, +# u.user_id, u.telegram_id, u.full_name, u.student_group, +# u.phone_number, u.email, u.username, +# s.name as studio_name +# FROM applications a +# JOIN users u ON a.user_id = u.user_id +# JOIN studios s ON a.studio_id = s.studio_id +# WHERE a.application_id = ? +# """, (application_id,)) +# +# result = cursor.fetchone() +# if not result: +# return None +# +# # Получаем ответы на вопросы +# cursor.execute(""" +# SELECT question_text, answer_text +# FROM application_answers +# WHERE application_id = ? +# ORDER BY answer_id +# """, (application_id,)) +# +# answers = cursor.fetchall() +# +# # Формируем словарь с данными +# app_data = { +# 'application_id': result[0], +# 'summary': result[1], +# 'status': result[2], +# 'created_at': result[3], +# 'user_id': result[4], +# 'user_telegram_id': result[5], +# 'full_name': result[6], +# 'student_group': result[7], +# 'phone_number': result[8], +# 'email': result[9], +# 'username': result[10], +# 'studio_name': result[11], +# 'answers': dict(answers) if answers else {} +# } +# +# # Сохраняем в кэш +# self.applications_cache[application_id] = app_data +# return app_data +# +# except Exception as e: +# logger.error(f"Error getting application details {application_id}: {e}") +# return None +# +# def format_application_message(self, app_data: Dict[str, Any]) -> str: +# """Форматирует сообщение с информацией о заявке""" +# if not app_data: +# return "❌ Ошибка загрузки заявки" +# +# message = f"Заявка #{app_data['application_id']}\n" +# message += f"Студент: {app_data['full_name']}\n" +# message += f"Группа: {app_data['student_group'] or 'Не указана'}\n" +# +# if app_data.get('username'): +# message += f"Telegram: @{app_data['username']}\n" +# +# if app_data.get('phone_number'): +# message += f"Телефон: {app_data['phone_number']}\n" +# +# if app_data.get('email'): +# message += f"Email: {app_data['email']}\n\n" +# else: +# message += "\n" +# +# # Добавляем ответы на вопросы +# if app_data.get('answers'): +# message += "Ответы на вопросы:\n" +# for i, (question, answer) in enumerate(app_data['answers'].items(), 1): +# message += f"{i}. {question}\n{answer}\n\n" +# elif app_data.get('summary'): +# message += "Сводка анкеты:\n" +# # Преобразуем Markdown в HTML для лучшего отображения +# summary = app_data['summary'].replace('*', '').replace('_', '') +# message += f"{summary}\n\n" +# +# # Показываем прогресс +# if 'user_id' in self.user_states: +# user_id = None +# for uid, state in self.user_states.items(): +# if state.get('current_application_id') == app_data['application_id']: +# user_id = uid +# break +# +# if user_id: +# state = self.user_states[user_id] +# current_idx = state.get('current_index', 0) + 1 +# total = len(state.get('applications', [])) +# message += f"Прогресс: {current_idx} из {total}" +# +# return message +# +# def start_reviewing_applications(self, message): +# """Начинает просмотр заявок руководителем""" +# user_id = message.from_user.id +# +# # Получаем студию руководителя +# studio_id = self.get_studio_id_for_head(user_id) +# if not studio_id: +# self.bot.send_message( +# message.chat.id, +# "❌ Вы не закреплены ни за одной студией.", +# parse_mode='HTML' +# ) +# return +# +# # Получаем новые заявки +# applications = self.get_new_applications(studio_id) +# +# if not applications: +# self.bot.send_message( +# message.chat.id, +# "Нет новых заявок для рассмотрения.", +# parse_mode='HTML' +# ) +# return +# +# # Получаем название студии для сообщения +# studio_name = self.get_studio_name(studio_id) +# +# # Показываем информацию о количестве заявок +# self.bot.send_message( +# message.chat.id, +# f"{studio_name}\n" +# f"Найдено заявок: {len(applications)}\n\n", +# parse_mode='HTML' +# ) +# +# # Сохраняем состояние пользователя +# self.user_states[user_id] = { +# 'studio_id': studio_id, +# 'applications': applications, +# 'current_index': 0, +# 'status': 'reviewing' +# } +# +# # Показываем первую заявку +# self.show_next_application(message.chat.id, user_id) +# +# def show_next_application(self, chat_id: int, user_id: int): +# """Показывает следующую заявку в очереди""" +# if user_id not in self.user_states: +# return +# +# state = self.user_states[user_id] +# applications = state['applications'] +# current_index = state['current_index'] +# +# if current_index >= len(applications): +# # Все заявки обработаны +# self.bot.send_message( +# chat_id, +# "Все заявки рассмотрены!\n\n" +# "Новых заявок в очереди нет.", +# parse_mode='HTML' +# ) +# +# # Очищаем состояние +# del self.user_states[user_id] +# return +# +# # Получаем текущую заявку +# app_id, summary, full_name, created_at, username, phone, email = applications[current_index] +# +# # Получаем данные заявки +# app_data = self.get_application_details(app_id) +# +# if not app_data: +# # Если не удалось загрузить, пропускаем эту заявку +# self.bot.send_message( +# chat_id, +# f"⚠️ Ошибка загрузки заявки #{app_id}.", +# parse_mode='HTML' +# ) +# +# # Увеличиваем индекс и показываем следующую +# state['current_index'] += 1 +# self.show_next_application(chat_id, user_id) +# return +# +# # Сохраняем ID текущей заявки +# state['current_application_id'] = app_id +# +# # Форматируем сообщение +# message_text = self.format_application_message(app_data) +# +# markup = types.InlineKeyboardMarkup() +# +# markup.row( +# types.InlineKeyboardButton("✅ Принять", callback_data=f"accept:{app_id}"), +# types.InlineKeyboardButton("❌ Отклонить", callback_data=f"reject:{app_id}") +# ) +# +# markup.row( +# types.InlineKeyboardButton("⏭ Пропустить", callback_data=f"skip:{app_id}") +# ) +# +# markup.row( +# types.InlineKeyboardButton("⏹ Прекратить просмотр", callback_data="stop_review") +# ) +# +# # Отправляем сообщение +# self.bot.send_message( +# chat_id, +# message_text, +# parse_mode='HTML', +# reply_markup=markup +# ) +# +# def handle_application_action(self, call): +# +# user_id = call.from_user.id +# +# if user_id not in self.user_states: +# self.bot.answer_callback_query(call.id, "Сессия устарела") +# return +# +# action_data = call.data +# state = self.user_states[user_id] +# +# if action_data == "stop_review": +# # Прекращаем просмотр +# self.bot.answer_callback_query(call.id, "Просмотр прекращен") +# +# # Показываем сообщение о прекращении +# self.bot.edit_message_text( +# "Просмотр заявок прекращен\n\n" +# "Вы можете возобновить в любой момент.", +# call.message.chat.id, +# call.message.message_id, +# parse_mode='HTML' +# ) +# +# # Очищаем состояние +# del self.user_states[user_id] +# return +# +# +# # Извлекаем ID заявки и действие +# parts = action_data.split(":") +# if len(parts) != 2: +# self.bot.answer_callback_query(call.id, "Ошибка") +# return +# +# action, app_id_str = parts +# app_id = int(app_id_str) +# +# # Проверяем, что это текущая заявка +# if state.get('current_application_id') != app_id: +# self.bot.answer_callback_query(call.id, "Заявка уже обработана") +# return +# +# # Обрабатываем действие +# if action == "accept": +# self.accept_application(call, app_id) +# elif action == "reject": +# self.reject_application(call, app_id) +# elif action == "skip": +# self.skip_application(call, app_id) +# +# def accept_application(self, call, application_id: int): +# """Принять заявку """ +# try: +# with self.get_connection() as conn: +# cursor = conn.cursor() +# +# # Обновляем статус заявки +# cursor.execute(""" +# UPDATE applications +# SET status = 'accepted', +# processed_at = ? +# WHERE application_id = ? +# """, (datetime.now(), application_id)) +# +# conn.commit() +# +# # Получаем данные студента для уведомления +# cursor.execute(""" +# SELECT u.telegram_id, s.name, u.full_name +# FROM applications a +# JOIN users u ON a.user_id = u.user_id +# JOIN studios s ON a.studio_id = s.studio_id +# WHERE a.application_id = ? +# """, (application_id,)) +# +# result = cursor.fetchone() +# if result: +# student_telegram_id, studio_name, student_name = result +# # Отправляем уведомление студенту +# try: +# self.bot.send_message( +# student_telegram_id, +# f"🎉 Поздравляем, {student_name}!\n\n" +# f"Ваша заявка в студию '{studio_name}' одобрена!\n\n", +# parse_mode='HTML' +# ) +# except Exception as e: +# logger.error(f"Error notifying student {student_telegram_id}: {e}") +# +# self.bot.answer_callback_query(call.id, "✅ Заявка принята") +# +# # Удаляем заявку из состояния пользователя +# self.remove_application_from_state(call.from_user.id, application_id) +# +# # Удаляем сообщение с заявкой +# self.bot.delete_message(call.message.chat.id, call.message.message_id) +# +# # Показываем следующую заявку (если есть) +# self.show_next_application(call.message.chat.id, call.from_user.id) +# +# except Exception as e: +# logger.error(f"Error accepting application {application_id}: {e}") +# self.bot.answer_callback_query(call.id, "❌ Ошибка при принятии") +# +# def reject_application(self, call, application_id: int): +# """ Отклонить заявку""" +# try: +# with self.get_connection() as conn: +# cursor = conn.cursor() +# +# # Обновляем статус заявки +# cursor.execute(""" +# UPDATE applications +# SET status = 'rejected', +# processed_at = ? +# WHERE application_id = ? +# """, (datetime.now(), application_id)) +# +# conn.commit() +# +# # Получаем данные студента для уведомления +# cursor.execute(""" +# SELECT u.telegram_id, s.name, u.full_name +# FROM applications a +# JOIN users u ON a.user_id = u.user_id +# JOIN studios s ON a.studio_id = s.studio_id +# WHERE a.application_id = ? +# """, (application_id,)) +# +# result = cursor.fetchone() +# if result: +# student_telegram_id, studio_name, student_name = result +# try: +# self.bot.send_message( +# student_telegram_id, +# f"Уважаемый(ая) {student_name}!\n\n" +# f"К сожалению, ваша заявка в студию '{studio_name}' не может быть принята.\n\n" +# f"Не расстраивайтесь! Вы можете подать заявку в другие студии " +# f"или попробовать снова в следующем семестре.", +# parse_mode='HTML' +# ) +# except Exception as e: +# logger.error(f"Error notifying student {student_telegram_id}: {e}") +# +# self.bot.answer_callback_query(call.id, "❌ Заявка отклонена") +# +# # Удаляем заявку из состояния пользователя +# self.remove_application_from_state(call.from_user.id, application_id) +# +# # Удаляем сообщение с заявкой +# self.bot.delete_message(call.message.chat.id, call.message.message_id) +# +# # Показываем следующую заявку +# self.show_next_application(call.message.chat.id, call.from_user.id) +# +# except Exception as e: +# logger.error(f"Error rejecting application {application_id}: {e}") +# self.bot.answer_callback_query(call.id, "❌ Ошибка при отклонении") +# +# +# def skip_application(self, call, application_id: int): +# """Пропустить заявку (оставить в очереди)""" +# # Перемещаем заявку в конец очереди +# self.move_application_to_end(call.from_user.id, application_id) +# +# self.bot.answer_callback_query(call.id, "Заявка перемещена в конец очереди") +# +# # Удаляем текущее сообщение и показываем следующую заявку +# self.bot.delete_message(call.message.chat.id, call.message.message_id) +# self.show_next_application(call.message.chat.id, call.from_user.id) +# +# def remove_application_from_state(self, user_id: int, application_id: int): +# """Удаляет заявку из состояния пользователя""" +# if user_id not in self.user_states: +# return +# +# state = self.user_states[user_id] +# applications = state['applications'] +# +# # Удаляем заявку из списка +# new_applications = [ +# app for app in applications +# if app[0] != application_id +# ] +# +# # Обновляем список заявок +# state['applications'] = new_applications +# +# # Если текущая заявка была удалена, корректируем индекс +# if state.get('current_index') >= len(new_applications): +# state['current_index'] = max(0, len(new_applications) - 1) +# +# def move_application_to_end(self, user_id: int, application_id: int): +# """Перемещает заявку в конец очереди """ +# if user_id not in self.user_states: +# return +# +# state = self.user_states[user_id] +# applications = state['applications'] +# +# # Находим заявку +# target_application = None +# new_applications = [] +# +# for app in applications: +# if app[0] == application_id: +# target_application = app +# else: +# new_applications.append(app) +# +# # Если нашли заявку, добавляем ее в конец +# if target_application: +# new_applications.append(target_application) +# +# # Обновляем список заявок +# state['applications'] = new_applications +# +# # Корректируем текущий индекс +# # Текущая заявка будет удалена из сообщения, +# # поэтому показываем следующую по порядку +# # Индекс не меняем, так как список сдвинулся +# if state.get('current_index') >= len(new_applications): +# state['current_index'] = len(new_applications) - 1 +# +# def get_studio_name(self, studio_id: int) -> str: +# """Получает название студии по ID""" +# try: +# with self.get_connection() as conn: +# cursor = conn.cursor() +# cursor.execute( +# "SELECT name FROM studios WHERE studio_id = ?", +# (studio_id,) +# ) +# result = cursor.fetchone() +# return result[0] if result else "Неизвестная студия" +# except Exception as e: +# logger.error(f"Error getting studio name: {e}") +# return "Неизвестная студия" +# +# def get_statistics(self, studio_id: int) -> Dict[str, Any]: +# """Получает отчет по заявкам студии""" +# try: +# with self.get_connection() as conn: +# cursor = conn.cursor() +# +# cursor.execute(""" +# SELECT +# COUNT(*) as total, +# SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, +# SUM(CASE WHEN status = 'accepted' THEN 1 ELSE 0 END) as accepted, +# SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected +# FROM applications +# WHERE studio_id = ? +# """, (studio_id,)) +# +# stats = cursor.fetchone() +# +# # Статистика по дням (последние 7 дней) +# cursor.execute(""" +# SELECT +# DATE(created_at) as date, +# COUNT(*) as count +# FROM applications +# WHERE studio_id = ? +# AND created_at >= DATE('now', '-7 days') +# GROUP BY DATE(created_at) +# ORDER BY date DESC +# """, (studio_id,)) +# +# daily_stats = cursor.fetchall() +# +# +# +# except Exception as e: +# logger.error(f"Error getting statistics for studio {studio_id}: {e}") +# return {} +# +# def format_statistics_message(self, stats: Dict[str, Any], studio_name: str) -> str: +# """Форматирует сообщение с отчетом""" +# if not stats: +# return "❌ Не удалось получить отчет" +# +# message = f"отчет по заявкам студии '{studio_name}'\n\n" +# +# message += f"Общая статистика:\n" +# message += f"📋 Всего заявок: {stats.get('total', 0)}\n" +# message += f"⏳ Ожидают рассмотрения: {stats.get('pending', 0)}\n" +# message += f"✅ Принято: {stats.get('accepted', 0)}\n" +# message += f"❌ Отклонено: {stats.get('rejected', 0)}\n\n" +# +# if stats.get('daily'): +# message += "За последние 7 дней:\n" +# for date, count in stats['daily'][:7]: +# message += f"{date}: {count} заявок\n" +# message += "\n" +# +# return message \ No newline at end of file diff --git a/.venv/userrrrr.py b/.venv/userrrrr.py new file mode 100644 index 0000000..307481b --- /dev/null +++ b/.venv/userrrrr.py @@ -0,0 +1,189 @@ +import telebot +from telebot import types +import logging + +from auth import get_user_role +from database import load_active_studios +from questionnaire_flow import QuestionnaireFlow +from catalog import show_studio_catalog, show_studio_info +from users_collector import QuestionnaireBuilder +from questions import get_question_obj +from applications_manager import ApplicationsManager + +# Настройка логирования +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +bot = telebot.TeleBot("8575877961:AAHgw2xCBZagYmj1s-LwlA-dhAcgXALxVzE") + +# Инициализация компонентов +questionnaire_flow = QuestionnaireFlow(bot) +applications_manager = ApplicationsManager(bot) + +# Инициализация модуля администратора +from Admin import initialize as init_admin + +init_admin(bot) + +@bot.message_handler(commands=['start']) +def start(message): + """Главный обработчик команды /start""" + username = message.from_user.username + role = get_user_role(username) + + if role == 'admin': + from Admin import admin_start + admin_start(message) + return + elif role == 'head': + from head_of_studio import handle_head_start + handle_head_start(message) + return + + user_start(message) + +def user_start(message): + """Старт для обычного пользователя""" + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + markup.row(btn2) + + bot.send_message( + message.chat.id, + 'Привет, в этом боте ты можешь узнать про творческие студии' + ' ГУАП и стать частью активной студенческой жизни!', + parse_mode='html', + reply_markup=markup + ) + +def create_main_user_menu(): + """Создает главное меню пользователя""" + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + btn1 = types.KeyboardButton('Каталог студий') + markup.row(btn1) + btn2 = types.KeyboardButton('Мои заявки') + markup.row(btn2) + return markup + +@bot.message_handler(func=lambda message: message.text == 'Каталог студий') +def handle_catalog(message): + """Показ каталога студий""" + show_studio_catalog(bot, message.chat.id) + +@bot.message_handler(func=lambda message: message.text == 'Мои заявки') +def handle_my_applications(message): + """Показ заявок пользователя""" + user_id = message.from_user.id + applications_manager.show_user_applications(message.chat.id, user_id) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('info:')) +def handle_studio_info(call): + """Показ информации о студии""" + show_studio_info(bot, call) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('apply:')) +def handle_apply(call): + """Начало заполнения анкеты""" + studio_id = int(call.data.split(':')[1]) + questionnaire_flow.start_questionnaire(call, studio_id) + + +@bot.callback_query_handler(func=lambda call: call.data in ['use_autofill', 'manual_fill']) +def handle_autofill_choice(call): + """Обработка выбора автозаполнения""" + user_id = call.from_user.id + + if user_id not in questionnaire_flow.user_states: + bot.answer_callback_query(call.id, "Сессия устарела") + return + + state = questionnaire_flow.user_states[user_id] + studio_id = state['studio_id'] + + if call.data == 'use_autofill': + user_data = state['user_data'] + logger.info(f"Данные пользователя для автозаполнения: {user_data}") + + questions = QuestionnaireBuilder.get_question_objects(studio_id) + + for q in questions: + if q.autofill_field and q.autofill_field in user_data: + value = user_data[q.autofill_field] + if value and str(value).strip(): + # Сохраняем значение в ответы + state['answers'][q.id] = str(value) + logger.info(f"Автозаполнение: {q.id} = {value}") + + bot.edit_message_text( + "✅ Данные автозаполнены.\nПродолжаем с оставшимися вопросами...", + call.message.chat.id, + call.message.message_id + ) + + state['state'] = 'answering' + questionnaire_flow._ask_next_question(call.message.chat.id, user_id) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('answer:')) +def handle_select_answer(call): + """Обработка выбора из инлайн-клавиатуры""" + if questionnaire_flow.user_states.get(call.from_user.id, {}).get('state') == 'answering': + questionnaire_flow._handle_select_answer(call) + +@bot.message_handler(func=lambda message: True, content_types=['text']) +def handle_all_text_messages(message): + """Обработка всех текстовых сообщений""" + user_id = message.from_user.id + username = message.from_user.username + + if user_id in questionnaire_flow.user_states and questionnaire_flow.user_states[user_id].get( + 'state') == 'answering': + questionnaire_flow.process_answer(message) + return + + role = get_user_role(username) + + if role == "admin": + from Admin import handle_admin_message + handle_admin_message(message) + return + if message.text == 'Мои заявки': + bot.send_message(message.chat.id, "Функция 'Мои заявки' в разработке") + +@bot.callback_query_handler(func=lambda call: call.data == 'confirm_application') +def handle_confirm_application(call): + """Обработка подтверждения заявки""" + if call.from_user.id in questionnaire_flow.user_states: + questionnaire_flow._confirm_application(call) + + markup = create_main_user_menu() + bot.send_message( + call.message.chat.id, + "✅ Ваша заявка успешно отправлена!\n\n" + "Вы можете:\n" + "• Просмотреть свои заявки в разделе 'Мои заявки'\n", + parse_mode='HTML', + reply_markup=markup + ) + +@bot.callback_query_handler(func=lambda call: call.data == 'restart_questionnaire') +def handle_restart_questionnaire(call): + """Обработка перезаполнения анкеты""" + user_id = call.from_user.id + if user_id in questionnaire_flow.user_states: + state = questionnaire_flow.user_states[user_id] + studio_id = state['studio_id'] + + questionnaire_flow.start_questionnaire(call, studio_id) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('select_answer:')) +def handle_select_answer_callback(call): + questionnaire_flow.process_select_answer(call) + +if __name__ == '__main__': + print("Бот запущен...") + bot.polling(none_stop=True) \ No newline at end of file diff --git a/.venv/users_collector.py b/.venv/users_collector.py index 928f8dd..e29af4b 100644 --- a/.venv/users_collector.py +++ b/.venv/users_collector.py @@ -212,3 +212,11 @@ def validate_answers(cls, studio_id: int, answers: Dict[str, str]) -> tuple[bool + + + + + + + + diff --git "a/.venv/\320\262\320\276\320\277\321\200\320\276\321\201\321\213.py" "b/.venv/\320\262\320\276\320\277\321\200\320\276\321\201\321\213.py" deleted file mode 100644 index 2759065..0000000 --- "a/.venv/\320\262\320\276\320\277\321\200\320\276\321\201\321\213.py" +++ /dev/null @@ -1,224 +0,0 @@ -from typing import Dict, Any, Callable, Optional -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.ext import CallbackContext -import re - -class Question: - """Класс для хранения вопроса""" - def __init__(self, - question_id: str, - text: str, - question_type: str = 'text', - is_required: bool = True, - autofill_field: Optional[str] = None, - validation_func: Optional[Callable] = None, - options: Optional[list] = None, - keyboard: Optional[list] = None): - self.id = question_id - self.text = text - self.type = question_type # text, phone, email, boolean, select, multi_select - self.is_required = is_required - self.autofill_field = autofill_field # Поле для автозаполнения из БД - self.validation_func = validation_func - self.options = options or [] - self.keyboard = keyboard - - def validate(self, answer: str) -> tuple[bool, str]: - """Валидация ответа""" - answer = answer.strip() - - if self.is_required and not answer: - return False, "Это обязательный вопрос" - - if self.validation_func: - return self.validation_func(answer) - - # Базовая валидация по типу - if self.type == 'phone': - phone_digits = re.sub(r'[^\d]', '', answer) - if len(phone_digits) < 10: - return False, "Введите корректный номер телефона" - - elif self.type == 'email': - if '@' not in answer or '.' not in answer: - return False, "Введите корректный email" - - elif self.type == 'boolean': - if answer.lower() not in ['да', 'нет']: - return False, "Ответьте 'да' или 'нет'" - - elif self.type == 'select': - if answer not in self.options: - return False, f"Выберите вариант из списка: {', '.join(self.options)}" - - return True, "" - - -# ------ Общие вопросы------ -def ask_full_name(update: Update, context: CallbackContext) -> None: - """Вопрос: ФИО""" - update.message.reply_text("1. 📝 Укажите ваше ФИО (полностью):") - -def ask_group(update: Update, context: CallbackContext) -> None: - """Вопрос: Группа""" - update.message.reply_text("2. 🎓 Укажите номер вашей группу:") - -def ask_phone(update: Update, context: CallbackContext) -> None: - """Вопрос: Телефон""" - update.message.reply_text("3. 📱 Укажите ваш номер телефона:") - -def ask_email(update: Update, context: CallbackContext) -> None: - """Вопрос: Email""" - update.message.reply_text("4. 📧 Укажите ваш email адрес:") - -def ask_experience(update: Update, context: CallbackContext) -> None: - """Вопрос: Опыт в деятельности""" - update.message.reply_text("5. 💼 Расскажите о вашем опыте в данной деятельности:") - -def ask_motivation(update: Update, context: CallbackContext) -> None: - """Вопрос: Мотивация""" - update.message.reply_text("6. 🎯 Почему вы хотите вступить в эту студию?") - -def ask_expectations(update: Update, context: CallbackContext) -> None: - """Вопрос: Ожидания""" - update.message.reply_text("7. 🌟 Какие у вас ожидания от участия в студии?") - -def ask_activeness(update: Update, context: CallbackContext) -> None: - """Вопрос: Участие в мероприятиях""" - keyboard = [ - [InlineKeyboardButton("Да", callback_data="activeness_yes"), - InlineKeyboardButton("Нет", callback_data="activeness_no")] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - "8. 📢 Готовы ли вы участвовать в мероприятиях студии?", - reply_markup=reply_markup - ) - -# ------ дополнительные опросы ------ - -def ask_dance_style(update: Update, context: CallbackContext) -> None: - """Вопрос: Стиль танца""" - keyboard = [ - [InlineKeyboardButton("Хип-хоп", callback_data="dance_hiphop")], - [InlineKeyboardButton("Бальные танцы", callback_data="dance_ballroom")], - [InlineKeyboardButton("Современные", callback_data="dance_modern")], - [InlineKeyboardButton("Народные", callback_data="dance_folk")] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - "9. 💃 Какой танцевальный стиль вам интересен?", - reply_markup=reply_markup - ) - -def ask_music_instrument(update: Update, context: CallbackContext) -> None: - """Вопрос: Музыкальный инструмент""" - update.message.reply_text("10. 🎵 На каком инструменте вы играете (если играете)?") - -def ask_singing_experience(update: Update, context: CallbackContext) -> None: - """Вопрос: Опыт пения""" - update.message.reply_text("11. 🎤 Есть ли у вас опыт пения? Если да, опишите его:") - -def ask_theater_role(update: Update, context: CallbackContext) -> None: - """Вопрос: Предпочитаемые роли в театре""" - keyboard = [ - [InlineKeyboardButton("Драматические", callback_data="role_drama")], - [InlineKeyboardButton("Комические", callback_data="role_comedy")], - [InlineKeyboardButton("Характерные", callback_data="role_character")], - [InlineKeyboardButton("Не знаю", callback_data="role_unknown")] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - "12. 🎭 Какие роли вам интересны в театре?", - reply_markup=reply_markup - ) - -def ask_tech_skills(update: Update, context: CallbackContext) -> None: - """Вопрос: Технические навыки""" - update.message.reply_text("13. 💻 Какими техническими навыками вы владеете?") - -def ask_organizational_exp(update: Update, context: CallbackContext) -> None: - """Вопрос: Организационный опыт""" - update.message.reply_text("14. 📋 Был ли у вас опыт организации мероприятий? Опишите:") - -def ask_leadership(update: Update, context: CallbackContext) -> None: - """Вопрос: Лидерские качества""" - keyboard = [ - [InlineKeyboardButton("Да, есть опыт", callback_data="leadership_yes")], - [InlineKeyboardButton("Хочу развивать", callback_data="leadership_develop")], - [InlineKeyboardButton("Пока нет", callback_data="leadership_no")] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - "15. 👨‍💼 Есть ли у вас лидерский опыт?", - reply_markup=reply_markup - ) - -def ask_project_ideas(update: Update, context: CallbackContext) -> None: - """Вопрос: Идеи для проектов""" - update.message.reply_text("16. 💡 Есть ли у вас идеи для проектов в студии?") - -def ask_time_commitment(update: Update, context: CallbackContext) -> None: - """Вопрос: Временные возможности""" - update.message.reply_text("17. ⏰ Сколько часов в неделю вы готовы уделять студии?") - -def ask_why_choose_us(update: Update, context: CallbackContext) -> None: - """Вопрос: Почему выбрали нас""" - update.message.reply_text("18. ❓ Почему вы выбрали именно нашу студию?") - -def ask_feedback_source(update: Update, context: CallbackContext) -> None: - """Вопрос: Откуда узнали о студии""" - keyboard = [ - [InlineKeyboardButton("От друзей", callback_data="source_friends")], - [InlineKeyboardButton("Соцсети", callback_data="source_social")], - [InlineKeyboardButton("Сайт вуза", callback_data="source_website")], - [InlineKeyboardButton("Объявление", callback_data="source_poster")] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - update.message.reply_text( - "19. 📢 Откуда вы узнали о нашей студии?", - reply_markup=reply_markup - ) - -def ask_special_needs(update: Update, context: CallbackContext) -> None: - """Вопрос: Особые потребности""" - update.message.reply_text("20. ♿ Есть ли у вас особые потребности, которые мы должны учитывать?") - - -# ========== СЛОВАРЬ ВСЕХ ВОПРОСОВ ========== -ALL_QUESTIONS = { - # ID вопроса: (функция-обработчик, объект Question) - 'full_name': (ask_full_name, Question('full_name', 'ФИО', autofill_field='full_name')), - 'group': (ask_group, Question('group', 'Группа', autofill_field='student_group')), - 'phone': (ask_phone, Question('phone', 'Телефон', 'phone', autofill_field='phone')), - 'email': (ask_email, Question('email', 'Email', 'email', autofill_field='email')), - 'experience': (ask_experience, Question('experience', 'Опыт в деятельности')), - 'motivation': (ask_motivation, Question('motivation', 'Мотивация вступления')), - 'expectations': (ask_expectations, Question('expectations', 'Ожидания от студии')), - 'activeness': (ask_activeness, Question('activeness', 'Участие в мероприятиях', 'boolean')), - 'dance_style': (ask_dance_style, Question('dance_style', 'Танцевальный стиль', 'select', - options=['Хип-хоп', 'Бальные танцы', 'Современные', 'Народные'])), - 'music_instrument': (ask_music_instrument, Question('music_instrument', 'Музыкальный инструмент')), - 'singing_experience': (ask_singing_experience, Question('singing_experience', 'Опыт пения')), - 'theater_role': (ask_theater_role, Question('theater_role', 'Театральные роли', 'select', - options=['Драматические', 'Комические', 'Характерные', 'Не знаю'])), - 'tech_skills': (ask_tech_skills, Question('tech_skills', 'Технические навыки')), - 'organizational_exp': (ask_organizational_exp, Question('organizational_exp', 'Организационный опыт')), - 'leadership': (ask_leadership, Question('leadership', 'Лидерский опыт', 'select', - options=['Да, есть опыт', 'Хочу развивать', 'Пока нет'])), - 'project_ideas': (ask_project_ideas, Question('project_ideas', 'Идеи для проектов')), - 'time_commitment': (ask_time_commitment, Question('time_commitment', 'Временные возможности')), - 'why_choose_us': (ask_why_choose_us, Question('why_choose_us', 'Почему выбрали нас')), - 'feedback_source': (ask_feedback_source, Question('feedback_source', 'Источник информации', 'select', - options=['От друзей', 'Соцсети', 'Сайт вуза', 'Объявление'])), - 'special_needs': (ask_special_needs, Question('special_needs', 'Особые потребности', is_required=False)) -} - -def get_question_handler(question_id: str): - """Возвращает обработчик вопроса по ID""" - return ALL_QUESTIONS.get(question_id, (None, None))[0] - -def get_question_obj(question_id: str) -> Optional[Question]: - """Возвращает объект вопроса по ID""" - result = ALL_QUESTIONS.get(question_id) - return result[1] if result else None diff --git "a/.venv/\321\201\320\261\320\276\321\200\321\211\320\270\320\272_\320\260\320\275\320\272\320\265\321\202.py" "b/.venv/\321\201\320\261\320\276\321\200\321\211\320\270\320\272_\320\260\320\275\320\272\320\265\321\202.py" deleted file mode 100644 index 6777ec1..0000000 --- "a/.venv/\321\201\320\261\320\276\321\200\321\211\320\270\320\272_\320\260\320\275\320\272\320\265\321\202.py" +++ /dev/null @@ -1,221 +0,0 @@ -from typing import Dict, List, Any, Optional, Callable -from .вопросы import get_question_handler, get_question_obj, ALL_QUESTIONS - -class QuestionnaireBuilder: - """Сборщик анкет для разных студий""" - - STUDIO_QUESTIONNAIRES = { - # ID студии: [список ID вопросов из ALL_QUESTIONS] - 1: { # Танцевальная студия - 'name': 'Танцевальная студия', - 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone', # 3. Телефон - 'email', # 4. Email - 'dance_style', # 5. Стиль танца - 'experience', # 6. Опыт - 'motivation', # 7. Мотивация - 'activeness', # 8. Участие в мероприятиях - ] - }, - 2: { # КВН - 'name': 'КВН', - 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone', # 3. Телефон - 'email', # 4. Email - 'experience', # 5. Опыт - 'why_choose_us', # 6. Почему выбрали нас - 'time_commitment',# 7. Временные возможности - ] - }, - 3: { # Театральная студия - 'name': 'Театральная студия', - 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone', # 3. Телефон - 'email', # 4. Email - 'theater_role', # 5. Театральные роли - 'experience', # 6. Опыт - 'motivation', # 7. Мотивация - 'activeness', # 8. Участие в мероприятиях - ] - }, - 4: { # МУЗГУАП - 'name': 'МУЗГУАП', - 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone', # 3. Телефон - 'email', # 4. Email - 'music_instrument', # 5. Музыкальный инструмент - 'singing_experience', # 6. Опыт пения - 'expectations', # 7. Ожидания - 'activeness', # 8. Участие в мероприятиях - ] - }, - 5: { # Медиацентр - 'name': 'Медиацентр', - 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone', # 3. Телефон - 'email', # 4. Email - 'tech_skills', # 5. Технические навыки - 'experience', # 6. Опыт - 'project_ideas', # 7. Идеи для проектов - 'time_commitment',# 8. Временные возможности - ] - }, - 6: { # Техничка - 'name': 'Студия технического обеспечения мероприятий', - 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone', # 3. Телефон - 'email', # 4. Email - 'tech_skills', # 5. Технические навыки - 'organizational_exp', # 6. Организационный опыт - 'leadership', # 7. Лидерский опыт - 'feedback_source',# 8. Откуда узнали - ] - }, - 7: { # Студия организации мероприятий - 'name': 'Студия организации мероприятий', - 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone', # 3. Телефон - 'email', # 4. Email - 'organizational_exp', # 5. Организационный опыт - 'motivation', # 6. Мотивация - 'activeness', # 7. Участие в мероприятиях - 'tech_skills', # 8. Технические навыки - ] - }, - 8: { # студия ведущих - 'name': 'Студия ведущих', - 'questions': [ - 'full_name', # 1. ФИО - 'group', # 2. Группа - 'phone', # 3. Телефон - 'email', # 4. Email - 'experience', # 5. Опыт в деятельности - 'project_ideas', # 6. Идеи для проектов - 'expectations', # 7. Ожидания - 'time_commitment',# 8. Временные возможности - ] - }, - } - - @classmethod - def get_questionnaire_for_studio(cls, studio_id: int) -> Optional[Dict[str, Any]]: - """Получает конфигурацию анкеты для студии""" - return cls.STUDIO_QUESTIONNAIRES.get(studio_id) - - @classmethod - def get_studio_name(cls, studio_id: int) -> str: - """Получает название студии""" - config = cls.get_questionnaire_for_studio(studio_id) - return config.get('name', 'Неизвестная студия') if config else 'Неизвестная студия' - - @classmethod - def get_question_sequence(cls, studio_id: int) -> List[str]: - """Получает последовательность вопросов для студии""" - config = cls.get_questionnaire_for_studio(studio_id) - return config.get('questions', []) if config else [] - - @classmethod - def build_questionnaire_handlers(cls, studio_id: int) -> List[Callable]: - """Собирает список обработчиков вопросов для студии""" - question_ids = cls.get_question_sequence(studio_id) - handlers = [] - - for qid in question_ids: - handler = get_question_handler(qid) - if handler: - handlers.append(handler) - - return handlers - - @classmethod - def get_question_objects(cls, studio_id: int) -> List[Any]: - """Получает объекты вопросов для студии""" - question_ids = cls.get_question_sequence(studio_id) - questions = [] - - for qid in question_ids: - q_obj = get_question_obj(qid) - if q_obj: - questions.append(q_obj) - - return questions - - @classmethod - def get_autofill_fields(cls, studio_id: int) -> List[str]: - """Получает список полей для автозаполнения""" - questions = cls.get_question_objects(studio_id) - autofill_fields = [] - - for q in questions: - if q.autofill_field: - autofill_fields.append(q.autofill_field) - - # Убираем дубликаты - return list(set(autofill_fields)) - - @classmethod - def can_autofill(cls, studio_id: int, user_data: Dict[str, Any]) -> bool: - """Проверяет, можно ли автозаполнить анкету""" - autofill_fields = cls.get_autofill_fields(studio_id) - - if not autofill_fields: - return False - - # Проверяем, сколько полей можно автозаполнить - fillable_count = 0 - for field in autofill_fields: - value = user_data.get(field, '') - if value and str(value).strip(): - fillable_count += 1 - - return fillable_count == len(autofill_fields) - - @classmethod - def generate_summary(cls, studio_id: int, answers: Dict[str, str]) -> str: - """Генерирует сводку по ответам""" - questions = cls.get_question_objects(studio_id) - studio_name = cls.get_studio_name(studio_id) - - lines = [f"📋 *Анкета для студии: {studio_name}*\n"] - - for i, q in enumerate(questions, 1): - answer = answers.get(q.id, "❌ Не заполнено") - lines.append(f"\n{i}. *{q.text}*\n{answer}") - - return "\n".join(lines) - - @classmethod - def validate_answers(cls, studio_id: int, answers: Dict[str, str]) -> tuple[bool, Dict[str, str]]: - """Валидирует все ответы анкеты""" - questions = cls.get_question_objects(studio_id) - errors = {} - - for q in questions: - answer = answers.get(q.id, '').strip() - - if q.is_required and not answer: - errors[q.id] = "Обязательный вопрос не заполнен" - continue - - is_valid, error_msg = q.validate(answer) - if not is_valid: - errors[q.id] = error_msg - - return len(errors) == 0, errors - - -