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)