diff --git a/bot/callbacks/categories.py b/bot/callbacks/categories.py new file mode 100644 index 0000000..5f4fd45 --- /dev/null +++ b/bot/callbacks/categories.py @@ -0,0 +1,159 @@ +from telebot import types, TeleBot +from database.models import Categories +from bot.handlers.start import startMarkup +from bot.utils.crud_helpers import create_entity_markup + +# TODO: Add Option for showing courses with specific tags. + + +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"), + ('parent_id', "The parent category id") + ] + 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.getCategorieById(category_id) + + if category: + details = f""" + 🔖 Category Profile + + Name: {category[1]} + Parent Category: {category[3]} + Description: \n{category[2]} + """ + details.strip() + markup = create_entity_markup("category", 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] = 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): + category_id = call.data.split('_')[2] + category = Categories.getCategorieById(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): + 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) diff --git a/bot/callbacks/init.py b/bot/callbacks/init.py index c7fa4d5..18d11ae 100644 --- a/bot/callbacks/init.py +++ b/bot/callbacks/init.py @@ -1,4 +1,4 @@ -from . import students, teachers, courses, tags +from . import students, teachers, courses, tags, categories from bot.handlers.start import startMarkup @@ -8,6 +8,7 @@ def register_all_callbacks(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') diff --git a/bot/callbacks/tags.py b/bot/callbacks/tags.py index 54abfe7..70e2f57 100644 --- a/bot/callbacks/tags.py +++ b/bot/callbacks/tags.py @@ -22,7 +22,7 @@ def register(bot: TeleBot): # Showing details of a Tag @bot.callback_query_handler(func=lambda call: call.data.startswith('tag_')) - def show_teacher_details(call): + def show_tag_details(call): tag_id = call.data.split('_')[1] tag = Tags.getTagById(tag_id) @@ -135,7 +135,7 @@ def register(bot: TeleBot): bot.send_message(message.chat.id, msg, reply_markup=startMarkup()) return - # Update teacher + # 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()) @@ -143,7 +143,7 @@ def register(bot: TeleBot): bot.send_message( message.chat.id, f"❌ Failed to update Tag's {field}.", reply_markup=startMarkup()) - # Deleting a Teacher + # Deleting a Tag @bot.callback_query_handler(func=lambda call: call.data.startswith('delete_tag_')) def delete_tag(call): tag_id = call.data.split('_')[2] diff --git a/bot/handlers/categories.py b/bot/handlers/categories.py new file mode 100644 index 0000000..4c7ac34 --- /dev/null +++ b/bot/handlers/categories.py @@ -0,0 +1,31 @@ +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: + bot.reply_to(message, "No data found.") + except Exception as e: + logger.error(f"Error in 'Show Categories' handler: {e}") + bot.reply_to(message, "Sorry, an error occurred.") diff --git a/bot/handlers/init.py b/bot/handlers/init.py index c3ef3f6..50b2f80 100644 --- a/bot/handlers/init.py +++ b/bot/handlers/init.py @@ -1,4 +1,4 @@ -from . import start, students, teachers, courses, tags, common +from . import start, students, teachers, courses, tags, categories, common from bot.callbacks.init import register_all_callbacks @@ -10,6 +10,7 @@ def register_all_handlers(bot): teachers.register(bot) courses.register(bot) tags.register(bot) + categories.register(bot) common.register(bot) # Must be last (catch-all) # Callback handlers diff --git a/database/models.py b/database/models.py index e01ddc6..dc713c8 100644 --- a/database/models.py +++ b/database/models.py @@ -522,3 +522,144 @@ class Tags: finally: if conn: connection.release_db_connection(conn) + + +class Categories: + def getAllCategories(): + conn = None + try: + conn = connection.get_db_connection() + if not conn: + return None + + cursor = conn.cursor() + cursor.execute( + "SELECT id, name, description, parent_id FROM categories") + result = cursor.fetchall() + cursor.close() + + return result if result else None + except Exception as e: + logger.error(f"Database query error in getAllCategories: {e}") + return None + finally: + if conn: + connection.release_db_connection(conn) + + def getCategorieById(category_id): + conn = None + try: + conn = connection.get_db_connection() + if not conn: + return None + + cursor = conn.cursor() + cursor.execute(""" + SELECT + c1.id, + c1.name, + c1.description, + COALESCE(c2.name, 'None') AS parent_category + FROM + categories c1 + LEFT JOIN categories c2 ON c2.id = c1.parent_id + WHERE + c1.id = %s; + """, (category_id,)) + + result = cursor.fetchall() + cursor.close() + + return result[0] if result else None + except Exception as e: + logger.error(f"Database query error in getCategoryById: {e}") + return None + finally: + if conn: + connection.release_db_connection(conn) + + def createCategory(name, description, parent_id=None): + conn = None + try: + conn = connection.get_db_connection() + if not conn: + return False + + cursor = conn.cursor() + cursor.execute( + "INSERT INTO categories (name, description, parent_id) VALUES (%s, %s, %s)", + (name, description, parent_id) + ) + conn.commit() + cursor.close() + + return True + except Exception as e: + logger.error(f"Database query error in createCategory: {e}") + return False + finally: + if conn: + connection.release_db_connection(conn) + + def updateCategory(category_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(category_id) + + query = f""" + UPDATE categories + 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 updateCategory: {e}") + return False + + finally: + if conn: + connection.release_db_connection(conn) + + def deleteCategory(category_id): + conn = None + try: + conn = connection.get_db_connection() + if not conn: + return False + + cursor = conn.cursor() + cursor.execute( + "DELETE FROM categories WHERE id = %s", (category_id,)) + conn.commit() + cursor.close() + + return True + except Exception as e: + logger.error(f"Database query error in deleteCategory: {e}") + return False + finally: + if conn: + connection.release_db_connection(conn)