diff --git a/bot/callbacks/courses.py b/bot/callbacks/courses.py new file mode 100644 index 0000000..2ffadf6 --- /dev/null +++ b/bot/callbacks/courses.py @@ -0,0 +1,159 @@ +from telebot import TeleBot, types +from database.models import Courses +from bot.utils.crud_helpers import create_entity_markup +from bot.utils.formatters import format_course_info +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 ()"), + ('language', "language ('english', 'spanish', 'german', 'french', 'persian')"), + ('difficulty', "difficulty () ('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.") + return + + details = format_course_info(course) + markup = create_entity_markup("course", course_id) + markup.add(types.InlineKeyboardButton( + f"👨‍🏫 Teacher's Info", callback_data=f"teacher_{course[3]}")) + + 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] = 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, "✅ Teacher 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): + 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} (current)": + 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): + 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) diff --git a/bot/callbacks/init.py b/bot/callbacks/init.py index 217b108..d834403 100644 --- a/bot/callbacks/init.py +++ b/bot/callbacks/init.py @@ -1,7 +1,17 @@ -from . import students, teachers +from . import students, teachers, courses +from bot.handlers.start import startMarkup def register_all_callbacks(bot): """Register all callback handlers""" students.register(bot) teachers.register(bot) + courses.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) diff --git a/bot/callbacks/teachers.py b/bot/callbacks/teachers.py index 5f152d6..cb2d9d2 100644 --- a/bot/callbacks/teachers.py +++ b/bot/callbacks/teachers.py @@ -4,6 +4,12 @@ from bot.utils.formatters import format_teacher_info from bot.handlers.start import startMarkup from bot.utils.crud_helpers import create_entity_markup +# TODO: Add Buttons for skipping Optional Fields (Add to all entities). +# TODO: Add Option for handling all updates at once. +# TODO: Add Authentication for sensitive actions and info. + +# TODO: Add Option for seeing courses taught by a teacher in Teacher Details. + def register(bot: TeleBot): cancelMarkup = types.InlineKeyboardMarkup() @@ -91,13 +97,6 @@ def register(bot: TeleBot): bot.send_message( message.chat.id, "❌ Failed to create teacher.", reply_markup=startMarkup()) - @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) - # Editing a Teacher Flow @bot.callback_query_handler(func=lambda call: call.data.startswith('edit_teacher_')) def start_teacher_editing(call): diff --git a/bot/handlers/courses.py b/bot/handlers/courses.py new file mode 100644 index 0000000..c424c0e --- /dev/null +++ b/bot/handlers/courses.py @@ -0,0 +1,30 @@ +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: + bot.reply_to(message, "No data found.") + except Exception as e: + logger.error(f"Error in (Show Courses) handler: {e}") + bot.reply_to(message, "Sorry, an error occurred.") diff --git a/bot/handlers/init.py b/bot/handlers/init.py index 1085dad..fbe885c 100644 --- a/bot/handlers/init.py +++ b/bot/handlers/init.py @@ -1,4 +1,4 @@ -from . import start, students, teachers, common +from . import start, students, teachers, courses, common from bot.callbacks.init import register_all_callbacks @@ -8,6 +8,7 @@ def register_all_handlers(bot): start.register(bot) students.register(bot) teachers.register(bot) + courses.register(bot) common.register(bot) # Must be last (catch-all) # Callback handlers diff --git a/bot/handlers/start.py b/bot/handlers/start.py index e5cbe64..19ee62c 100644 --- a/bot/handlers/start.py +++ b/bot/handlers/start.py @@ -1,6 +1,8 @@ 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): @bot.message_handler(commands=["start"]) @@ -16,11 +18,6 @@ def register(bot): 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 handle_test_options(message): - option = message.text - bot.send_message(message.chat.id, f"You selected: {option}") - def startMarkup(): markup = types.ReplyKeyboardMarkup( diff --git a/bot/utils/formatters.py b/bot/utils/formatters.py index 747b01d..3f756db 100644 --- a/bot/utils/formatters.py +++ b/bot/utils/formatters.py @@ -32,15 +32,15 @@ def calculate_age(birthday_string): except: return "N/A" + # Student Info Formatter - - def format_student_info(student): """ Format student data into a nice message 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 verified_status = "✅ Verified" if is_verfied else "❌ Not Verified" @@ -73,10 +73,11 @@ def format_student_info(student): # Teacher Info Formatter def format_teacher_info(teacher): """ - Format student data into a nice message - student is a tuple: (id, username, created_at, email, phone_number, last_seen, is_verified, birthday) + Format teacher data into a nice message + 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 verified_status = "✅ Verified" if is_verfied else "❌ Not Verified" @@ -107,3 +108,36 @@ def format_teacher_info(teacher): ID: {id} """ 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) + """ + # print(course) + + id, name, created_at, teacher_id, updated_at, description, difficulty, language = course + + # Format dates nicely + created_date = format_date(created_at) + updated_at = format_date(updated_at) + + details = f""" + 📚 Course Profile + + Name: {name} + Description: + {description} + + Language: {language} + Difficulty: {difficulty} + + 📅 Course Info + Created At: {created_date} + Last Updated At: {updated_at} + ID: {id} + """ + return details.strip() diff --git a/database/models.py b/database/models.py index afc0d20..73f45e7 100644 --- a/database/models.py +++ b/database/models.py @@ -264,3 +264,132 @@ class Teachers: finally: if conn: connection.release_db_connection(conn) + + +class Courses: + def getAllCourses(): + conn = None + try: + conn = connection.get_db_connection() + if not conn: + return None + + cursor = conn.cursor() + cursor.execute("SELECT id, name FROM courses") + result = cursor.fetchall() + cursor.close() + + return result if result else None + except Exception as e: + logger.error(f"Database query error in getAllCourses: {e}") + return None + finally: + if conn: + connection.release_db_connection(conn) + + def getCourseById(course_id): + conn = None + try: + conn = connection.get_db_connection() + if not conn: + return None + + cursor = conn.cursor() + cursor.execute( + "SELECT id, name, created_at, teacher_id, updated_at, description, difficulty, language FROM courses WHERE id = %s", (course_id, )) + result = cursor.fetchall() + cursor.close() + + return result[0] if result else None + except Exception as e: + logger.error(f"Database query error in getCourseById: {e}") + return None + finally: + if conn: + connection.release_db_connection(conn) + + def createCourse(name, teacher_id, description, language, difficulty): + conn = None + try: + conn = connection.get_db_connection() + if not conn: + return False + + cursor = conn.cursor() + cursor.execute( + "INSERT INTO courses (name, teacher_id, description, language, difficulty) VALUES (%s, %s, %s, %s, %s)", + (name, teacher_id, description, language, difficulty) + ) + conn.commit() + cursor.close() + + return True + except Exception as e: + logger.error(f"Database query error in createCourse: {e}") + return False + finally: + if conn: + connection.release_db_connection(conn) + + def updateCourse(course_id, **fields): + if not fields: + return False # nothing to update + + conn = None + try: + conn = connection.get_db_connection() + if not conn: + return False + + cursor = conn.cursor() + + # Build dynamic SET clause + columns = [] + values = [] + + for key, value in fields.items(): + columns.append(f"{key} = %s") + values.append(value) + + values.append(course_id) + + query = f""" + UPDATE courses + SET {', '.join(columns)} + WHERE id = %s + """ + + cursor.execute(query, tuple(values)) + conn.commit() + cursor.close() + + return True + + except Exception as e: + logger.error(f"Database query error in updateCourse: {e}") + return False + + finally: + if conn: + connection.release_db_connection(conn) + + def deleteCourse(course_id): + conn = None + try: + conn = connection.get_db_connection() + if not conn: + return False + + cursor = conn.cursor() + cursor.execute( + "DELETE FROM courses WHERE id = %s", (course_id,)) + conn.commit() + cursor.close() + + return True + except Exception as e: + logger.error(f"Database query error in deleteCourse: {e}") + return False + finally: + if conn: + connection.release_db_connection(conn)