Compare commits
10
Commits
af88cbce8f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c681a5c1d7 | ||
|
|
45138bb234 | ||
|
|
caac6c3a08 | ||
|
|
4b5b24c97c | ||
|
|
8680778584 | ||
|
|
7b6c2a3245 | ||
|
|
389a763571 | ||
|
|
4767a1af15 | ||
|
|
f03362c313 | ||
|
|
e844392479 |
@@ -0,0 +1,190 @@
|
|||||||
|
from telebot import types, TeleBot
|
||||||
|
from database.models import Categories, Admins
|
||||||
|
from bot.handlers.start import startMarkup
|
||||||
|
from bot.utils.crud_helpers import create_entity_markup
|
||||||
|
|
||||||
|
# TODO: Add Option for showing parent categories when creating a new category.
|
||||||
|
|
||||||
|
|
||||||
|
def register(bot: TeleBot):
|
||||||
|
cancelMarkup = types.InlineKeyboardMarkup()
|
||||||
|
cancelMarkup.add(types.InlineKeyboardButton(
|
||||||
|
"Cancel", callback_data="cancel"))
|
||||||
|
|
||||||
|
CATEGORY_FIELDS = [
|
||||||
|
('name', "the category's name"),
|
||||||
|
('description', "The description for category (<Optional>) ('s' to skip)"),
|
||||||
|
('parent_id', "The parent category id (<Optional>) ('s' to skip))")
|
||||||
|
]
|
||||||
|
EDITABLE_FIELDS = {
|
||||||
|
'name': 1,
|
||||||
|
'description': 2,
|
||||||
|
'parent_id': 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# Showing details of a Category
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('category_'))
|
||||||
|
def show_category_details(call):
|
||||||
|
category_id = call.data.split('_')[1]
|
||||||
|
category = Categories.getCategoryById(category_id)
|
||||||
|
|
||||||
|
if category:
|
||||||
|
details = f"""
|
||||||
|
<b>🔖 Category Profile</b>
|
||||||
|
|
||||||
|
<b>Name:</b> {category[1]}
|
||||||
|
<b>Parent Category:</b> {category[3]}
|
||||||
|
<b>Description:</b> \n{category[2]}
|
||||||
|
"""
|
||||||
|
details.strip()
|
||||||
|
markup = create_entity_markup("category", category_id, True)
|
||||||
|
markup.add(types.InlineKeyboardButton("📁 Show Top Courses",
|
||||||
|
callback_data=f"categoryCourses_{category_id}"))
|
||||||
|
|
||||||
|
bot.send_message(call.message.chat.id, details,
|
||||||
|
reply_markup=markup, parse_mode="HTML")
|
||||||
|
else:
|
||||||
|
bot.send_message(call.message.chat.id, "Category not found.")
|
||||||
|
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
# Creating a Category Flow
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data == 'create_category')
|
||||||
|
def start_category_creation(call):
|
||||||
|
msg = bot.send_message(call.message.chat.id,
|
||||||
|
"Please enter following data: (enter any key to start)",
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, collect_field, {}, 0)
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
def collect_field(message, data, step):
|
||||||
|
# Save previous field
|
||||||
|
if step > 0:
|
||||||
|
field_name = CATEGORY_FIELDS[step - 1][0]
|
||||||
|
data[field_name] = None if (
|
||||||
|
(field_name in ['description', 'parent_id']) and message.text == 's') else message.text
|
||||||
|
|
||||||
|
# Done collecting?
|
||||||
|
if step >= len(CATEGORY_FIELDS):
|
||||||
|
show_confirmation(message, data)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ask next question
|
||||||
|
field_name, prompt = CATEGORY_FIELDS[step]
|
||||||
|
msg = bot.send_message(message.chat.id, f"Now enter {prompt}:",
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, collect_field, data, step + 1)
|
||||||
|
|
||||||
|
def show_confirmation(message, data):
|
||||||
|
summary = "Is this correct? (enter any key to continue or cancel to exit)\n\n" + "\n".join(
|
||||||
|
f"{name.replace('_', ' ').title()}: {data[name]}"
|
||||||
|
for name, _ in CATEGORY_FIELDS
|
||||||
|
)
|
||||||
|
msg = bot.send_message(message.chat.id, summary,
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, create_category, data)
|
||||||
|
|
||||||
|
def create_category(message, data):
|
||||||
|
if Categories.createCategory(**data):
|
||||||
|
bot.send_message(message.chat.id, "✅ Category created!",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "❌ Failed to create category.", reply_markup=startMarkup())
|
||||||
|
|
||||||
|
# Editing a Category Flow
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('edit_category_'))
|
||||||
|
def start_category_editing(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
category_id = call.data.split('_')[2]
|
||||||
|
category = Categories.getCategoryById(category_id)
|
||||||
|
|
||||||
|
if not category:
|
||||||
|
bot.send_message(call.message.chat.id, "Category not found.")
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
editMarkup = types.ReplyKeyboardMarkup(
|
||||||
|
resize_keyboard=True, one_time_keyboard=True)
|
||||||
|
for field in list(EDITABLE_FIELDS.keys()) + ['Cancel']:
|
||||||
|
editMarkup.add(types.KeyboardButton(field.capitalize()))
|
||||||
|
|
||||||
|
msg = bot.send_message(call.message.chat.id,
|
||||||
|
"Please enter the field you want to edit: ", reply_markup=editMarkup)
|
||||||
|
|
||||||
|
bot.register_next_step_handler(
|
||||||
|
msg, process_field_select, category)
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
def process_field_select(message, category: tuple):
|
||||||
|
field = message.text.lower()
|
||||||
|
if field == 'cancel' or field not in EDITABLE_FIELDS:
|
||||||
|
msg = "Action cancelled." if field == 'cancel' else "Invalid field. Action cancelled."
|
||||||
|
bot.send_message(message.chat.id, msg,
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
return
|
||||||
|
|
||||||
|
current_value = category[EDITABLE_FIELDS[field]]
|
||||||
|
|
||||||
|
msg = bot.send_message(
|
||||||
|
message.chat.id, f"Current value is: {current_value}.\n Please enter new value for {field}:", reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(
|
||||||
|
msg, process_value_edit, category[0], field, current_value)
|
||||||
|
|
||||||
|
def process_value_edit(message, category_id, field, previous_value):
|
||||||
|
new_value = message.text
|
||||||
|
|
||||||
|
# Handle cancellation
|
||||||
|
if new_value.lower() == 'cancel' or new_value == f"{previous_value}":
|
||||||
|
msg = "Action cancelled." if new_value.lower(
|
||||||
|
) == 'cancel' else f"No changes made to {field}."
|
||||||
|
bot.send_message(message.chat.id, msg, reply_markup=startMarkup())
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update Category
|
||||||
|
if Categories.updateCategory(category_id, **{field: new_value}):
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, f"✅ Category's {field} updated successfully.", reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, f"❌ Failed to update Category's {field}.", reply_markup=startMarkup())
|
||||||
|
|
||||||
|
# Deleting a Category
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('delete_category_'))
|
||||||
|
def delete_category(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
category_id = call.data.split('_')[2]
|
||||||
|
if (Categories.deleteCategory(category_id)):
|
||||||
|
bot.send_message(call.message.chat.id, "✅ Category deleted.",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(call.message.chat.id, "❌ Failed to delete Category.",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
# Show Top Courses in a Category
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('categoryCourses_'))
|
||||||
|
def category_courses(call):
|
||||||
|
category_id = call.data.split('_')[1]
|
||||||
|
courses = Categories.getTopCoursesByCategory(category_id)
|
||||||
|
|
||||||
|
markup = types.InlineKeyboardMarkup()
|
||||||
|
if courses:
|
||||||
|
for course in courses:
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
course[1], callback_data=f"course_{course[0]}"))
|
||||||
|
bot.send_message(
|
||||||
|
call.message.chat.id, "📚 Top Courses in this Category:", reply_markup=markup)
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
call.message.chat.id, "There are no courses in this category.", reply_markup=markup)
|
||||||
|
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
from telebot import TeleBot, types
|
||||||
|
from database.models import Courses, Admins
|
||||||
|
from bot.utils.crud_helpers import create_entity_markup
|
||||||
|
from bot.utils.formatters import format_course_info, format_course_review
|
||||||
|
from bot.handlers.start import startMarkup
|
||||||
|
|
||||||
|
# TODO: Add Option for selecting Teacher in a more user-friendly way (name instead of ID) + pagination for it.
|
||||||
|
# TODO: Add Buttons for offering multiple choices for fields like language and difficulty.
|
||||||
|
|
||||||
|
|
||||||
|
def register(bot: TeleBot):
|
||||||
|
cancelMarkup = types.InlineKeyboardMarkup()
|
||||||
|
cancelMarkup.add(types.InlineKeyboardButton(
|
||||||
|
"Cancel", callback_data="cancel"))
|
||||||
|
|
||||||
|
COURSE_FIELDS = [
|
||||||
|
('name', "the course's name"),
|
||||||
|
('teacher_id', "The course's Teacher ID"),
|
||||||
|
('description', "description (<Optional>) ('s' to skip)"),
|
||||||
|
('language', "language ('english', 'spanish', 'german', 'french', 'persian')"),
|
||||||
|
('difficulty', "difficulty (<Optional>) ('s' to skip) ('beginner', 'intermediate', 'expert')"),
|
||||||
|
]
|
||||||
|
EDITABLE_FIELDS = {
|
||||||
|
'name': 1,
|
||||||
|
'description': 5,
|
||||||
|
'difficulty': 6,
|
||||||
|
'language': 7
|
||||||
|
}
|
||||||
|
|
||||||
|
# Showing details of a Course
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('course_'))
|
||||||
|
def show_course_details(call):
|
||||||
|
course_id = call.data.split('_')[1]
|
||||||
|
course = Courses.getCourseById(course_id)
|
||||||
|
|
||||||
|
if not course:
|
||||||
|
bot.send_message(call.message.chat.id, "Course not found.")
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
details = format_course_info(course)
|
||||||
|
markup = create_entity_markup("course", course_id, True)
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
f"👨🏫 Teacher's Info", callback_data=f"teacher_{course[3]}"))
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
f" 📰 Course Reviews", callback_data=f"courseReviews_{course_id}"))
|
||||||
|
|
||||||
|
bot.send_message(call.message.chat.id, details,
|
||||||
|
reply_markup=markup, parse_mode="HTML")
|
||||||
|
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
# Creating a Course Flow
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data == 'create_course')
|
||||||
|
def start_course_creation(call):
|
||||||
|
msg = bot.send_message(call.message.chat.id,
|
||||||
|
"Please enter following data: (enter any key to start)",
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, collect_field, {}, 0)
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
def collect_field(message, data, step):
|
||||||
|
# Save previous field
|
||||||
|
if step > 0:
|
||||||
|
field_name = COURSE_FIELDS[step - 1][0]
|
||||||
|
data[field_name] = None if (
|
||||||
|
(field_name in ['description', 'difficulty']) and message.text == 's') else message.text
|
||||||
|
|
||||||
|
# Done collecting?
|
||||||
|
if step >= len(COURSE_FIELDS):
|
||||||
|
show_confirmation(message, data)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ask next question
|
||||||
|
field_name, prompt = COURSE_FIELDS[step]
|
||||||
|
msg = bot.send_message(message.chat.id, f"Now enter {prompt}:",
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, collect_field, data, step + 1)
|
||||||
|
|
||||||
|
def show_confirmation(message, data):
|
||||||
|
summary = "Is this correct? (enter any key to continue or cancel to exit)\n\n" + "\n".join(
|
||||||
|
f"{name.replace('_', ' ').title()}: {data[name]}"
|
||||||
|
for name, _ in COURSE_FIELDS
|
||||||
|
)
|
||||||
|
msg = bot.send_message(message.chat.id, summary,
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, create_course, data)
|
||||||
|
|
||||||
|
def create_course(message, data):
|
||||||
|
if Courses.createCourse(**data):
|
||||||
|
bot.send_message(message.chat.id, "✅ Course created!",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "❌ Failed to create course.", reply_markup=startMarkup())
|
||||||
|
|
||||||
|
# Editing a Course Flow
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('edit_course_'))
|
||||||
|
def start_course_editing(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
course_id = call.data.split('_')[2]
|
||||||
|
course = Courses.getCourseById(course_id)
|
||||||
|
|
||||||
|
if not course:
|
||||||
|
bot.send_message(call.message.chat.id, "Course not found.")
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
editMarkup = types.ReplyKeyboardMarkup(
|
||||||
|
resize_keyboard=True, one_time_keyboard=True)
|
||||||
|
for field in list(EDITABLE_FIELDS.keys()) + ['Cancel']:
|
||||||
|
editMarkup.add(types.KeyboardButton(field.capitalize()))
|
||||||
|
|
||||||
|
msg = bot.send_message(call.message.chat.id,
|
||||||
|
"Please enter the field you want to edit: ", reply_markup=editMarkup)
|
||||||
|
|
||||||
|
bot.register_next_step_handler(
|
||||||
|
msg, process_field_select, course)
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
def process_field_select(message, course: tuple):
|
||||||
|
field = message.text.lower()
|
||||||
|
if field == 'cancel' or field not in EDITABLE_FIELDS:
|
||||||
|
msg = "Action cancelled." if field == 'cancel' else "Invalid field. Action cancelled."
|
||||||
|
bot.send_message(message.chat.id, msg,
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
return
|
||||||
|
|
||||||
|
current_value = course[EDITABLE_FIELDS[field]]
|
||||||
|
|
||||||
|
msg = bot.send_message(
|
||||||
|
message.chat.id, f"Current value is: {current_value}.\n Please enter new value for {field}:", reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(
|
||||||
|
msg, process_value_edit, course[0], field, current_value)
|
||||||
|
|
||||||
|
def process_value_edit(message, course_id, field, previous_value):
|
||||||
|
new_value = message.text
|
||||||
|
|
||||||
|
# Handle cancellation
|
||||||
|
if new_value.lower() == 'cancel' or new_value == f"{previous_value}":
|
||||||
|
msg = "Action cancelled." if new_value.lower(
|
||||||
|
) == 'cancel' else f"No changes made to {field}."
|
||||||
|
bot.send_message(message.chat.id, msg, reply_markup=startMarkup())
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update teacher
|
||||||
|
if Courses.updateCourse(course_id, **{field: new_value}):
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, f"✅ Course's {field} updated successfully.", reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, f"❌ Failed to update Course's {field}.", reply_markup=startMarkup())
|
||||||
|
|
||||||
|
# Deleting a Teacher
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('delete_course_'))
|
||||||
|
def delete_course(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
course_id = call.data.split('_')[2]
|
||||||
|
if (Courses.deleteCourse(course_id)):
|
||||||
|
bot.send_message(call.message.chat.id, "✅ Course deleted.",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(call.message.chat.id, "❌ Failed to delete Course.",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
# Show Some of the course reviews
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('courseReviews_'))
|
||||||
|
def show_course_reviews(call):
|
||||||
|
course_id = call.data.split('_')[1]
|
||||||
|
reviews = Courses.getCourseReviews(course_id)
|
||||||
|
|
||||||
|
if reviews:
|
||||||
|
for rev in reviews:
|
||||||
|
details = format_course_review(rev)
|
||||||
|
bot.send_message(call.message.chat.id,
|
||||||
|
details, parse_mode="HTML")
|
||||||
|
else:
|
||||||
|
bot.send_message(call.message.chat.id, "No reviews yet")
|
||||||
|
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
+16
-1
@@ -1,7 +1,22 @@
|
|||||||
from . import students, teachers
|
from . import students, teachers, courses, tags, categories
|
||||||
|
from bot.handlers.start import startMarkup
|
||||||
|
|
||||||
|
# TODO: Create a new entity for Reviews (Change The Corresponding codes from courses and students to use that)
|
||||||
|
# TODO: Add Option for handling all updates at once.
|
||||||
|
|
||||||
|
|
||||||
def register_all_callbacks(bot):
|
def register_all_callbacks(bot):
|
||||||
"""Register all callback handlers"""
|
"""Register all callback handlers"""
|
||||||
students.register(bot)
|
students.register(bot)
|
||||||
teachers.register(bot)
|
teachers.register(bot)
|
||||||
|
courses.register(bot)
|
||||||
|
tags.register(bot)
|
||||||
|
categories.register(bot)
|
||||||
|
|
||||||
|
# Cancel is common among all
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data == 'cancel')
|
||||||
|
def cancel_action(call):
|
||||||
|
bot.clear_step_handler(call.message)
|
||||||
|
bot.send_message(call.message.chat.id,
|
||||||
|
"Action cancelled.", reply_markup=startMarkup())
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|||||||
+213
-5
@@ -1,20 +1,228 @@
|
|||||||
from telebot import types
|
from telebot import types, TeleBot
|
||||||
from database.models import Students
|
from database.models import Students, Admins
|
||||||
from bot.utils.formatters import format_student_info
|
from bot.utils.formatters import format_course_review, format_student_info
|
||||||
|
from bot.utils.crud_helpers import create_entity_markup
|
||||||
|
from bot.handlers.start import startMarkup
|
||||||
|
|
||||||
|
|
||||||
def register(bot):
|
def register(bot: TeleBot):
|
||||||
|
cancelMarkup = types.InlineKeyboardMarkup()
|
||||||
|
cancelMarkup.add(types.InlineKeyboardButton(
|
||||||
|
"Cancel", callback_data="cancel"))
|
||||||
|
|
||||||
|
STUDENT_FIELDS = [
|
||||||
|
('name', "the student's name"),
|
||||||
|
('email', "email"),
|
||||||
|
('phone_number', "phone (<Optional>) ('s' to skip)"),
|
||||||
|
('password', "password"),
|
||||||
|
('username', "username"),
|
||||||
|
('birthday', "birthday (YY/MM/DD)"),
|
||||||
|
]
|
||||||
|
EDITABLE_FIELDS = {
|
||||||
|
'name': 2,
|
||||||
|
'email': 4,
|
||||||
|
'phone_number': 5,
|
||||||
|
'password': 3,
|
||||||
|
'username': 1,
|
||||||
|
'birthday': 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Showing details of a Student
|
||||||
@bot.callback_query_handler(func=lambda call: call.data.startswith('student_'))
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('student_'))
|
||||||
def show_student_details(call):
|
def show_student_details(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
student_id = call.data.split('_')[1]
|
student_id = call.data.split('_')[1]
|
||||||
student = Students.getStudentById(student_id)
|
student = Students.getStudentById(student_id)
|
||||||
|
|
||||||
if student:
|
if student:
|
||||||
details = format_student_info(student)
|
details = format_student_info(student)
|
||||||
markup = types.InlineKeyboardMarkup()
|
markup = create_entity_markup("student", student_id, True)
|
||||||
|
markup.add(types.InlineKeyboardButton("📚 Show Courses 🔒",
|
||||||
|
callback_data=f"studentCourses_{student_id}"))
|
||||||
|
markup.add(types.InlineKeyboardButton("📰 Reviews By Student 🔒",
|
||||||
|
callback_data=f"studentReviews_{student_id}"))
|
||||||
|
|
||||||
bot.send_message(call.message.chat.id, details,
|
bot.send_message(call.message.chat.id, details,
|
||||||
reply_markup=markup, parse_mode="HTML")
|
reply_markup=markup, parse_mode="HTML")
|
||||||
else:
|
else:
|
||||||
bot.send_message(call.message.chat.id, "Student not found.")
|
bot.send_message(call.message.chat.id, "Student not found.")
|
||||||
|
|
||||||
bot.answer_callback_query(call.id)
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
# Creating a Student Flow
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data == 'create_student')
|
||||||
|
def create_student(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = bot.send_message(call.message.chat.id,
|
||||||
|
"Please enter following data: (enter any key to start)",
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, collect_field, {}, 0)
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
def collect_field(message, data, step):
|
||||||
|
# Save previous field
|
||||||
|
if step > 0:
|
||||||
|
field_name = STUDENT_FIELDS[step - 1][0]
|
||||||
|
data[field_name] = None if (
|
||||||
|
(field_name == 'phone_number') and message.text == 's') else message.text
|
||||||
|
|
||||||
|
# Done collecting?
|
||||||
|
if step >= len(STUDENT_FIELDS):
|
||||||
|
show_confirmation(message, data)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ask next question
|
||||||
|
field_name, prompt = STUDENT_FIELDS[step]
|
||||||
|
msg = bot.send_message(message.chat.id, f"Now enter {prompt}:",
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, collect_field, data, step + 1)
|
||||||
|
|
||||||
|
def show_confirmation(message, data):
|
||||||
|
summary = "Is this correct? (enter any key to continue or cancel to exit)\n\n" + "\n".join(
|
||||||
|
f"{name.replace('_', ' ').title()}: {data[name]}"
|
||||||
|
for name, _ in STUDENT_FIELDS
|
||||||
|
)
|
||||||
|
msg = bot.send_message(message.chat.id, summary,
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, create_student, data)
|
||||||
|
|
||||||
|
def create_student(message, data):
|
||||||
|
if Students.createStudent(**data):
|
||||||
|
bot.send_message(message.chat.id, "✅ Student created!",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "❌ Failed to create student.", reply_markup=startMarkup())
|
||||||
|
|
||||||
|
# Editing a Student Flow
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('edit_student_'))
|
||||||
|
def start_student_editing(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
student_id = call.data.split('_')[2]
|
||||||
|
student = Students.getStudentById(student_id)
|
||||||
|
|
||||||
|
if not student:
|
||||||
|
bot.send_message(call.message.chat.id, "Student not found.")
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
editMarkup = types.ReplyKeyboardMarkup(
|
||||||
|
resize_keyboard=True, one_time_keyboard=True)
|
||||||
|
for field in list(EDITABLE_FIELDS.keys()) + ['Cancel']:
|
||||||
|
editMarkup.add(types.KeyboardButton(field.capitalize()))
|
||||||
|
|
||||||
|
msg = bot.send_message(call.message.chat.id,
|
||||||
|
"Please enter the field you want to edit: ", reply_markup=editMarkup)
|
||||||
|
|
||||||
|
bot.register_next_step_handler(
|
||||||
|
msg, process_field_select, student)
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
def process_field_select(message, student: tuple):
|
||||||
|
field = message.text.lower()
|
||||||
|
if field == 'cancel' or field not in EDITABLE_FIELDS:
|
||||||
|
msg = "Action cancelled." if field == 'cancel' else "Invalid field. Action cancelled."
|
||||||
|
bot.send_message(message.chat.id, msg,
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
return
|
||||||
|
|
||||||
|
current_value = student[EDITABLE_FIELDS[field]]
|
||||||
|
|
||||||
|
msg = bot.send_message(
|
||||||
|
message.chat.id, f"Current value is: {current_value if field != 'password' else '********'}.\n Please enter new value for {field}:", reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(
|
||||||
|
msg, process_value_edit, student[0], field, current_value)
|
||||||
|
|
||||||
|
def process_value_edit(message, student_id, field, previous_value):
|
||||||
|
new_value = message.text
|
||||||
|
|
||||||
|
# Handle cancellation
|
||||||
|
if new_value.lower() == 'cancel' or new_value == f"{previous_value}":
|
||||||
|
msg = "Action cancelled." if new_value.lower(
|
||||||
|
) == 'cancel' else f"No changes made to {field}."
|
||||||
|
bot.send_message(message.chat.id, msg, reply_markup=startMarkup())
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update student
|
||||||
|
if Students.updateStudent(student_id, **{field: new_value}):
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, f"✅ Student's {field} updated successfully.", reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, f"❌ Failed to update Student's {field}.", reply_markup=startMarkup())
|
||||||
|
|
||||||
|
# Deleting a Teacher
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('delete_student_'))
|
||||||
|
def delete_student(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
student_id = call.data.split('_')[2]
|
||||||
|
if (Students.deleteStudent(student_id)):
|
||||||
|
bot.send_message(call.message.chat.id, "✅ Student deleted.",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(call.message.chat.id, "❌ Failed to delete Student.",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
# Showing Courses a Students has enrolled in
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith("studentCourses_"))
|
||||||
|
def show_student_course(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
student_id = call.data.split('_')[1]
|
||||||
|
courses = Students.getStudentCourses(student_id)
|
||||||
|
|
||||||
|
if courses:
|
||||||
|
markup = types.InlineKeyboardMarkup(row_width=1)
|
||||||
|
for c in courses:
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
f"{c[1][:26] + "..."} | id#{c[0]}", callback_data=f"course_{c[0]}")
|
||||||
|
)
|
||||||
|
|
||||||
|
bot.send_message(call.message.chat.id,
|
||||||
|
"Courses: ", reply_markup=markup)
|
||||||
|
else:
|
||||||
|
bot.send_message(call.message.chat.id, "No courses.")
|
||||||
|
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
# Showing Reviews a Students has written
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith("studentReviews_"))
|
||||||
|
def show_student_reviews(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
student_id = call.data.split('_')[1]
|
||||||
|
reviews = Students.getStudentReviews(student_id)
|
||||||
|
|
||||||
|
if reviews:
|
||||||
|
for rev in reviews:
|
||||||
|
details = format_course_review(rev)
|
||||||
|
bot.send_message(call.message.chat.id,
|
||||||
|
details, parse_mode="HTML")
|
||||||
|
|
||||||
|
else:
|
||||||
|
bot.send_message(call.message.chat.id, "No reviews.")
|
||||||
|
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
from telebot import types, TeleBot
|
||||||
|
from database.models import Tags, Admins
|
||||||
|
from bot.handlers.start import startMarkup
|
||||||
|
from bot.utils.crud_helpers import create_entity_markup
|
||||||
|
|
||||||
|
|
||||||
|
def register(bot: TeleBot):
|
||||||
|
cancelMarkup = types.InlineKeyboardMarkup()
|
||||||
|
cancelMarkup.add(types.InlineKeyboardButton(
|
||||||
|
"Cancel", callback_data="cancel"))
|
||||||
|
|
||||||
|
TAG_FIELDS = [
|
||||||
|
('name', "the tag's name"),
|
||||||
|
('slug', "The slug for tag")
|
||||||
|
]
|
||||||
|
EDITABLE_FIELDS = {
|
||||||
|
'name': 1,
|
||||||
|
'slug': 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Showing details of a Tag
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('tag_'))
|
||||||
|
def show_tag_details(call):
|
||||||
|
tag_id = call.data.split('_')[1]
|
||||||
|
tag = Tags.getTagById(tag_id)
|
||||||
|
|
||||||
|
if tag:
|
||||||
|
details = f"""
|
||||||
|
<b>🔖 Tag Profile</b>
|
||||||
|
|
||||||
|
<b>Name:</b> {tag[1]}
|
||||||
|
<b>slug:</b> {tag[2]}
|
||||||
|
"""
|
||||||
|
details.strip()
|
||||||
|
markup = create_entity_markup("tag", tag_id, True)
|
||||||
|
markup.add(types.InlineKeyboardButton("📁 Show Top Courses",
|
||||||
|
callback_data=f"tagCourses_{tag_id}"))
|
||||||
|
|
||||||
|
bot.send_message(call.message.chat.id, details,
|
||||||
|
reply_markup=markup, parse_mode="HTML")
|
||||||
|
else:
|
||||||
|
bot.send_message(call.message.chat.id, "Tag not found.")
|
||||||
|
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
# Creating a Tag Flow
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data == 'create_tag')
|
||||||
|
def start_tag_creation(call):
|
||||||
|
msg = bot.send_message(call.message.chat.id,
|
||||||
|
"Please enter following data: (enter any key to start)",
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, collect_field, {}, 0)
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
def collect_field(message, data, step):
|
||||||
|
# Save previous field
|
||||||
|
if step > 0:
|
||||||
|
field_name = TAG_FIELDS[step - 1][0]
|
||||||
|
data[field_name] = message.text
|
||||||
|
|
||||||
|
# Done collecting?
|
||||||
|
if step >= len(TAG_FIELDS):
|
||||||
|
show_confirmation(message, data)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ask next question
|
||||||
|
field_name, prompt = TAG_FIELDS[step]
|
||||||
|
msg = bot.send_message(message.chat.id, f"Now enter {prompt}:",
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, collect_field, data, step + 1)
|
||||||
|
|
||||||
|
def show_confirmation(message, data):
|
||||||
|
summary = "Is this correct? (enter any key to continue or cancel to exit)\n\n" + "\n".join(
|
||||||
|
f"{name.replace('_', ' ').title()}: {data[name]}"
|
||||||
|
for name, _ in TAG_FIELDS
|
||||||
|
)
|
||||||
|
msg = bot.send_message(message.chat.id, summary,
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, create_tag, data)
|
||||||
|
|
||||||
|
def create_tag(message, data):
|
||||||
|
if Tags.createTag(**data):
|
||||||
|
bot.send_message(message.chat.id, "✅ Tag created!",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "❌ Failed to create tag.", reply_markup=startMarkup())
|
||||||
|
|
||||||
|
# Editing a Tag Flow
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('edit_tag_'))
|
||||||
|
def start_tag_editing(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
tag_id = call.data.split('_')[2]
|
||||||
|
tag = Tags.getTagById(tag_id)
|
||||||
|
|
||||||
|
if not tag:
|
||||||
|
bot.send_message(call.message.chat.id, "Tag not found.")
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
editMarkup = types.ReplyKeyboardMarkup(
|
||||||
|
resize_keyboard=True, one_time_keyboard=True)
|
||||||
|
for field in list(EDITABLE_FIELDS.keys()) + ['Cancel']:
|
||||||
|
editMarkup.add(types.KeyboardButton(field.capitalize()))
|
||||||
|
|
||||||
|
msg = bot.send_message(call.message.chat.id,
|
||||||
|
"Please enter the field you want to edit: ", reply_markup=editMarkup)
|
||||||
|
|
||||||
|
bot.register_next_step_handler(
|
||||||
|
msg, process_field_select, tag)
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
def process_field_select(message, tag: tuple):
|
||||||
|
field = message.text.lower()
|
||||||
|
if field == 'cancel' or field not in EDITABLE_FIELDS:
|
||||||
|
msg = "Action cancelled." if field == 'cancel' else "Invalid field. Action cancelled."
|
||||||
|
bot.send_message(message.chat.id, msg,
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
return
|
||||||
|
|
||||||
|
current_value = tag[EDITABLE_FIELDS[field]]
|
||||||
|
|
||||||
|
msg = bot.send_message(
|
||||||
|
message.chat.id, f"Current value is: {current_value}.\n Please enter new value for {field}:", reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(
|
||||||
|
msg, process_value_edit, tag[0], field, current_value)
|
||||||
|
|
||||||
|
def process_value_edit(message, tag_id, field, previous_value):
|
||||||
|
new_value = message.text
|
||||||
|
|
||||||
|
# Handle cancellation
|
||||||
|
if new_value.lower() == 'cancel' or new_value == f"{previous_value}":
|
||||||
|
msg = "Action cancelled." if new_value.lower(
|
||||||
|
) == 'cancel' else f"No changes made to {field}."
|
||||||
|
bot.send_message(message.chat.id, msg, reply_markup=startMarkup())
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update tag
|
||||||
|
if Tags.updateTag(tag_id, **{field: new_value}):
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, f"✅ Tag's {field} updated successfully.", reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, f"❌ Failed to update Tag's {field}.", reply_markup=startMarkup())
|
||||||
|
|
||||||
|
# Deleting a Tag
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('delete_tag_'))
|
||||||
|
def delete_tag(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
tag_id = call.data.split('_')[2]
|
||||||
|
if (Tags.deleteTag(tag_id)):
|
||||||
|
bot.send_message(call.message.chat.id, "✅ Tag deleted.",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(call.message.chat.id, "❌ Failed to delete Tag.",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
# Show Top Courses with a Tag
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('tagCourses_'))
|
||||||
|
def delete_tag(call):
|
||||||
|
tag_id = call.data.split('_')[1]
|
||||||
|
courses = Tags.getTopCoursesByTag(tag_id)
|
||||||
|
|
||||||
|
markup = types.InlineKeyboardMarkup()
|
||||||
|
if courses:
|
||||||
|
for course in courses:
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
course[1], callback_data=f"course_{course[0]}"))
|
||||||
|
bot.send_message(
|
||||||
|
call.message.chat.id, "📚 Top Courses with this Tag:", reply_markup=markup)
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
call.message.chat.id, "There are no courses with this Tag.", reply_markup=markup)
|
||||||
|
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
+142
-76
@@ -1,6 +1,8 @@
|
|||||||
from telebot import types, TeleBot
|
from telebot import types, TeleBot
|
||||||
from database.models import Teachers
|
from database.models import Teachers, Admins
|
||||||
from bot.utils.formatters import format_teacher_info
|
from bot.utils.formatters import format_teacher_info
|
||||||
|
from bot.handlers.start import startMarkup
|
||||||
|
from bot.utils.crud_helpers import create_entity_markup
|
||||||
|
|
||||||
|
|
||||||
def register(bot: TeleBot):
|
def register(bot: TeleBot):
|
||||||
@@ -8,15 +10,37 @@ def register(bot: TeleBot):
|
|||||||
cancelMarkup.add(types.InlineKeyboardButton(
|
cancelMarkup.add(types.InlineKeyboardButton(
|
||||||
"Cancel", callback_data="cancel"))
|
"Cancel", callback_data="cancel"))
|
||||||
|
|
||||||
|
TEACHER_FIELDS = [
|
||||||
|
('name', "the teacher's name"),
|
||||||
|
('email', "email"),
|
||||||
|
('phone_number', "phone (<Optional>) ('s' to skip)"),
|
||||||
|
('password', "password"),
|
||||||
|
('username', "username"),
|
||||||
|
('birthday', "birthday (YY/MM/DD)"),
|
||||||
|
('about_me', "about me"),
|
||||||
|
('job_title', "job title"),
|
||||||
|
]
|
||||||
|
EDITABLE_FIELDS = {
|
||||||
|
'name': 2,
|
||||||
|
'email': 4,
|
||||||
|
'phone_number': 5,
|
||||||
|
'password': 3,
|
||||||
|
'username': 1,
|
||||||
|
'birthday': 8,
|
||||||
|
'about_me': 9,
|
||||||
|
'job_title': 10
|
||||||
|
}
|
||||||
|
|
||||||
# Showing details of a Teacher
|
# Showing details of a Teacher
|
||||||
@bot.callback_query_handler(func=lambda call: call.data.startswith('teacher_'))
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('teacher_'))
|
||||||
def show_teacher_details(call):
|
def show_teacher_details(call):
|
||||||
teacher_id = call.data.split('_')[1]
|
teacher_id = call.data.split('_')[1]
|
||||||
teacher = Teachers.getTeachertById(teacher_id)
|
teacher = Teachers.getTeacherById(teacher_id)
|
||||||
|
|
||||||
if teacher:
|
if teacher:
|
||||||
details = format_teacher_info(teacher)
|
details = format_teacher_info(teacher)
|
||||||
markup = types.InlineKeyboardMarkup()
|
markup = create_entity_markup("teacher", teacher_id, True)
|
||||||
|
|
||||||
bot.send_message(call.message.chat.id, details,
|
bot.send_message(call.message.chat.id, details,
|
||||||
reply_markup=markup, parse_mode="HTML")
|
reply_markup=markup, parse_mode="HTML")
|
||||||
else:
|
else:
|
||||||
@@ -27,84 +51,126 @@ def register(bot: TeleBot):
|
|||||||
# Creating a Teacher Flow
|
# Creating a Teacher Flow
|
||||||
@bot.callback_query_handler(func=lambda call: call.data == 'create_teacher')
|
@bot.callback_query_handler(func=lambda call: call.data == 'create_teacher')
|
||||||
def start_teacher_creation(call):
|
def start_teacher_creation(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
msg = bot.send_message(call.message.chat.id,
|
msg = bot.send_message(call.message.chat.id,
|
||||||
"Please enter the teacher's name:", reply_markup=cancelMarkup)
|
"Please enter following data: (enter any key to start)",
|
||||||
bot.register_next_step_handler(
|
|
||||||
msg, process_name_step)
|
|
||||||
bot.answer_callback_query(call.id)
|
|
||||||
|
|
||||||
def process_name_step(message):
|
|
||||||
name = message.text
|
|
||||||
msg = bot.send_message(
|
|
||||||
message.chat.id, f"Name: {name}\n\nNow enter email:", reply_markup=cancelMarkup)
|
|
||||||
bot.register_next_step_handler(msg, process_email_step, name)
|
|
||||||
|
|
||||||
def process_email_step(message, name):
|
|
||||||
email = message.text
|
|
||||||
msg = bot.send_message(
|
|
||||||
message.chat.id, f"Email: {email}\n\nNow enter phone: (<Optional>)", reply_markup=cancelMarkup)
|
|
||||||
bot.register_next_step_handler(msg, process_phone_step, name, email)
|
|
||||||
|
|
||||||
def process_phone_step(message, name, email):
|
|
||||||
phone = message.text
|
|
||||||
msg = bot.send_message(
|
|
||||||
message.chat.id, f"Phone: {phone}\n\nNow enter password: ", reply_markup=cancelMarkup)
|
|
||||||
bot.register_next_step_handler(
|
|
||||||
msg, process_password_step, name, email, phone)
|
|
||||||
|
|
||||||
def process_password_step(message, name, email, phone):
|
|
||||||
password = message.text
|
|
||||||
msg = bot.send_message(
|
|
||||||
message.chat.id, f"password: {password}\n\nNow enter username:", reply_markup=cancelMarkup)
|
|
||||||
bot.register_next_step_handler(
|
|
||||||
msg, process_username_step, name, email, phone, password)
|
|
||||||
|
|
||||||
def process_username_step(message, name, email, phone, password):
|
|
||||||
username = message.text
|
|
||||||
msg = bot.send_message(
|
|
||||||
message.chat.id, f"username: {username}\n\nNow enter birthday: (YY/MM/DD)", reply_markup=cancelMarkup)
|
|
||||||
bot.register_next_step_handler(
|
|
||||||
msg, process_birthday_step, name, email, phone, password, username)
|
|
||||||
|
|
||||||
def process_birthday_step(message, name, email, phone, password, username):
|
|
||||||
birthday = message.text
|
|
||||||
msg = bot.send_message(
|
|
||||||
message.chat.id, f"birthday: {birthday}\n\nNow enter about me: ", reply_markup=cancelMarkup)
|
|
||||||
bot.register_next_step_handler(
|
|
||||||
msg, process_aboutme_step, name, email, phone, password, username, birthday)
|
|
||||||
|
|
||||||
def process_aboutme_step(message, name, email, phone, password, username, birthday):
|
|
||||||
about_me = message.text
|
|
||||||
msg = bot.send_message(
|
|
||||||
message.chat.id, f"about me: {about_me}\n\nNow enter job title: ", reply_markup=cancelMarkup)
|
|
||||||
bot.register_next_step_handler(
|
|
||||||
msg, process_jobtitle_step, name, email, phone, password, username, birthday, about_me)
|
|
||||||
|
|
||||||
def process_jobtitle_step(message, name, email, phone, password, username, birthday, about_me):
|
|
||||||
job_title = message.text
|
|
||||||
msg = bot.send_message(message.chat.id, "is this correct? (enter any key) (Use Cancel to Stop creating)\n"
|
|
||||||
f"Name: {name}\n"
|
|
||||||
f"Email: {email}\n"
|
|
||||||
f"Phone: {phone}\n"
|
|
||||||
f"Password: {password}\n"
|
|
||||||
f"Birthday: {birthday}\n"
|
|
||||||
f"About Me: {about_me}\n"
|
|
||||||
f"Job Title: {job_title}",
|
|
||||||
reply_markup=cancelMarkup)
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, collect_field, {}, 0)
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
|
def collect_field(message, data, step):
|
||||||
|
# Save previous field
|
||||||
|
if step > 0:
|
||||||
|
field_name = TEACHER_FIELDS[step - 1][0]
|
||||||
|
data[field_name] = None if (
|
||||||
|
(field_name == 'phone_number') and message.text == 's') else message.text
|
||||||
|
|
||||||
|
# Done collecting?
|
||||||
|
if step >= len(TEACHER_FIELDS):
|
||||||
|
show_confirmation(message, data)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ask next question
|
||||||
|
field_name, prompt = TEACHER_FIELDS[step]
|
||||||
|
msg = bot.send_message(message.chat.id, f"Now enter {prompt}:",
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, collect_field, data, step + 1)
|
||||||
|
|
||||||
|
def show_confirmation(message, data):
|
||||||
|
summary = "Is this correct? (enter any key to continue or cancel to exit)\n\n" + "\n".join(
|
||||||
|
f"{name.replace('_', ' ').title()}: {data[name]}"
|
||||||
|
for name, _ in TEACHER_FIELDS
|
||||||
|
)
|
||||||
|
msg = bot.send_message(message.chat.id, summary,
|
||||||
|
reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(msg, create_teacher, data)
|
||||||
|
|
||||||
|
def create_teacher(message, data):
|
||||||
|
if Teachers.createTeacher(**data):
|
||||||
|
bot.send_message(message.chat.id, "✅ Teacher created!",
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "❌ Failed to create teacher.", reply_markup=startMarkup())
|
||||||
|
|
||||||
|
# Editing a Teacher Flow
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('edit_teacher_'))
|
||||||
|
def start_teacher_editing(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
teacher_id = call.data.split('_')[2]
|
||||||
|
teacher = Teachers.getTeacherById(teacher_id)
|
||||||
|
|
||||||
|
if not teacher:
|
||||||
|
bot.send_message(call.message.chat.id, "Teacher not found.")
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
editMarkup = types.ReplyKeyboardMarkup(
|
||||||
|
resize_keyboard=True, one_time_keyboard=True)
|
||||||
|
for field in list(EDITABLE_FIELDS.keys()) + ['Cancel']:
|
||||||
|
editMarkup.add(types.KeyboardButton(field.capitalize()))
|
||||||
|
|
||||||
|
msg = bot.send_message(call.message.chat.id,
|
||||||
|
"Please enter the field you want to edit: ", reply_markup=editMarkup)
|
||||||
|
|
||||||
bot.register_next_step_handler(
|
bot.register_next_step_handler(
|
||||||
msg, confirm_teacher_creation, name, email, phone, password, username, birthday, about_me, job_title)
|
msg, process_field_select, teacher)
|
||||||
|
bot.answer_callback_query(call.id)
|
||||||
|
|
||||||
def confirm_teacher_creation(message, name, email, phone, password, username, birthday, about_me, job_title):
|
def process_field_select(message, teacher: tuple):
|
||||||
if (Teachers.createTeacher(name=name, email=email, phone_number=phone,
|
field = message.text.lower()
|
||||||
password=password, username=username, birthday=birthday,
|
if field == 'cancel' or field not in EDITABLE_FIELDS:
|
||||||
about_me=about_me, job_title=job_title)):
|
msg = "Action cancelled." if field == 'cancel' else "Invalid field. Action cancelled."
|
||||||
bot.send_message(message.chat.id, "✅ Teacher created!")
|
bot.send_message(message.chat.id, msg,
|
||||||
|
reply_markup=startMarkup())
|
||||||
|
return
|
||||||
|
|
||||||
|
current_value = teacher[EDITABLE_FIELDS[field]]
|
||||||
|
|
||||||
|
msg = bot.send_message(
|
||||||
|
message.chat.id, f"Current value is: {current_value if field != 'password' else '********'}.\n Please enter new value for {field}:", reply_markup=cancelMarkup)
|
||||||
|
bot.register_next_step_handler(
|
||||||
|
msg, process_value_edit, teacher[0], field, current_value)
|
||||||
|
|
||||||
|
def process_value_edit(message, teacher_id, field, previous_value):
|
||||||
|
new_value = message.text
|
||||||
|
|
||||||
|
# Handle cancellation
|
||||||
|
if new_value.lower() == 'cancel' or new_value == f"{previous_value}":
|
||||||
|
msg = "Action cancelled." if new_value.lower(
|
||||||
|
) == 'cancel' else f"No changes made to {field}."
|
||||||
|
bot.send_message(message.chat.id, msg, reply_markup=startMarkup())
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update teacher
|
||||||
|
if Teachers.updateTeacher(teacher_id, **{field: new_value}):
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, f"✅ Teacher's {field} updated successfully.", reply_markup=startMarkup())
|
||||||
else:
|
else:
|
||||||
bot.send_message(message.chat.id, "❌ Failed to create teacher.")
|
bot.send_message(
|
||||||
|
message.chat.id, f"❌ Failed to update Teacher's {field}.", reply_markup=startMarkup())
|
||||||
|
|
||||||
|
# Deleting a Teacher
|
||||||
|
@bot.callback_query_handler(func=lambda call: call.data.startswith('delete_teacher_'))
|
||||||
|
def delete_teacher(call):
|
||||||
|
if not Admins.is_authenticated(call.from_user.id):
|
||||||
|
bot.answer_callback_query(call.id, "⛔ Unauthorized access!")
|
||||||
|
bot.send_message(call.message.chat.id, "Please /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
@bot.callback_query_handler(func=lambda call: call.data == 'cancel')
|
teacher_id = call.data.split('_')[2]
|
||||||
def cancel_action(call):
|
if (Teachers.deleteTeacher(teacher_id)):
|
||||||
bot.clear_step_handler(call.message)
|
bot.send_message(call.message.chat.id, "✅ Teacher deleted.",
|
||||||
bot.send_message(call.message.chat.id, "Action cancelled.")
|
reply_markup=startMarkup())
|
||||||
|
else:
|
||||||
|
bot.send_message(call.message.chat.id, "❌ Failed to delete Teacher.",
|
||||||
|
reply_markup=startMarkup())
|
||||||
bot.answer_callback_query(call.id)
|
bot.answer_callback_query(call.id)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import logging
|
||||||
|
from telebot import types
|
||||||
|
from database.models import Categories
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def register(bot):
|
||||||
|
@bot.message_handler(func=lambda message: message.text == "Show Categories")
|
||||||
|
def get_categories(message):
|
||||||
|
try:
|
||||||
|
data = Categories.getAllCategories()
|
||||||
|
if data:
|
||||||
|
markup = types.InlineKeyboardMarkup(row_width=2)
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
btn = types.InlineKeyboardButton(
|
||||||
|
f"{row[1]} | id#{row[0]}", callback_data=f"category_{row[0]}")
|
||||||
|
markup.add(btn)
|
||||||
|
|
||||||
|
# add button for creating a new category
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
"➕ Create New Category", callback_data="create_category"))
|
||||||
|
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "Here is the data:", reply_markup=markup)
|
||||||
|
else:
|
||||||
|
markup = types.InlineKeyboardMarkup(row_width=1)
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
"➕ Create New Category", callback_data="create_category"))
|
||||||
|
|
||||||
|
bot.reply_to(message, "No data found.", reply_markup=markup)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in 'Show Categories' handler: {e}")
|
||||||
|
bot.reply_to(message, "Sorry, an error occurred.")
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
|
from telebot import types
|
||||||
|
from bot.handlers.start import startMessage
|
||||||
|
|
||||||
|
|
||||||
def register(bot):
|
def register(bot):
|
||||||
@bot.message_handler(func=lambda message: True)
|
@bot.message_handler(func=lambda message: True)
|
||||||
def handle_unknown(message):
|
def handle_unknown(message):
|
||||||
bot.reply_to(message, f"You said: {message.text}")
|
bot.reply_to(
|
||||||
|
message, f"You said: {message.text}. send /start to get started.")
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import logging
|
||||||
|
from telebot import types
|
||||||
|
from database.models import Courses
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def register(bot):
|
||||||
|
@bot.message_handler(func=lambda message: message.text == "Show Courses")
|
||||||
|
def get_students(message):
|
||||||
|
try:
|
||||||
|
data = Courses.getAllCourses()
|
||||||
|
if data:
|
||||||
|
markup = types.InlineKeyboardMarkup(row_width=2)
|
||||||
|
for row in data:
|
||||||
|
btn = types.InlineKeyboardButton(
|
||||||
|
f"{row[1][:26] + "..."} | id#{row[0]}", callback_data=f"course_{row[0]}")
|
||||||
|
markup.add(btn)
|
||||||
|
|
||||||
|
# add button for creating a new course
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
"➕ Create New Course", callback_data="create_course"))
|
||||||
|
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "Here is the data:", reply_markup=markup)
|
||||||
|
else:
|
||||||
|
markup = types.InlineKeyboardMarkup(row_width=1)
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
"➕ Create New Course", callback_data="create_course"))
|
||||||
|
bot.reply_to(message, "No data found.", reply_markup=markup)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in (Show Courses) handler: {e}")
|
||||||
|
bot.reply_to(message, "Sorry, an error occurred.")
|
||||||
+18
-1
@@ -1,13 +1,30 @@
|
|||||||
from . import start, students, teachers, common
|
from . import start, students, teachers, courses, tags, categories, common, login
|
||||||
from bot.callbacks.init import register_all_callbacks
|
from bot.callbacks.init import register_all_callbacks
|
||||||
|
from telebot import types
|
||||||
|
|
||||||
|
|
||||||
def register_all_handlers(bot):
|
def register_all_handlers(bot):
|
||||||
"""Register all handlers and callbacks"""
|
"""Register all handlers and callbacks"""
|
||||||
|
# Set bot commands
|
||||||
|
commands = [
|
||||||
|
types.BotCommand(command="start", description="Start the bot"),
|
||||||
|
types.BotCommand(command="login", description="Login to your account"),
|
||||||
|
types.BotCommand(command="logout",
|
||||||
|
description="Logout of your account"),
|
||||||
|
types.BotCommand(command="session",
|
||||||
|
description="Info about your session"),
|
||||||
|
]
|
||||||
|
|
||||||
|
bot.set_my_commands(commands)
|
||||||
|
|
||||||
# Message handlers
|
# Message handlers
|
||||||
start.register(bot)
|
start.register(bot)
|
||||||
|
login.register(bot)
|
||||||
students.register(bot)
|
students.register(bot)
|
||||||
teachers.register(bot)
|
teachers.register(bot)
|
||||||
|
courses.register(bot)
|
||||||
|
tags.register(bot)
|
||||||
|
categories.register(bot)
|
||||||
common.register(bot) # Must be last (catch-all)
|
common.register(bot) # Must be last (catch-all)
|
||||||
|
|
||||||
# Callback handlers
|
# Callback handlers
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
from telebot import types, TeleBot
|
||||||
|
from database.models import Admins
|
||||||
|
from bot.handlers.start import startMarkup
|
||||||
|
|
||||||
|
|
||||||
|
def register(bot: TeleBot):
|
||||||
|
|
||||||
|
@bot.message_handler(commands=['login'])
|
||||||
|
def start_login(message):
|
||||||
|
# Check if already authenticated
|
||||||
|
if Admins.is_authenticated(message.from_user.id):
|
||||||
|
session_info = Admins.get_session_info(message.from_user.id)
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id,
|
||||||
|
f"✅ You are already logged in!\n\n"
|
||||||
|
f"Session expires in: {session_info['time_remaining']}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = bot.send_message(
|
||||||
|
message.chat.id,
|
||||||
|
"🔐 Please enter your username:"
|
||||||
|
)
|
||||||
|
bot.register_next_step_handler(msg, get_password)
|
||||||
|
|
||||||
|
def get_password(message):
|
||||||
|
"""Get username and ask for password"""
|
||||||
|
username = message.text.strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
bot.delete_message(message.chat.id, message.message_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
msg = bot.send_message(
|
||||||
|
message.chat.id,
|
||||||
|
f"Username: {username}\n\n🔑 Now enter your password:"
|
||||||
|
)
|
||||||
|
bot.register_next_step_handler(msg, verify_and_login, username)
|
||||||
|
|
||||||
|
def verify_and_login(message, username):
|
||||||
|
"""Verify credentials and create session"""
|
||||||
|
password = message.text.strip()
|
||||||
|
telegram_id = message.from_user.id
|
||||||
|
|
||||||
|
try:
|
||||||
|
bot.delete_message(message.chat.id, message.message_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Attempt login (this creates the session)
|
||||||
|
if Admins.login(username, password, telegram_id):
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id,
|
||||||
|
f"✅ Login successful!\n\n"
|
||||||
|
f"Welcome back, {username}!\n"
|
||||||
|
f"Session valid for: {Admins.SESSION_TIMEOUT_HOURS} hours\n"
|
||||||
|
f"Use /session for info about your session, or /logout to logout.",
|
||||||
|
reply_markup=startMarkup()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id,
|
||||||
|
"❌ Invalid credentials or unauthorized Telegram account.\n\n"
|
||||||
|
"Please check your username and password.\nContact @HoseinA05 for support."
|
||||||
|
)
|
||||||
|
|
||||||
|
@bot.message_handler(commands=['logout'])
|
||||||
|
def logout(message):
|
||||||
|
"""Logout user manually"""
|
||||||
|
|
||||||
|
if not Admins.is_authenticated(message.from_user.id):
|
||||||
|
bot.send_message(message.chat.id, "You are not logged in.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if Admins.logout(message.from_user.id):
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id,
|
||||||
|
"✅ You have been logged out successfully.\n\n"
|
||||||
|
"Use /login to access admin features again."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "❌ Logout failed. Please try again later.")
|
||||||
|
|
||||||
|
@bot.message_handler(commands=['session'])
|
||||||
|
def check_session(message):
|
||||||
|
"""Check current session status"""
|
||||||
|
session_info = Admins.get_session_info(message.from_user.id)
|
||||||
|
|
||||||
|
if not session_info:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "❌ You don't have an admin account.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if session_info['is_active']:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id,
|
||||||
|
f"✅ Active Session\n\n"
|
||||||
|
f"Username: {session_info['username']}\n"
|
||||||
|
f"Last Login: {session_info['last_login']}\n"
|
||||||
|
f"Time Remaining: {session_info['time_remaining']}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id,
|
||||||
|
f"⏱️ Session Expired\n\n"
|
||||||
|
f"Username: {session_info['username']}\n"
|
||||||
|
f"Please use /login to continue."
|
||||||
|
)
|
||||||
+23
-21
@@ -1,29 +1,31 @@
|
|||||||
from telebot import types
|
from telebot import types
|
||||||
from bot.utils.auth import is_authenticated
|
|
||||||
|
# TODO: Add Pagincation to Showing rows of entities (To all entities)
|
||||||
|
|
||||||
|
|
||||||
def register(bot):
|
def register(bot):
|
||||||
@bot.message_handler(commands=["start"])
|
@bot.message_handler(commands=["start"])
|
||||||
def start_handler(message):
|
def start_handler(message):
|
||||||
if is_authenticated(message.from_user.id):
|
startMessage(bot, message=message)
|
||||||
bot.send_message(
|
|
||||||
message.chat.id, "authorized user! Welcome.")
|
|
||||||
else:
|
|
||||||
bot.send_message(
|
|
||||||
message.chat.id, "Welcome! Use buttons to fetch from database.")
|
|
||||||
|
|
||||||
markup = types.ReplyKeyboardMarkup(
|
|
||||||
row_width=2, one_time_keyboard=True, resize_keyboard=True)
|
|
||||||
btn1 = types.KeyboardButton("Show Students")
|
|
||||||
btn2 = types.KeyboardButton("Show Teachers")
|
|
||||||
btn3 = types.KeyboardButton("Show Courses")
|
|
||||||
btn4 = types.KeyboardButton("Show Tag")
|
|
||||||
btn5 = types.KeyboardButton("Show Categories")
|
|
||||||
markup.add(btn1, btn2, btn3, btn4, btn5)
|
|
||||||
bot.reply_to(message, "Use buttons to fetch from database.",
|
|
||||||
reply_markup=markup)
|
|
||||||
|
|
||||||
@bot.message_handler(func=lambda message: message.text in ["Show Courses", "Show Tag", "Show Categories"])
|
def startMessage(bot, message):
|
||||||
def handle_test_options(message):
|
# if is_authenticated(message.from_user.id):
|
||||||
option = message.text
|
# bot.send_message(
|
||||||
bot.send_message(message.chat.id, f"You selected: {option}")
|
# message.chat.id, "authorized user! Welcome.")
|
||||||
|
|
||||||
|
markup = startMarkup()
|
||||||
|
bot.reply_to(message, "Use buttons to fetch from database.",
|
||||||
|
reply_markup=markup)
|
||||||
|
|
||||||
|
|
||||||
|
def startMarkup():
|
||||||
|
markup = types.ReplyKeyboardMarkup(
|
||||||
|
row_width=2, one_time_keyboard=True, resize_keyboard=True)
|
||||||
|
btn1 = types.KeyboardButton("Show Students")
|
||||||
|
btn2 = types.KeyboardButton("Show Teachers")
|
||||||
|
btn3 = types.KeyboardButton("Show Courses")
|
||||||
|
btn4 = types.KeyboardButton("Show Tags")
|
||||||
|
btn5 = types.KeyboardButton("Show Categories")
|
||||||
|
markup.add(btn1, btn2, btn3, btn4, btn5)
|
||||||
|
return markup
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import logging
|
import logging
|
||||||
from telebot import types
|
from telebot import types, TeleBot
|
||||||
from database.models import Students
|
from database.models import Students, Admins
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def register(bot):
|
def register(bot: TeleBot):
|
||||||
@bot.message_handler(func=lambda message: message.text == "Show Students")
|
@bot.message_handler(func=lambda message: message.text == "Show Students")
|
||||||
def get_students(message):
|
def get_students(message):
|
||||||
|
if not Admins.is_authenticated(message.from_user.id):
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "⛔ Unauthorized access!\nPlease /login first.")
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = Students.getAllStudents()
|
data = Students.getAllStudents()
|
||||||
if data:
|
if data:
|
||||||
@@ -17,10 +22,18 @@ def register(bot):
|
|||||||
btn = types.InlineKeyboardButton(
|
btn = types.InlineKeyboardButton(
|
||||||
f"@{row[1]} | id#{row[0]}", callback_data=f"student_{row[0]}")
|
f"@{row[1]} | id#{row[0]}", callback_data=f"student_{row[0]}")
|
||||||
markup.add(btn)
|
markup.add(btn)
|
||||||
|
|
||||||
|
# add button for creating a new student
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
"➕ Create New Student 🔒", callback_data="create_student"))
|
||||||
|
|
||||||
bot.send_message(
|
bot.send_message(
|
||||||
message.chat.id, "Here is the data:", reply_markup=markup)
|
message.chat.id, "Here is the data:", reply_markup=markup)
|
||||||
else:
|
else:
|
||||||
bot.reply_to(message, "No data found.")
|
markup = types.InlineKeyboardMarkup(row_width=1)
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
"➕ Create New Student 🔒", callback_data="create_student"))
|
||||||
|
bot.reply_to(message, "No data found.", reply_markup=markup)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in get_students handler: {e}")
|
logger.error(f"Error in get_students handler: {e}")
|
||||||
bot.reply_to(message, "Sorry, an error occurred.")
|
bot.reply_to(message, "Sorry, an error occurred.")
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import logging
|
||||||
|
from telebot import types, TeleBot
|
||||||
|
from database.models import Tags
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def register(bot: TeleBot):
|
||||||
|
@bot.message_handler(func=lambda message: message.text == "Show Tags")
|
||||||
|
def get_tags(message):
|
||||||
|
try:
|
||||||
|
data = Tags.getAllTags()
|
||||||
|
if data:
|
||||||
|
markup = types.InlineKeyboardMarkup(row_width=2)
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
btn = types.InlineKeyboardButton(
|
||||||
|
f"{row[1]} | id#{row[0]}", callback_data=f"tag_{row[0]}")
|
||||||
|
markup.add(btn)
|
||||||
|
|
||||||
|
# add button for creating a new tag
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
"➕ Create New Tag", callback_data="create_tag"))
|
||||||
|
|
||||||
|
bot.send_message(
|
||||||
|
message.chat.id, "Here is the data:", reply_markup=markup)
|
||||||
|
else:
|
||||||
|
markup = types.InlineKeyboardMarkup(row_width=1)
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
"➕ Create New Tag", callback_data="create_tag"))
|
||||||
|
|
||||||
|
bot.reply_to(message, "No data found.", reply_markup=markup)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in 'Show Tags' handler: {e}")
|
||||||
|
bot.reply_to(message, "Sorry, an error occurred.")
|
||||||
@@ -19,12 +19,15 @@ def register(bot):
|
|||||||
markup.add(btn)
|
markup.add(btn)
|
||||||
# add button for creating a new teacher
|
# add button for creating a new teacher
|
||||||
markup.add(types.InlineKeyboardButton(
|
markup.add(types.InlineKeyboardButton(
|
||||||
"➕ Create New Teacher", callback_data="create_teacher"))
|
"➕ Create New Teacher 🔒", callback_data="create_teacher"))
|
||||||
|
|
||||||
bot.send_message(
|
bot.send_message(
|
||||||
message.chat.id, "Here is the data:", reply_markup=markup)
|
message.chat.id, "Here is the data:", reply_markup=markup)
|
||||||
else:
|
else:
|
||||||
bot.reply_to(message, "No data found.")
|
markup = types.InlineKeyboardMarkup(row_width=1)
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
"➕ Create New Teacher 🔒", callback_data="create_teacher"))
|
||||||
|
bot.reply_to(message, "No data found.", reply_markup=markup)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in get_teachers handler: {e}")
|
logger.error(f"Error in get_teachers handler: {e}")
|
||||||
bot.reply_to(message, "Sorry, an error occurred.")
|
bot.reply_to(message, "Sorry, an error occurred.")
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
authenticated_users = {2032936226}
|
|
||||||
|
|
||||||
def is_authenticated(user_id):
|
|
||||||
return user_id in authenticated_users
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from telebot import types
|
||||||
|
|
||||||
|
|
||||||
|
def create_entity_markup(entity_name, enitity_id, is_auth_needed=False):
|
||||||
|
markup = types.InlineKeyboardMarkup()
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
f"✏️ Edit {entity_name} " + ("🔒" if is_auth_needed else ""), callback_data=f"edit_{entity_name}_{enitity_id}"))
|
||||||
|
markup.add(types.InlineKeyboardButton(
|
||||||
|
f"🗑️ Delete {entity_name} " + ("🔒" if is_auth_needed else ""), callback_data=f"delete_{entity_name}_{enitity_id}"))
|
||||||
|
|
||||||
|
return markup
|
||||||
+68
-6
@@ -32,15 +32,15 @@ def calculate_age(birthday_string):
|
|||||||
except:
|
except:
|
||||||
return "N/A"
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
# Student Info Formatter
|
# Student Info Formatter
|
||||||
|
|
||||||
|
|
||||||
def format_student_info(student):
|
def format_student_info(student):
|
||||||
"""
|
"""
|
||||||
Format student data into a nice message
|
Format student data into a nice message
|
||||||
student is a tuple: (id, username, name, created_at, email, phone_number, last_seen, is_verified, birthday)
|
student is a tuple: (id, username, name, created_at, email, phone_number, last_seen, is_verified, birthday)
|
||||||
"""
|
"""
|
||||||
print(student)
|
# print(student)
|
||||||
|
|
||||||
id, username, name, created_at, email, phone_number, last_seen, is_verfied, birthday = student
|
id, username, name, created_at, email, phone_number, last_seen, is_verfied, birthday = student
|
||||||
|
|
||||||
verified_status = "✅ Verified" if is_verfied else "❌ Not Verified"
|
verified_status = "✅ Verified" if is_verfied else "❌ Not Verified"
|
||||||
@@ -73,10 +73,11 @@ def format_student_info(student):
|
|||||||
# Teacher Info Formatter
|
# Teacher Info Formatter
|
||||||
def format_teacher_info(teacher):
|
def format_teacher_info(teacher):
|
||||||
"""
|
"""
|
||||||
Format student data into a nice message
|
Format teacher data into a nice message
|
||||||
student is a tuple: (id, username, created_at, email, phone_number, last_seen, is_verified, birthday)
|
teacher is a tuple: (id, username, name, created_at, email, phone_number, last_seen, is_verified, birthday, about_me, job_title)
|
||||||
"""
|
"""
|
||||||
print(teacher)
|
# print(teacher)
|
||||||
|
|
||||||
id, username, name, created_at, email, phone_number, last_seen, is_verfied, birthday, about_me, job_title = teacher
|
id, username, name, created_at, email, phone_number, last_seen, is_verfied, birthday, about_me, job_title = teacher
|
||||||
|
|
||||||
verified_status = "✅ Verified" if is_verfied else "❌ Not Verified"
|
verified_status = "✅ Verified" if is_verfied else "❌ Not Verified"
|
||||||
@@ -107,3 +108,64 @@ def format_teacher_info(teacher):
|
|||||||
<b>ID:</b> <code>{id}</code>
|
<b>ID:</b> <code>{id}</code>
|
||||||
"""
|
"""
|
||||||
return details.strip()
|
return details.strip()
|
||||||
|
|
||||||
|
# Course Info Formatter
|
||||||
|
|
||||||
|
|
||||||
|
def format_course_info(course):
|
||||||
|
"""
|
||||||
|
Format course data into a nice message
|
||||||
|
course is a tuple: (id, name, created_at, teacher_id, updated_at, description, difficulty, language, avgerage_rate)
|
||||||
|
"""
|
||||||
|
# print(course)
|
||||||
|
|
||||||
|
id, name, created_at, teacher_id, updated_at, description, difficulty, language, avgerage_rate = course
|
||||||
|
|
||||||
|
# Format dates nicely
|
||||||
|
created_date = format_date(created_at)
|
||||||
|
updated_at = format_date(updated_at)
|
||||||
|
|
||||||
|
details = f"""
|
||||||
|
<b>📚 Course Profile</b>
|
||||||
|
|
||||||
|
<b>Name:</b> {name}
|
||||||
|
<b>⭐️ Average Rating:</b> {avgerage_rate}
|
||||||
|
<b>Description:</b>
|
||||||
|
{description}
|
||||||
|
|
||||||
|
<b>Language:</b> {language}
|
||||||
|
<b>Difficulty:</b> {difficulty}
|
||||||
|
|
||||||
|
<b>📅 Course Info</b>
|
||||||
|
<b>Created At:</b> {created_date}
|
||||||
|
<b>Last Updated At:</b> {updated_at}
|
||||||
|
<b>ID:</b> <code>{id}</code>
|
||||||
|
"""
|
||||||
|
return details.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def format_course_review(review):
|
||||||
|
"""
|
||||||
|
Formate course review into a nice message
|
||||||
|
:param review: tuple(body: str, rate: int, student_name: str, course_name: str, created_at: date, updated_at: date)
|
||||||
|
"""
|
||||||
|
|
||||||
|
reviewBody, rate, student_name, course_name, created_at, updated_at = review
|
||||||
|
|
||||||
|
created_date = format_date(created_at)
|
||||||
|
updated_at = format_date(updated_at)
|
||||||
|
|
||||||
|
details = f"""
|
||||||
|
<b>✉️ Review</b>
|
||||||
|
|
||||||
|
<b>rate:</b> {'⭐️' * rate}
|
||||||
|
<b>Body:</b>\n{reviewBody}
|
||||||
|
|
||||||
|
<b>Course name:</b> {course_name}
|
||||||
|
<b>Student name:</b> {student_name}
|
||||||
|
|
||||||
|
<b>📅 Review Info</b>
|
||||||
|
<b>Created At:</b> {created_date}
|
||||||
|
<b>Last Updated At:</b> {updated_at}
|
||||||
|
"""
|
||||||
|
return details.strip()
|
||||||
|
|||||||
+9
-10
@@ -5,28 +5,26 @@ from psycopg2 import pool
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Connection pool for reusing connections (important for serverless!)
|
# Connection pool for reusing connections
|
||||||
_connection_pool = None
|
_connection_pool = None
|
||||||
|
|
||||||
|
|
||||||
def get_connection_pool():
|
def get_connection_pool():
|
||||||
"""Create a connection pool"""
|
"""Create a connection pool"""
|
||||||
global _connection_pool
|
global _connection_pool
|
||||||
if _connection_pool is None:
|
if _connection_pool is None:
|
||||||
try:
|
try:
|
||||||
# --- Production settings
|
DATABASE_URL = os.environ.get("DATABASE_URL")
|
||||||
# DATABASE_URL = os.environ.get("DATABASE_URL")
|
_connection_pool = psycopg2.pool.SimpleConnectionPool(
|
||||||
# _connection_pool = psycopg2.pool.SimpleConnectionPool(1, 5, DATABASE_URL);
|
1, 5, DATABASE_URL)
|
||||||
|
|
||||||
# --- Development settings
|
|
||||||
dev_url = "postgresql://postgres:123@localhost:5432/OLP"
|
|
||||||
_connection_pool = psycopg2.pool.SimpleConnectionPool(1, 5, dev_url);
|
|
||||||
|
|
||||||
logger.info("Database connection pool created")
|
logger.info("Database connection pool created")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to create connection pool: {e}")
|
logger.error(f"Failed to create connection pool: {e}")
|
||||||
return None
|
return None
|
||||||
return _connection_pool
|
return _connection_pool
|
||||||
|
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
"""Get a connection from the pool"""
|
"""Get a connection from the pool"""
|
||||||
try:
|
try:
|
||||||
@@ -39,6 +37,7 @@ def get_db_connection():
|
|||||||
logger.error(f"Database connection error: {e}")
|
logger.error(f"Database connection error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def release_db_connection(conn):
|
def release_db_connection(conn):
|
||||||
"""Return connection to the pool"""
|
"""Return connection to the pool"""
|
||||||
try:
|
try:
|
||||||
@@ -46,4 +45,4 @@ def release_db_connection(conn):
|
|||||||
if pool and conn:
|
if pool and conn:
|
||||||
pool.putconn(conn)
|
pool.putconn(conn)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error releasing connection: {e}")
|
logger.error(f"Error releasing connection: {e}")
|
||||||
|
|||||||
+557
-78
@@ -1,11 +1,20 @@
|
|||||||
import logging
|
import logging
|
||||||
import database.connection as connection
|
import database.connection as connection
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Students:
|
class BaseRepository:
|
||||||
def getAllStudents():
|
"""Base class for all repository classes"""
|
||||||
|
|
||||||
|
table_name = None # Should be Overrided in subclasses
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _execute_query(cls, query_func, operation_name):
|
||||||
|
"""Execute a SELECT query"""
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = connection.get_db_connection()
|
conn = connection.get_db_connection()
|
||||||
@@ -13,83 +22,21 @@ class Students:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT id,username FROM students")
|
result = query_func(cursor)
|
||||||
result = cursor.fetchall()
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
return result
|
||||||
|
|
||||||
return result if result else None
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Database query error in getAllStudents: {e}")
|
logger.error(
|
||||||
|
f"Database error in {cls.__name__}.{operation_name}: {e}")
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
connection.release_db_connection(conn)
|
connection.release_db_connection(conn)
|
||||||
|
|
||||||
def getStudentById(student_id):
|
@classmethod
|
||||||
conn = None
|
def _execute_mutation(cls, mutation_func, operation_name):
|
||||||
try:
|
"""Execute an INSERT, UPDATE, or DELETE"""
|
||||||
conn = connection.get_db_connection()
|
|
||||||
if not conn:
|
|
||||||
return None
|
|
||||||
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute(
|
|
||||||
"SELECT id,username, name, created_at, email, phone_number, last_seen, is_verfied, birthday FROM students WHERE id = %s", (student_id,))
|
|
||||||
result = cursor.fetchall()
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
return result[0] if result else None
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Database query error in getStudentById: {e}")
|
|
||||||
return None
|
|
||||||
finally:
|
|
||||||
if conn:
|
|
||||||
connection.release_db_connection(conn)
|
|
||||||
|
|
||||||
|
|
||||||
class Teachers:
|
|
||||||
def getAllTeachers():
|
|
||||||
conn = None
|
|
||||||
try:
|
|
||||||
conn = connection.get_db_connection()
|
|
||||||
if not conn:
|
|
||||||
return None
|
|
||||||
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("SELECT id,username FROM teachers")
|
|
||||||
result = cursor.fetchall()
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
return result if result else None
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Database query error in getAllTeachers: {e}")
|
|
||||||
return None
|
|
||||||
finally:
|
|
||||||
if conn:
|
|
||||||
connection.release_db_connection(conn)
|
|
||||||
|
|
||||||
def getTeachertById(teacher_id):
|
|
||||||
conn = None
|
|
||||||
try:
|
|
||||||
conn = connection.get_db_connection()
|
|
||||||
if not conn:
|
|
||||||
return None
|
|
||||||
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute(
|
|
||||||
"SELECT id, username, name, created_at, email, phone_number, last_seen, is_verfied, birthday, about_me, job_title FROM teachers WHERE id = %s", (teacher_id, ))
|
|
||||||
result = cursor.fetchall()
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
return result[0] if result else None
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Database query error in getTeacherById: {e}")
|
|
||||||
return None
|
|
||||||
finally:
|
|
||||||
if conn:
|
|
||||||
connection.release_db_connection(conn)
|
|
||||||
|
|
||||||
def createTeacher(name, email, phone_number, password, username, birthday, about_me, job_title):
|
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = connection.get_db_connection()
|
conn = connection.get_db_connection()
|
||||||
@@ -97,18 +44,550 @@ class Teachers:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(
|
mutation_func(cursor, conn)
|
||||||
"INSERT INTO teachers (name, email, phone_number, hashed_password, username, birthday, about_me, job_title) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
|
|
||||||
(name, email, phone_number, password,
|
|
||||||
username, birthday, about_me, job_title)
|
|
||||||
)
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Database query error in createTeacher: {e}")
|
logger.error(
|
||||||
|
f"Database error in {cls.__name__}.{operation_name}: {e}")
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
connection.release_db_connection(conn)
|
connection.release_db_connection(conn)
|
||||||
|
|
||||||
|
|
||||||
|
class Admins(BaseRepository):
|
||||||
|
|
||||||
|
SESSION_TIMEOUT_HOURS = 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
"""Hash password using SHA256"""
|
||||||
|
return hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def login(username: str, password: str, telegram_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Verify credentials and create a session.
|
||||||
|
Returns True if login successful, False otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def query(cursor):
|
||||||
|
password_hash = Admins.hash_password(password)
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM admins WHERE username = %s AND password_hash = %s AND telegram_id = %s",
|
||||||
|
(username, password_hash, telegram_id)
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
return result is not None
|
||||||
|
|
||||||
|
result = Admins._execute_query(query, "login")
|
||||||
|
|
||||||
|
if result:
|
||||||
|
# Create session by updating last_login and session_expires_at
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
expires_at = datetime.now() + timedelta(hours=Admins.SESSION_TIMEOUT_HOURS)
|
||||||
|
cursor.execute(
|
||||||
|
"""UPDATE admins
|
||||||
|
SET last_login = CURRENT_TIMESTAMP,
|
||||||
|
session_expires_at = %s
|
||||||
|
WHERE telegram_id = %s""",
|
||||||
|
(expires_at, telegram_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Admins._execute_mutation(mutation, "login")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_authenticated(telegram_id: int) -> bool:
|
||||||
|
"""Check if user has a valid active session"""
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT session_expires_at FROM admins WHERE telegram_id = %s",
|
||||||
|
(telegram_id,)
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
|
||||||
|
if not result or result[0] is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if session has expired
|
||||||
|
expires_at = result[0]
|
||||||
|
|
||||||
|
if isinstance(expires_at, str):
|
||||||
|
expires_at = datetime.fromisoformat(expires_at)
|
||||||
|
|
||||||
|
return datetime.now() < expires_at
|
||||||
|
|
||||||
|
result = Admins._execute_query(query, "is_authenticated")
|
||||||
|
return result if result is not None else False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def logout(telegram_id: int) -> bool:
|
||||||
|
"""Logout admin by clearing session"""
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE admins SET session_expires_at = NULL WHERE telegram_id = %s",
|
||||||
|
(telegram_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Admins._execute_mutation(mutation, "logout")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_session_info(telegram_id: int) -> dict:
|
||||||
|
"""Get session info for the admin"""
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT username, last_login, session_expires_at FROM admins WHERE telegram_id = %s",
|
||||||
|
(telegram_id,)
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
return result
|
||||||
|
|
||||||
|
result = Admins._execute_query(query, "get_session_info")
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
|
||||||
|
session_info = {
|
||||||
|
'username': result[0],
|
||||||
|
'last_login': result[1],
|
||||||
|
'expires_at': result[2],
|
||||||
|
'is_active': False
|
||||||
|
}
|
||||||
|
|
||||||
|
if result[2]:
|
||||||
|
expires_at = result[2]
|
||||||
|
|
||||||
|
if isinstance(expires_at, str):
|
||||||
|
expires_at = datetime.fromisoformat(expires_at)
|
||||||
|
|
||||||
|
session_info['is_active'] = datetime.now() < expires_at
|
||||||
|
session_info['time_remaining'] = str(
|
||||||
|
expires_at - datetime.now()).split('.')[0]
|
||||||
|
|
||||||
|
return session_info
|
||||||
|
|
||||||
|
|
||||||
|
class Students(BaseRepository):
|
||||||
|
table_name = "students"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getAllStudents(cls):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"SELECT id,username FROM {cls.table_name}")
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getAllStudents")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getStudentById(cls, student_id):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"SELECT id,username, name, created_at, email, phone_number, last_seen, is_verfied, birthday FROM {cls.table_name} WHERE id = %s", (student_id,))
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getStudentById")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def createStudent(cls, name, email, phone_number, password, username, birthday):
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
f"INSERT INTO {cls.table_name} (name, email, phone_number, hashed_password, username, birthday) VALUES (%s, %s, %s, %s, %s, %s)",
|
||||||
|
(name, email, phone_number, password,
|
||||||
|
username, birthday))
|
||||||
|
return cls._execute_mutation(mutation, "createStudent")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def updateStudent(cls, student_id, **fields):
|
||||||
|
if not fields:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
columns = [(f"{key} = %s" if key != 'password' else "hashed_password = %s")
|
||||||
|
for key in fields.keys()]
|
||||||
|
values = list(fields.values()) + [student_id]
|
||||||
|
cursor.execute(
|
||||||
|
f"UPDATE {cls.table_name} SET {', '.join(columns)} WHERE id = %s",
|
||||||
|
tuple(values)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "updateStudent")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def deleteStudent(cls, student_id):
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
f"DELETE FROM {cls.table_name} WHERE id = %s", (student_id,))
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "deleteStudent")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getStudentCourses(cls, student_id):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
c.id,
|
||||||
|
c.name
|
||||||
|
FROM courses c
|
||||||
|
JOIN course_enrolments ce ON ce.course_id = c.id
|
||||||
|
JOIN students s ON s.id = ce.student_id
|
||||||
|
WHERE s.id = %s
|
||||||
|
""", (student_id,)
|
||||||
|
)
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, 'getStudentCourses')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getStudentReviews(cls, student_id):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
SELECT r.body review, r.rate, s.name student_name, c.name course_name, r.created_at, r.updated_at
|
||||||
|
FROM reviews r
|
||||||
|
JOIN course_enrolments ce ON ce.id = r.enrolment_id
|
||||||
|
JOIN students s ON s.id = ce.student_id
|
||||||
|
JOIN courses c ON c.id = ce.course_id
|
||||||
|
WHERE s.id = %s
|
||||||
|
LIMIT (3)
|
||||||
|
""", (student_id,)
|
||||||
|
)
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, 'getStudentReviews')
|
||||||
|
|
||||||
|
|
||||||
|
class Teachers(BaseRepository):
|
||||||
|
table_name = "teachers"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getAllTeachers(cls):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"SELECT id,username FROM {cls.table_name}")
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getAllTeachers")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getTeacherById(cls, teacher_id):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"SELECT id, username, name, created_at, email, phone_number, last_seen, is_verfied, birthday, about_me, job_title FROM {cls.table_name} WHERE id = %s", (teacher_id, ))
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getTeacherById")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def createTeacher(cls, name, email, phone_number, password, username, birthday, about_me, job_title):
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
f"INSERT INTO {cls.table_name} (name, email, phone_number, hashed_password, username, birthday, about_me, job_title) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
|
||||||
|
(name, email, phone_number, password,
|
||||||
|
username, birthday, about_me, job_title))
|
||||||
|
return cls._execute_mutation(mutation, "createTeacher")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def updateTeacher(cls, teacher_id, **fields):
|
||||||
|
if not fields:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
columns = [(f"{key} = %s" if key != 'password' else "hashed_password = %s")
|
||||||
|
for key in fields.keys()]
|
||||||
|
values = list(fields.values()) + [teacher_id]
|
||||||
|
cursor.execute(
|
||||||
|
f"UPDATE {cls.table_name} SET {', '.join(columns)} WHERE id = %s",
|
||||||
|
tuple(values)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "updateTeacher")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def deleteTeacher(cls, teacher_id):
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
f"DELETE FROM {cls.table_name} WHERE id = %s", (teacher_id,))
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "deleteTeacher")
|
||||||
|
|
||||||
|
|
||||||
|
class Courses(BaseRepository):
|
||||||
|
table_name = "courses"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getAllCourses(cls):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"SELECT id, name FROM {cls.table_name}")
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getAllCourses")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getCourseById(cls, course_id):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
created_at,
|
||||||
|
teacher_id,
|
||||||
|
updated_at,
|
||||||
|
description,
|
||||||
|
difficulty,
|
||||||
|
language,
|
||||||
|
r.avg_rate
|
||||||
|
FROM
|
||||||
|
courses
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
ce.course_id,
|
||||||
|
ROUND(AVG(r.rate), 2) avg_rate
|
||||||
|
FROM course_enrolments ce
|
||||||
|
JOIN reviews r ON r.enrolment_id = ce.id
|
||||||
|
GROUP BY ce.course_id
|
||||||
|
) r ON r.course_id = courses.id
|
||||||
|
WHERE
|
||||||
|
id = %s
|
||||||
|
|
||||||
|
""", (course_id, ))
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getCourseById")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def createCourse(cls, name, teacher_id, description, language, difficulty):
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
f"INSERT INTO {cls.table_name} (name, teacher_id, description, language, difficulty) VALUES (%s, %s, %s, %s, %s)",
|
||||||
|
(name, teacher_id, description, language, difficulty))
|
||||||
|
return cls._execute_mutation(mutation, "createCourse")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def updateCourse(cls, course_id, **fields):
|
||||||
|
if not fields:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
columns = [f"{key} = %s" for key in fields.keys()]
|
||||||
|
values = list(fields.values()) + [course_id]
|
||||||
|
cursor.execute(
|
||||||
|
f"UPDATE {cls.table_name} SET {', '.join(columns)} WHERE id = %s",
|
||||||
|
tuple(values)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "updateCourse")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def deleteCourse(cls, course_id):
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
f"DELETE FROM {cls.table_name} WHERE id = %s", (course_id,))
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "deleteCourse")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getCourseReviews(cls, course_id):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT r.body review, r.rate, s.name student_name, c.name course_name, r.created_at, r.updated_at
|
||||||
|
FROM reviews r
|
||||||
|
JOIN course_enrolments ce ON ce.id = r.enrolment_id
|
||||||
|
JOIN courses c ON c.id = ce.course_id
|
||||||
|
JOIN students s ON s.id = ce.student_id
|
||||||
|
WHERE c.id = %s
|
||||||
|
LIMIT (3)
|
||||||
|
""", (course_id,))
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, 'getCourseReviews')
|
||||||
|
|
||||||
|
|
||||||
|
class Tags(BaseRepository):
|
||||||
|
table_name = "tags"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getAllTags(cls):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"SELECT id, name, slug FROM {cls.table_name}")
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getAllTags")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getTagById(cls, tag_id):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"SELECT id, name, slug FROM {cls.table_name} WHERE id = %s", (tag_id,))
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getTagById")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def createTag(cls, name, slug):
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
f"INSERT INTO {cls.table_name} (name, slug) VALUES (%s, %s)", (name, slug))
|
||||||
|
return cls._execute_mutation(mutation, "createTag")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def updateTag(cls, tag_id, **fields):
|
||||||
|
if not fields:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
columns = [f"{key} = %s" for key in fields.keys()]
|
||||||
|
values = list(fields.values()) + [tag_id]
|
||||||
|
cursor.execute(
|
||||||
|
f"UPDATE {cls.table_name} SET {', '.join(columns)} WHERE id = %s",
|
||||||
|
tuple(values)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "updateTag")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def deleteTag(cls, tag_id):
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
f"DELETE FROM {cls.table_name} WHERE id = %s", (tag_id,))
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "deleteTag")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getTopCoursesByTag(cls, tag_id):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
cs.id,
|
||||||
|
cs.name course_name,
|
||||||
|
avg(r.rate) avg_rate
|
||||||
|
FROM
|
||||||
|
tags t
|
||||||
|
JOIN course_tags ct ON t.id = ct.tag_id
|
||||||
|
JOIN courses cs ON cs.id = ct.course_id
|
||||||
|
JOIN course_enrolments ce ON cs.id = ce.course_id
|
||||||
|
JOIN reviews r ON ce.id = r.enrolment_id
|
||||||
|
WHERE
|
||||||
|
t.id = %s
|
||||||
|
GROUP BY
|
||||||
|
cs.id,
|
||||||
|
cs.name
|
||||||
|
ORDER BY
|
||||||
|
avg_rate DESC
|
||||||
|
LIMIT
|
||||||
|
(5)
|
||||||
|
""", (tag_id, ))
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getTopCoursesByTag")
|
||||||
|
|
||||||
|
|
||||||
|
class Categories(BaseRepository):
|
||||||
|
table_name = "categories"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getAllCategories(cls):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
f"SELECT id, name, description, parent_id FROM {cls.table_name}")
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getAllCategories")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getCategoryById(cls, category_id):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT c1.id, c1.name, c1.description,
|
||||||
|
COALESCE(c2.name, 'None') AS parent_category
|
||||||
|
FROM {cls.table_name} c1
|
||||||
|
LEFT JOIN {cls.table_name} c2 ON c2.id = c1.parent_id
|
||||||
|
WHERE c1.id = %s
|
||||||
|
""", (category_id,))
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getCategoryById")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def createCategory(cls, name, description, parent_id=None):
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
f"INSERT INTO {cls.table_name} (name, description, parent_id) VALUES (%s, %s, %s)", (name, description, parent_id))
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "createCategory")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def updateCategory(cls, category_id, **fields):
|
||||||
|
if not fields:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
columns = [f"{key} = %s" for key in fields.keys()]
|
||||||
|
values = list(fields.values()) + [category_id]
|
||||||
|
cursor.execute(
|
||||||
|
f"UPDATE {cls.table_name} SET {', '.join(columns)} WHERE id = %s",
|
||||||
|
tuple(values)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "updateCategory")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def deleteCategory(cls, category_id):
|
||||||
|
def mutation(cursor, conn):
|
||||||
|
cursor.execute(
|
||||||
|
f"DELETE FROM {cls.table_name} WHERE id = %s", (category_id,))
|
||||||
|
|
||||||
|
return cls._execute_mutation(mutation, "deleteCategory")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getTopCoursesByCategory(cls, category_id):
|
||||||
|
def query(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
cs.id,
|
||||||
|
cs.name course_name,
|
||||||
|
avg(r.rate) avg_rate
|
||||||
|
FROM
|
||||||
|
categories c
|
||||||
|
JOIN course_categories cc ON c.id = cc.category_id
|
||||||
|
JOIN courses cs ON cc.course_id = cs.id
|
||||||
|
JOIN course_enrolments ce ON cs.id = ce.course_id
|
||||||
|
JOIN reviews r ON ce.id = r.enrolment_id
|
||||||
|
WHERE
|
||||||
|
c.id = %s
|
||||||
|
GROUP BY
|
||||||
|
cs.id,
|
||||||
|
cs.name
|
||||||
|
ORDER BY
|
||||||
|
avg_rate DESC
|
||||||
|
LIMIT
|
||||||
|
(5)
|
||||||
|
""", (category_id, ))
|
||||||
|
result = cursor.fetchall()
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
return cls._execute_query(query, "getTopCoursesByCategory")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ load_dotenv()
|
|||||||
|
|
||||||
BOT_TOKEN = os.environ.get("BOT_TOKEN")
|
BOT_TOKEN = os.environ.get("BOT_TOKEN")
|
||||||
|
|
||||||
|
|
||||||
def ensure_no_webhook():
|
def ensure_no_webhook():
|
||||||
"""Remove webhook before starting polling"""
|
"""Remove webhook before starting polling"""
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
@@ -15,13 +16,13 @@ def ensure_no_webhook():
|
|||||||
if response.json()['ok']:
|
if response.json()['ok']:
|
||||||
print("✅ Webhook removed, starting polling...")
|
print("✅ Webhook removed, starting polling...")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
print("⚠️ Warning: Could not remove webhook")
|
print("⚠️ Warning: Could not remove webhook")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if ensure_no_webhook():
|
if ensure_no_webhook():
|
||||||
bot = get_bot()
|
bot = get_bot()
|
||||||
print("🤖 Bot is polling locally...")
|
print("🤖 Bot is polling locally...")
|
||||||
bot.polling(non_stop=True)
|
bot.polling(non_stop=True)
|
||||||
|
|||||||
@@ -17,26 +17,24 @@ async def telegram_webhook(request: Request):
|
|||||||
try:
|
try:
|
||||||
import telebot
|
import telebot
|
||||||
from bot.bot import get_bot
|
from bot.bot import get_bot
|
||||||
|
|
||||||
bot = get_bot()
|
bot = get_bot()
|
||||||
json_data = await request.json()
|
json_data = await request.json()
|
||||||
|
|
||||||
logger.info(f"Received update: {json_data}")
|
logger.info(f"Received update: {json_data}")
|
||||||
|
|
||||||
update = telebot.types.Update.de_json(json_data)
|
update = telebot.types.Update.de_json(json_data)
|
||||||
|
|
||||||
# Process the update directly instead of using process_new_updates
|
# Process ALL types of updates (messages, callback queries, etc.)
|
||||||
if update.message:
|
try:
|
||||||
try:
|
bot.process_new_updates([update])
|
||||||
# Call your message handlers directly
|
except Exception as e:
|
||||||
bot.process_new_updates([update])
|
logger.error(f"Error processing update: {e}")
|
||||||
except Exception as e:
|
# Still return 200 to Telegram to acknowledge receipt
|
||||||
logger.error(f"Error processing update: {e}")
|
return JSONResponse(content={"ok": True}, status_code=200)
|
||||||
# Still return 200 to Telegram to acknowledge receipt
|
|
||||||
return JSONResponse(content={"ok": True}, status_code=200)
|
|
||||||
|
|
||||||
return JSONResponse(content={"ok": True}, status_code=200)
|
return JSONResponse(content={"ok": True}, status_code=200)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Webhook error: {e}")
|
logger.error(f"Webhook error: {e}")
|
||||||
# Return 200 even on error to prevent Telegram from retrying
|
# Return 200 even on error to prevent Telegram from retrying
|
||||||
@@ -54,4 +52,4 @@ def read_root():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run("main:app", host="0.0.0.0", port=5001, reload=True)
|
uvicorn.run("main:app", host="0.0.0.0", port=5001, reload=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user