diff --git a/.github/workflows/python-poetry-code-quality.yml b/.github/workflows/python-poetry-code-quality.yml new file mode 100644 index 0000000..3b66ff6 --- /dev/null +++ b/.github/workflows/python-poetry-code-quality.yml @@ -0,0 +1,66 @@ +name: code-quality + +on: + push: + +jobs: + pylint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: install poetry + run: pipx install poetry + + - name: Check pyproject.toml validity + run: poetry check --no-interaction + + - name: Install deps + if: steps.cache-deps.cache-hit != 'true' + run: | + poetry config virtualenvs.in-project true + poetry install --no-interaction + + - name: Analysing the code with pylint + run: poetry run pylint bot + + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ "3.13" ] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install poetry + run: pipx install poetry + + - name: Check pyproject.toml validity + run: poetry check --no-interaction + + - name: Install deps + if: steps.cache-deps.cache-hit != 'true' + run: | + poetry config virtualenvs.in-project true + poetry install --no-interaction + + - name: Test with pytest + run: | + poetry run pytest -v --cov=bot \ No newline at end of file diff --git a/README.md b/README.md index 0b2a480..a953f6a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ - -[![Actions](https://github.com/NikitosKey/alcounting_bot/actions/workflows/main.yml/badge.svg)](https://github.com/NikitosKey/alcounting_bot/actions/workflows/main.yml) +[![code-quality](https://github.com/NikitosKey/alcounting_bot/actions/workflows/python-poetry-code-quality.yml/badge.svg)](https://github.com/NikitosKey/alcounting_bot/actions/workflows/python-poetry-code-quality.yml) # Alcounting Telegram Bot @@ -9,7 +8,8 @@ A bot designed to help manage parties and prevent organizers from going into the - [Description](#description) - [Guide](#Guide) -- [User Documentation](#user-documentation) +- [Documentation](#documentation) +- [Contributing](#contributing) ## Description diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/__main__.py b/bot/__main__.py index c728dab..ae75550 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -1,9 +1,13 @@ +""" +This module initializes and runs the bot application. +""" + import os import logging from telegram import Update from telegram.ext import Application -from handlers import register_handlers +from bot.handlers import register_handlers # Set up logging logging.basicConfig( @@ -18,12 +22,12 @@ def main() -> None: """Allow running a bot.""" # Create the Application and pass it your bot's token. - application = Application.builder().token(os.getenv('BOT_TOKEN')).build() + application = Application.builder().token(os.getenv("BOT_TOKEN")).build() # Register all handlers register_handlers(application) - # Run the bot until the user presses Ctrl-C + # Run the bot application.run_polling(allowed_updates=Update.ALL_TYPES) diff --git a/bot/database/__init__.py b/bot/database/__init__.py index 977ae2f..d4c7cd7 100644 --- a/bot/database/__init__.py +++ b/bot/database/__init__.py @@ -1,11 +1,8 @@ +"""Module for database operations.""" + from bot.database.database import Database from bot.database.user import User from bot.database.product import Product from bot.database.order import Order -__all__ = [ - 'Database', - 'User', - 'Product', - 'Order' -] \ No newline at end of file +__all__ = ["Database", "User", "Product", "Order"] diff --git a/bot/database/database.py b/bot/database/database.py index 2acc467..289cb23 100644 --- a/bot/database/database.py +++ b/bot/database/database.py @@ -1,215 +1,212 @@ +"""Module for working with the database""" + +import logging +import os import sqlite3 -from bot.database.user import User -from bot.database.product import Product from bot.database.order import Order +from bot.database.product import Product +from bot.database.user import User + +DATABASE_DEFAULT_PATH = "data/database.db" -database_path = '../data/database.db' class Database: - def __init__(self): - pass + """Class for working with sqlite3 database""" - def create_tables(self) -> None: - conn = sqlite3.connect(database_path) - cur = conn.cursor() + def __init__(self, path: str = DATABASE_DEFAULT_PATH): + self.fix_path(path) - # --- создаём таблицу с меню --- - cur.execute(""" + self.conn = sqlite3.connect(path) + self.cur = self.conn.cursor() + + self.cur.execute( + """ CREATE TABLE IF NOT EXISTS Products ( - name TEXT NOT NULL PRIMARY KEY, - description TEXT NOT NULL, - price INTEGER + name TEXT NOT NULL PRIMARY KEY, + description TEXT NOT NULL, + price INTEGER ); - """) - - cur.execute(""" + """ + ) + self.cur.execute( + """ CREATE TABLE IF NOT EXISTS Users ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - type TEXT NOT NULL + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL ); - """) - - # --- создаём таблицу с покупками --- - cur.execute(""" + """ + ) + self.cur.execute( + """ CREATE TABLE IF NOT EXISTS Orders ( - date TEXT NOT NULL PRIMARY KEY, - product TEXT NOT NULL, - customer_id INTEGER, - barman_id INTEGER, - status TEXT NOT NULL, - FOREIGN KEY (product) REFERENCES products(product), - FOREIGN KEY (customer_id) REFERENCES user_base(id), - FOREIGN KEY (barman_id) REFERENCES user_base(id) + date TEXT NOT NULL PRIMARY KEY, + product TEXT NOT NULL, + customer_id INTEGER, + barman_id INTEGER, + status TEXT NOT NULL, + FOREIGN KEY (product) REFERENCES Products(name), + FOREIGN KEY (customer_id) REFERENCES Users(id), + FOREIGN KEY (barman_id) REFERENCES Users(id) ); - """) - conn.commit() - conn.close() + """ + ) + self.conn.commit() + logging.getLogger(__name__).debug("Database created") - def insert_order(self, order: Order) -> None: - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute(""" - INSERT INTO orders (date, product, customer_id, barman_id, status) - VALUES (?, ?, ?, ?, ?)""", - (order.date, order.product, order.customer_id, order.barman_id, order.status)) - conn.commit() - conn.close() + def __del__(self): + self.conn.close() - def insert_product(self, product: Product) -> None: - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute(""" - INSERT OR IGNORE INTO products (name, description, price) - values (?, ?, ?)""", - (product.name, product.description, product.price) - ) - conn.commit() - conn.close() + def fix_path(self, path: str) -> None: + """Create a directory and file if they do not exist""" + if not os.path.exists(path) and path != ":memory:": + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as file: + file.close() def insert_user(self, user: User) -> None: - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute(""" - INSERT OR IGNORE INTO Users values (?, ?, ?)""", - (user.id, user.name, user.type) - ) - conn.commit() - conn.close() + """Insert a new user into the Users table.""" + if user is None: + raise ValueError("User cannot be None") + self.cur.execute( + """ + INSERT OR IGNORE INTO Users (id, name, type) + VALUES (?, ?, ?)""", + (user.id, user.name, user.type), + ) + self.conn.commit() - def delete_order(self, order: Order) -> None: - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute('DELETE FROM Orders WHERE date = ?', (order.date,)) - conn.commit() - conn.close() + def insert_product(self, product: Product) -> None: + """Insert a new product into the Products table.""" + if product is None: + raise ValueError("Product cannot be None") + self.cur.execute( + """ + INSERT OR IGNORE INTO Products (name, description, price) + VALUES (?, ?, ?)""", + (product.name, product.description, product.price), + ) + self.conn.commit() + + def insert_order(self, order: Order) -> None: + """Insert a new order into the Orders table.""" + if order is None: + raise ValueError("Order cannot be None") + self.cur.execute( + """ + INSERT INTO Orders (date, product, customer_id, barman_id, status) + VALUES (?, ?, ?, ?, ?)""", + ( + order.date, + order.product, + order.customer_id, + order.barman_id, + order.status, + ), + ) + self.conn.commit() def delete_user(self, user: User) -> None: - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute('DELETE FROM Users WHERE id = ?', (user.id,)) - conn.commit() - conn.close() + """Delete a user from the Users table.""" + if user is None: + raise ValueError("User cannot be None") + self.cur.execute("DELETE FROM Users WHERE id = ?", (user.id,)) + self.conn.commit() def delete_product(self, product: Product) -> None: - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute('DELETE FROM Products WHERE name = ?', (product.name,)) - conn.commit() - conn.close() - - def get_all_products(self): - # Получение списка списков из бд - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute('SELECT * FROM Products') - rows: list = cur.fetchall() - conn.close() - if rows is None: - return None - # Конвертирование в список Products - result: list[Product] = [] - for row in rows: - result.append(Product(row[0], row[1], row[2])) - - return result - - def get_all_users(self): - # Получение списка списков из бд - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute('SELECT * FROM Users') - rows: list = cur.fetchall() - conn.close() - if rows is None: - return None - # Конвертирование в список Products - result: list[User] = [] - for row in rows: - result.append(User(row[0], row[1], row[2])) - - return result - - def get_all_orders(self): - # Получение списка списков из бд - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute('SELECT * FROM Orders') - rows: list = cur.fetchall() - conn.close() - if rows is None: - return None - # Конвертирование в список Products - result: list[Order] = [] - for row in rows: - result.append(Order(row[0], row[1], row[2], row[3], row[4])) - - return result + """Delete a product from the Products table.""" + if product is None: + raise ValueError("Product cannot be None") + self.cur.execute("DELETE FROM Products WHERE name = ?", (product.name,)) + self.conn.commit() - def get_user_by_id(self, user_id: int) -> User: - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute('SELECT * FROM Users WHERE id = ?', (user_id,)) - found = cur.fetchone() - conn.close() - if found is None: - return None - return User(found[0], found[1], found[2]) + def delete_order(self, order: Order) -> None: + """Delete an order from the Orders table.""" + if order is None: + raise ValueError("Order cannot be None") + self.cur.execute("DELETE FROM Orders WHERE date = ?", (order.date,)) + self.conn.commit() + + def get_all_products(self) -> list[Product]: + """Retrieve all products from the Products table.""" + self.cur.execute("SELECT * FROM Products") + rows = self.cur.fetchall() + return [Product(row[0], row[1], row[2]) for row in rows] + + def get_all_users(self) -> list[User]: + """Retrieve all users from the Users table.""" + self.cur.execute("SELECT * FROM Users") + rows = self.cur.fetchall() + return [User(row[0], row[1], row[2]) for row in rows] + + def get_all_orders(self) -> list[Order]: + """Retrieve all orders from the Orders table.""" + self.cur.execute("SELECT * FROM Orders") + rows = self.cur.fetchall() + return [Order(row[0], row[1], row[2], row[3], row[4]) for row in rows] + def get_user_by_id(self, user_id: int) -> User: + """Retrieve a user by their ID from the Users table.""" + self.cur.execute("SELECT * FROM Users WHERE id = ?", (user_id,)) + found = self.cur.fetchone() + return User(found[0], found[1], found[2]) if found else None def get_product_by_name(self, name: str) -> Product: - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute('SELECT * FROM Products WHERE name = ?', (name,)) - found = cur.fetchone() - if found is None: - return None - conn.close() - return Product(found[0], found[1], found[2]) - + """Retrieve a product by its name from the Products table.""" + self.cur.execute("SELECT * FROM Products WHERE name = ?", (name,)) + found = self.cur.fetchone() + return Product(found[0], found[1], found[2]) if found else None def get_order_by_date(self, order_date: str) -> Order: - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute("SELECT * FROM Orders WHERE date = ?", (order_date,)) - found = cur.fetchone() - conn.close() - return Order(found[0], found[1], found[2], found[3], found[4]) - - - def get_orders_by_customer_id(self, id: int): - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute("SELECT * FROM Orders WHERE customer_id = ?", (id,)) - rows: list = cur.fetchall() - conn.close() - if rows is None: - return None - # Конвертирование в список Products - result: list[Order] = [] - for row in rows: - result.append(Order(row[0], row[1], row[2], row[3], row[4])) - return result - - def get_orders_queue(self): - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute("SELECT * FROM Orders WHERE status = ?", ("размещён",)) - rows: list = cur.fetchall() - conn.close() - if rows is None: - return None - # Конвертирование в список Products - result: list[Order] = [] - for row in rows: - result.append(Order(row[0], row[1], row[2], row[3], row[4])) - return result - - def update_order(self, order: Order): - conn = sqlite3.connect(database_path) - cur = conn.cursor() - cur.execute(""" - UPDATE orders SET barman_id = ?, status = ? WHERE date = ?""", (order.barman_id, order.status, order.date,)) - conn.commit() - conn.close() + """Retrieve an order by its date from the Orders table.""" + self.cur.execute("SELECT * FROM Orders WHERE date = ?", (order_date,)) + found = self.cur.fetchone() + return ( + Order(found[0], found[1], found[2], found[3], found[4]) if found else None + ) + + def get_orders_by_customer_id(self, customer_id: int) -> list[Order]: + """Retrieve orders by customer ID from the Orders table.""" + self.cur.execute("SELECT * FROM Orders WHERE customer_id = ?", (customer_id,)) + rows = self.cur.fetchall() + return [Order(row[0], row[1], row[2], row[3], row[4]) for row in rows] + + def get_orders_queue(self) -> list[Order]: + """Retrieve orders with status 'размещён' from the Orders table.""" + self.cur.execute("SELECT * FROM Orders WHERE status = ?", ("размещён",)) + rows = self.cur.fetchall() + return [Order(row[0], row[1], row[2], row[3], row[4]) for row in rows] + + def update_user(self, user: User) -> None: + """Update a user in the Users table.""" + if user is None: + raise ValueError("User cannot be None") + self.cur.execute( + """ + UPDATE Users SET name = ?, type = ? WHERE id = ?""", + (user.name, user.type, user.id), + ) + self.conn.commit() + + def update_product(self, product: Product) -> None: + """Update a product in the Products table.""" + if product is None: + raise ValueError("Product cannot be None") + self.cur.execute( + """ + UPDATE Products SET description = ?, price = ? WHERE name = ?""", + (product.description, product.price, product.name), + ) + self.conn.commit() + + def update_order(self, order: Order) -> None: + """Update an order in the Orders table.""" + if order is None: + raise ValueError("Order cannot be None") + self.cur.execute( + """ + UPDATE Orders SET barman_id = ?, status = ? WHERE date = ?""", + (order.barman_id, order.status, order.date), + ) + self.conn.commit() diff --git a/bot/database/order.py b/bot/database/order.py index fa6a52d..eaf2e51 100644 --- a/bot/database/order.py +++ b/bot/database/order.py @@ -1,52 +1,54 @@ -import datetime +"""Module for the Order class.""" -from bot.database.product import Product +from dataclasses import dataclass + +@dataclass class Order: + """Class representing an order in the system.""" - def __init__(self, date, product, customer_id, barman_id, status) -> None: - """self.id = id""" - self.date: str = date - self.product: str = product - self.customer_id: int = customer_id - self.barman_id: int = barman_id - self.status: str = status + date: str + product: str + customer_id: int + barman_id: int + status: str - if self.status not in ['размещён', 'завершён']: - raise ValueError("Invalid status type") + def get_order_date(self) -> str: + """Get the date of the order.""" + return self.date - """def get_order_id(self) -> int: - return self.id""" + def get_order_product(self) -> str: + """Get the product of the order.""" + return self.product def get_order_customer_id(self) -> int: + """Get the customer ID of the order.""" return self.customer_id - def get_order_product(self) -> Product: - return self.choice - - def get_order_date(self) -> datetime: - return self.date + def get_order_barman_id(self) -> int: + """Get the barman ID of the order.""" + return self.barman_id def get_order_status(self) -> str: + """Get the status of the order.""" return self.status - def get_order_barman_id(self) -> str: - return self.barman_id - - """def set_order_id(self, val) -> None: - self.id = val""" - - def set_order_customer_id(self, val) -> None: - self.customer_id = val + def set_order_date(self, date: str): + """Set the date of the order.""" + self.date = date - def get_order_product(self, val) -> None: - self.product = val + def set_order_product(self, product: str): + """Set the product of the order.""" + self.product = product - def set_order_date(self, val) -> None: - self.date = val + def set_order_customer_id(self, customer_id: int): + """Set the customer ID of the order.""" + self.customer_id = customer_id - def set_order_status(self, val) -> None: - self.status = val + def set_order_barman_id(self, barman_id: int): + """Set the barman ID of the order.""" + self.barman_id = barman_id - def set_order_barman_id(self, val) -> None: - self.barman_id = val + def set_order_status(self, status: str): + """Set the status of the order.""" + self.status = status diff --git a/bot/database/product.py b/bot/database/product.py index da2c025..e829953 100644 --- a/bot/database/product.py +++ b/bot/database/product.py @@ -1,27 +1,36 @@ +"""Module for the Product class.""" + +from dataclasses import dataclass + + +@dataclass class Product: + """Class representing a product in the system.""" - def __init__(self, name, description, price) -> None: - # self.tag = tag - self.name = name - # self.photo = photo - self.description = description - self.price = price - # self.composition = composition + name: str + description: str + price: float def get_name(self) -> str: + """Get the name of the product.""" return self.name def get_price(self) -> float: + """Get the price of the product.""" return self.price def get_description(self) -> str: + """Get the description of the product.""" return self.description - def set_name(self, val) -> None: + def set_name(self, val: str) -> None: + """Set the name of the product.""" self.name = val - def set_price(self, val) -> None: + def set_price(self, val: float) -> None: + """Set the price of the product.""" self.price = val - def set_description(self, val) -> None: + def set_description(self, val: str) -> None: + """Set the description of the product.""" self.description = val diff --git a/bot/database/user.py b/bot/database/user.py index b7c30c2..7a9ce10 100644 --- a/bot/database/user.py +++ b/bot/database/user.py @@ -1,27 +1,43 @@ +"""Module for the User class.""" + +from dataclasses import dataclass + + +@dataclass class User: + """Class representing a user in the system.""" - def __init__(self, id, name, type) -> None: - self.id = id - self.name = name - self.type = type + id: int + name: str + type: str = "customer" - if self.type not in ['barman', 'admin', 'customer']: + def __post_init__(self): + """Post-initialization processing.""" + if self.type not in ["barman", "admin", "customer"]: raise ValueError("Invalid user type") def get_user_name(self) -> str: + """Get the name of the user.""" return self.name - def get_user_id(self) -> str: + def get_user_id(self) -> int: + """Get the ID of the user.""" return self.id def get_user_type(self) -> str: + """Get the type of the user.""" return self.type - def set_user_name(self, name) -> None: + def set_user_name(self, name: str) -> None: + """Set the name of the user.""" self.name = name - def set_user_id(self, user_id) -> None: + def set_user_id(self, user_id: int) -> None: + """Set the ID of the user.""" self.id = user_id - def set_user_type(self, user_type) -> None: + def set_user_type(self, user_type: str) -> None: + """Set the type of the user.""" + if user_type not in ["barman", "admin", "customer"]: + raise ValueError("Invalid user type") self.type = user_type diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py index 9f5a8b3..481f8c6 100644 --- a/bot/handlers/__init__.py +++ b/bot/handlers/__init__.py @@ -1,16 +1,27 @@ +""" +This module contains the handler registration for the bot. +""" + from telegram.ext import CallbackQueryHandler, CommandHandler from bot.handlers.callback_handler import callback_handler from bot.handlers.help_handler import help_handler from bot.handlers.menu_handler import menu_handler from bot.handlers.start_handler import start_handler +from bot.handlers.error_handler import error_handler + def register_handlers(application): + """ + Register all handlers to the application. + """ application.add_handler(CallbackQueryHandler(callback_handler)) - application.add_handler(CommandHandler('help', help_handler)) - application.add_handler(CommandHandler('menu', menu_handler)) - application.add_handler(CommandHandler('start', start_handler)) + application.add_handler(CommandHandler("help", help_handler)) + application.add_handler(CommandHandler("menu", menu_handler)) + application.add_handler(CommandHandler("start", start_handler)) + application.add_error_handler(error_handler) + __all__ = [ - 'register_handlers', -] \ No newline at end of file + "register_handlers", +] diff --git a/bot/handlers/callback_handler.py b/bot/handlers/callback_handler.py index f844597..914f93b 100644 --- a/bot/handlers/callback_handler.py +++ b/bot/handlers/callback_handler.py @@ -1,48 +1,43 @@ +""" +This module contains the callback handler for the bot. +""" + import logging from telegram import Update from telegram.ext import CallbackContext from telegram.constants import ParseMode from bot.database import Database -from bot.roles.admin import Admin -from bot.roles.barman import Barman -from bot.roles.customer import Customer - +from bot.roles import role_associations +from bot.settings import load_texts -async def callback_handler(update: Update, context: CallbackContext) -> None: +async def callback_handler(update: Update, _context: CallbackContext) -> None: """ - This handler processes the inline buttons on the menu + This handler processes the inline buttons on the menu. """ - database = Database() + db = Database() tg_user = update.effective_user - current_user = database.get_user_by_id(tg_user.id) + current_user = db.get_user_by_id(tg_user.id) data = update.callback_query.data - text = '' + text = "" markup = None - if current_user.type == "customer": - text, markup = Customer.on_button_tap(Customer, data, tg_user.id) - if markup is None: - text, markup = Customer.back_to_customer_menu(Customer, data, tg_user.id) - elif current_user.type == "barman": - text, markup = Barman.on_button_tap(Barman, data, tg_user.id) - if markup is None: - text, markup = Barman.back_to_barman_menu(Barman, data, tg_user.id) - elif current_user.type == "admin": - pass + role_class = role_associations.get(current_user.type) + texts = load_texts(tg_user.language_code)["texts"] + role_obj = role_class(db, tg_user.id, texts) + if role_class: + text, markup = role_obj.on_button_tap(data) + if not text or markup is None: + text, markup = "Callback, Err0r", role_obj.build_menu() else: - logging.getLogger(__name__).error("incorrect user type") + logging.error("incorrect user type") - logging.getLogger(__name__).info(f'{update.effective_user.id} Callbackdata: {data}') + logging.info("Callbackdata: %s", data) - # Close the query to end the client-side loading animation await update.callback_query.answer() - # Update message content with corresponding menu section await update.callback_query.edit_message_text( - text, - ParseMode.HTML, - reply_markup=markup - ) \ No newline at end of file + text, ParseMode.HTML, reply_markup=markup + ) diff --git a/bot/handlers/error_handler.py b/bot/handlers/error_handler.py new file mode 100644 index 0000000..0e8d4f2 --- /dev/null +++ b/bot/handlers/error_handler.py @@ -0,0 +1,17 @@ +""" +This module contains the error handler for the bot. +""" + +import logging +from telegram import Update +from telegram.ext import CallbackContext + + +async def error_handler(update: Update, context: CallbackContext) -> None: + """ + Log the error caused by an update. + """ + logging.error( + msg="Exception while handling an update:" + str(update.effective_user.id), + exc_info=context.error, + ) diff --git a/bot/handlers/help_handler.py b/bot/handlers/help_handler.py index a3e3340..9f32389 100644 --- a/bot/handlers/help_handler.py +++ b/bot/handlers/help_handler.py @@ -1,9 +1,19 @@ +""" +This module defines the help handler for the bot. +""" + import logging from telegram import Update from telegram.ext import ContextTypes +from bot.settings import load_texts + -async def help_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: +async def help_handler(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: """Send a message when the command /help is issued.""" - logging.getLogger(__name__).info(f'{update.message.from_user.id} use {update.message.text}') - await update.message.reply_text("Помощь.") \ No newline at end of file + logging.getLogger(__name__).info( + "%s use %s", update.message.from_user.id, update.message.text + ) + await update.message.reply_text( + load_texts(update.effective_user.language_code)["texts"]["help"] + ) diff --git a/bot/handlers/menu_handler.py b/bot/handlers/menu_handler.py index 8d28a49..1b26324 100644 --- a/bot/handlers/menu_handler.py +++ b/bot/handlers/menu_handler.py @@ -1,45 +1,36 @@ +""" +This module contains the menu handler for the bot. +""" + import logging from telegram import Update from telegram.ext import CallbackContext from telegram.constants import ParseMode from bot.database import Database -from bot.roles import Customer, Barman +from bot.roles import role_associations +from bot.settings import load_texts + async def menu_handler(update: Update, context: CallbackContext) -> None: """ This handler sends a menu with the inline buttons we pre-assigned above """ - logging.getLogger(__name__).info(f'{update.message.from_user.id} use {update.message.text}') + logging.getLogger(__name__).info( + "%s use %s", update.message.from_user.id, update.message.text + ) - database = Database() + db = Database() tg_user = update.effective_user - current_user = database.get_user_by_id(tg_user.id) - - if current_user.type == "customer": - await context.bot.send_message( - update.message.from_user.id, - Customer.CUSTOMER_MENU_TEXT, - parse_mode=ParseMode.HTML, - reply_markup=Customer.build_customer_menu(Customer) - ) + current_user = db.get_user_by_id(tg_user.id) + role_class = role_associations.get(current_user.type) + texts = load_texts(tg_user.language_code)["texts"] + role_object = role_class(db, tg_user.id, texts) - elif current_user.type == "barman": - await context.bot.send_message( - update.message.from_user.id, - Customer.CUSTOMER_MENU_TEXT, - parse_mode=ParseMode.HTML, - reply_markup=Barman.build_barman_menu(Barman) - ) - pass - elif current_user.type == "admin": - """await context.bot.send_message( - update.message.from_user.id, - CUSTOMER_MENU_TEXT, - parse_mode=ParseMode.HTML, - reply_markup=build_customer_menu() - )""" - pass - else: - logging.getLogger(__name__).error("incorrect user type") + await context.bot.send_message( + update.message.from_user.id, + texts["menu"], + parse_mode=ParseMode.HTML, + reply_markup=role_object.build_menu(), + ) diff --git a/bot/handlers/start_handler.py b/bot/handlers/start_handler.py index df4fb21..e4f3a9c 100644 --- a/bot/handlers/start_handler.py +++ b/bot/handlers/start_handler.py @@ -1,19 +1,25 @@ +""" +This module defines the start handler for the bot. +""" + import logging from telegram import Update from telegram.ext import ContextTypes from bot.database import Database, User +from bot.settings import load_texts -async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: +async def start_handler(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None: """Send a message when the command /start is issued.""" - logging.getLogger(__name__).info(f'{update.message.from_user.id} use {update.message.text}') + logging.getLogger(__name__).info( + "%s use %s", update.message.from_user.id, update.message.text + ) user = update.effective_user - """await update.message.reply_html( - rf"Hi {user.mention_html()}!", - reply_markup=ForceReply(selective=True), - )""" + database = Database() - new_user = User(user.id, user.name, "customer") + new_user = User(user.id, user.name) database.insert_user(new_user) - await update.message.reply_text('Привет! Я бот. Нажимай на кнопку "/help" для получения подсказок по командам.') \ No newline at end of file + await update.message.reply_text( + load_texts(user.language_code)["texts"]["start"], + ) diff --git a/bot/roles/__init__.py b/bot/roles/__init__.py index 824fafc..ff2c1af 100644 --- a/bot/roles/__init__.py +++ b/bot/roles/__init__.py @@ -1,9 +1,11 @@ +""" +This module initializes role associations for the bot. +""" + from bot.roles.admin import Admin from bot.roles.barman import Barman from bot.roles.customer import Customer -__all__ = [ - 'Admin', - 'Barman', - 'Customer' - ] \ No newline at end of file +__all__ = ["Admin", "Barman", "Customer", "role_associations"] + +role_associations = {"admin": Admin, "customer": Customer, "barman": Barman} diff --git a/bot/roles/admin.py b/bot/roles/admin.py index ee467b0..bd77e90 100644 --- a/bot/roles/admin.py +++ b/bot/roles/admin.py @@ -1,64 +1,11 @@ -import logging +""" +This module defines the Admin role for the bot. +""" -from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton -from telegram.constants import ParseMode -from telegram.ext import CallbackContext +from bot.roles.barman import Barman -# Pre-assign menu text -FIRST_MENU_MARKUP = None -FIRST_MENU = "Админ Панель\n\nХуй" -SECOND_MENU = "Menu 2\n\nЗалупа" -# Pre-assign button text -NEXT_BUTTON = "Next" -BACK_BUTTON = "Back" -TUTORIAL_BUTTON = "Tutorial" - -# Build keyboards -def build_menu_keyboard(): - buttons = [] - return InlineKeyboardMarkup(buttons) - - -class Admin: - def __init__(self): - pass - - async def menu(update: Update, context: CallbackContext) -> None: - """ - This handler sends a menu with the inline buttons we pre-assigned above - """ - # logging.getLogger(__name__).info(f'{update.message.from_user.id} use {update.message.text}') - - await context.bot.send_message( - update.message.from_user.id, - FIRST_MENU, - parse_mode=ParseMode.HTML, - reply_markup=FIRST_MENU_MARKUP - ) - - async def on_button_tap(update: Update, context: CallbackContext) -> None: - """ - This handler processes the inline buttons on the menu - """ - - data = update.callback_query.data - text = '' - markup = None - - if data == NEXT_BUTTON: - text = SECOND_MENU - markup = SECOND_MENU_MARKUP - elif data == BACK_BUTTON: - text = FIRST_MENU - markup = FIRST_MENU_MARKUP - - # Close the query to end the client-side loading animation - await update.callback_query.answer() - - # Update message content with corresponding menu section - await update.callback_query.edit_message_text( - text, - ParseMode.HTML, - reply_markup=markup - ) +class Admin(Barman): + """ + Represents an admin role in the bot. + """ diff --git a/bot/roles/barman.py b/bot/roles/barman.py index 8bc85f7..6cac802 100644 --- a/bot/roles/barman.py +++ b/bot/roles/barman.py @@ -1,101 +1,126 @@ -import logging -from telegram import ForceReply, Update, InlineKeyboardMarkup, InlineKeyboardButton +""" +This module contains the Barman class which represents a barman interacting with the bot. +""" +import logging +from telegram import InlineKeyboardMarkup, InlineKeyboardButton from bot.roles.customer import Customer -from bot.database import Database, Order +from bot.database import Database class Barman(Customer): - QUEUE_BUTTON = "Очередь заказов" - QUEUE_TEXT = "Очередь" - COMPLETE_BUTTON = "Завершить заказ" + """ + A class to represent a barman interacting with the bot. + """ + + def __init__(self, db: Database, tg_user_id: int, texts: dict): + super().__init__(db, tg_user_id, texts) + self.texts = texts def create_barman_buttons_menu(self): - buttons = Customer.create_customer_menu_buttons(Customer) - buttons.append([InlineKeyboardButton(self.QUEUE_BUTTON, callback_data=self.QUEUE_BUTTON)]) + """Create buttons for the barman menu""" + buttons = self.create_customer_menu_buttons() + buttons.append( + [ + InlineKeyboardButton( + self.texts["queue_button"], callback_data=self.texts["queue_button"] + ) + ] + ) return buttons - def build_barman_menu(self): - return InlineKeyboardMarkup(self.create_barman_buttons_menu(self)) + def build_menu(self) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup(self.create_barman_buttons_menu()) - def build_queue_menu(self) -> InlineKeyboardMarkup: - db = Database() - my_orders = db.get_orders_queue() + def __build_queue_menu(self) -> InlineKeyboardMarkup: + my_orders = self.db.get_orders_queue() buttons = [] if my_orders is not None: for my_order in my_orders: - button = InlineKeyboardButton(f'{my_order.date[:-7]} {my_order.product}', callback_data=f'chose_{my_order.date}') + button = InlineKeyboardButton( + f"{my_order.date[:-7]} {my_order.product}", + callback_data=f"complete_{my_order.date}", + ) buttons.append([button]) else: - logging.getLogger(__name__).info(f"No orders in database") - buttons.append( - [InlineKeyboardButton(Customer.BACK_TO_MENU_BUTTON, callback_data=Customer.BACK_TO_MENU_BUTTON)]) + logging.getLogger(__name__).info("No orders in database") + buttons.append(self.back_to_menu_button) return InlineKeyboardMarkup(buttons) - def build_pre_complete_order_menu(self, data): - return InlineKeyboardMarkup([[InlineKeyboardButton(self.COMPLETE_BUTTON, callback_data=data)], - [InlineKeyboardButton(Customer.BACK_BUTTON, callback_data=self.QUEUE_BUTTON)], - [InlineKeyboardButton(Customer.BACK_TO_MENU_BUTTON, callback_data=Customer.BACK_TO_MENU_BUTTON)] - ]) - - def build_complete_order_menu(self): - return InlineKeyboardMarkup([ - [InlineKeyboardButton(Customer.BACK_BUTTON, callback_data=self.QUEUE_BUTTON)], - [InlineKeyboardButton(Customer.BACK_TO_MENU_BUTTON, callback_data=Customer.BACK_TO_MENU_BUTTON)] - ]) - - def back_to_barman_menu(self, data, tg_user_id): - text = '' - markup = None - - # Обработка кнопки назад в меню - if data == self.BACK_TO_MENU_BUTTON: - logging.getLogger(__name__).info(f'{tg_user_id} return to the barman_menu') - text = self.CUSTOMER_MENU_TEXT - markup = self.build_barman_menu(self) - - return text, markup - - - def on_button_tap(self, data, tg_user_id): - text = '' - markup = None - - text, markup = Customer.on_button_tap(Customer, data, tg_user_id) - - db = Database() - - my_orders: list[Order] = db.get_all_orders() - order_dates = [str(Order.get_order_date()) for Order in my_orders] - - - if data == self.QUEUE_BUTTON: - logging.getLogger(__name__).info( - f'{tg_user_id} press the QUEUE_BUTTON or return to the QUEUE menu') - text = f'{self.QUEUE_BUTTON}' - markup = self.build_queue_menu(self) - - if data[6:] in order_dates and data[:6] == "chose_": - logging.getLogger(__name__).info(f'{tg_user_id} watch for the {data}') - db = Database() - order = db.get_order_by_date(data[6:]) - text = f''' - Заказ от: {order.date[:-7]}\nПродукт: {order.product}\nId покупателя: {order.customer_id}\nId бармена: {order.barman_id}\nСтатус: {order.status}''' - markup = self.build_pre_complete_order_menu(self, str("next"+data)) - - if data[10:] in order_dates and data[:10] == "nextchose_": - db = Database() - order = db.get_order_by_date(data[10:]) - order.set_order_barman_id(tg_user_id) - order.set_order_status("завершён") - db.update_order(order) - text = f'''Заказ завершён!!!\nОт: {order.date[:-7]}\nПродукт: {order.product}\nId покупателя: {order.customer_id}\nId бармена: {order.barman_id}\nСтатус: {order.status}''' - markup = self.build_complete_order_menu(self) - + def __build_pre_complete_order_menu(self, data) -> InlineKeyboardMarkup: + buttons = [ + [ + InlineKeyboardButton( + self.texts["complete_button"], callback_data="c" + data + ) + ], + [ + InlineKeyboardButton( + self.texts["back_button"], callback_data=self.texts["queue_button"] + ) + ], + self.back_to_menu_button, + ] + return InlineKeyboardMarkup(buttons) + def __build_complete_order_menu(self) -> InlineKeyboardMarkup: + buttons = [ + [ + InlineKeyboardButton( + self.texts["back_button"], callback_data=self.texts["queue_button"] + ) + ], + self.back_to_menu_button, + ] + return InlineKeyboardMarkup(buttons) + def __handle_queue_button(self): + logging.getLogger(__name__).info( + "%s press the QUEUE_BUTTON or return to the QUEUE menu", self.tg_user_id + ) + text = self.texts["queue_text"] + markup = self.__build_queue_menu() return text, markup + def __handle_pre_complete_order(self, data): + logging.getLogger(__name__).info("%s watch for the %s", self.tg_user_id, data) + order = self.db.get_order_by_date(data[9:]) + text = ( + f"""Заказ от: {order.date[:-7]}\n""" + f"""Продукт: {order.product}\n""" + f"""Id покупателя: {order.customer_id}\n""" + f"""Id бармена: {order.barman_id}\n""" + f"""Статус: {order.status}""" + ) + markup = self.__build_pre_complete_order_menu(data) + return text, markup + def __handle_complete_order(self, data): + logging.getLogger(__name__).info("%s approved the %s", self.tg_user_id, data) + order = self.db.get_order_by_date(data[10:]) + order.set_order_barman_id(self.tg_user_id) + order.set_order_status("завершён") + self.db.update_order(order) + text = ( + f"""Заказ завершён!!!\n""" + f"""От: {order.date[:-7]}\n""" + f"""Продукт: {order.product}\n""" + f"""Id покупателя: {order.customer_id}\n""" + f"""Id бармена: {order.barman_id}\n""" + f"""Статус: {order.status}""" + ) + markup = self.__build_complete_order_menu() + return text, markup + def on_button_tap(self, data) -> (str, InlineKeyboardMarkup): + text, markup = super().on_button_tap(data) + if text != "Err0r": + return text, markup + if data == self.texts["queue_button"]: + return self.__handle_queue_button() + if data.startswith("complete_"): + return self.__handle_pre_complete_order(data) + if data.startswith("ccomplete_"): + return self.__handle_complete_order(data) + return "Err0r", self.build_menu() diff --git a/bot/roles/customer.py b/bot/roles/customer.py index 4975506..e1ffe8c 100644 --- a/bot/roles/customer.py +++ b/bot/roles/customer.py @@ -1,191 +1,287 @@ +""" +This module contains the Customer class which represents a customer interacting with the bot. +""" + import logging import datetime from telegram import InlineKeyboardMarkup, InlineKeyboardButton - -from bot.database import Database, Product, Order +from bot.database import Product, Order class Customer: - CUSTOMER_MENU_TEXT = "Менюшечка\n\n" - SHOW_PRODUCTS_TEXT = "Барная карта\n\n Нажмите на напиток, чтобы показать подробности." - MAKE_ORDER_TEXT = "Выбор напитка\n\n Нажмите на напиток, чтобы заказать" - SHOW_ORDERS_TEXT = "Мои заказы\n\n" - - # Тексты для кнопок - SHOW_PRODUCTS_BUTTON = "Барная карта" - MAKE_ORDER_BUTTON = "Сделать заказ" - SHOW_ORDERS_BUTTON = "Мои заказы" - APPROVE_ORDERS_BUTTON = "Подтвердить заказ." - BACK_TO_MENU_BUTTON = "Вернуться в главное меню" - BACK_BUTTON = "Назад" - - def __init__(self): - - pass + """ + A class to represent a customer interacting with the bot. + """ + + def __init__(self, db, tg_user_id, texts): + """ + Initialize the Customer with database, Telegram user ID, and texts. + """ + self.db = db + self.tg_user_id = tg_user_id + self.texts = texts + self.back_to_menu_button = [ + InlineKeyboardButton( + self.texts["back_to_menu_button"], + callback_data=self.texts["back_to_menu_button"], + ) + ] def create_customer_menu_buttons(self): - return [[InlineKeyboardButton(self.SHOW_PRODUCTS_BUTTON, callback_data=self.SHOW_PRODUCTS_BUTTON)], - [InlineKeyboardButton(self.MAKE_ORDER_BUTTON, callback_data=self.MAKE_ORDER_BUTTON)], - [InlineKeyboardButton(self.SHOW_ORDERS_BUTTON, callback_data=self.SHOW_ORDERS_BUTTON)]] - - - def build_customer_menu(self) -> InlineKeyboardMarkup: - return InlineKeyboardMarkup(self.create_customer_menu_buttons(self)) - - def build_show_products_menu(self) -> InlineKeyboardMarkup: - db = Database() - my_products = db.get_all_products() - buttons = [] - if my_products is not None: - for product in my_products: - button = InlineKeyboardButton(product.name, callback_data=f'shown_{product.name}') - buttons.append([button]) - else: - logging.getLogger(__name__).info(f"No products in database") - buttons.append([InlineKeyboardButton(self.BACK_TO_MENU_BUTTON, callback_data=self.BACK_TO_MENU_BUTTON)]) + """Create buttons for the customer menu.""" + return [ + [ + InlineKeyboardButton( + self.texts["show_products_button"], + callback_data=self.texts["show_products_button"], + ) + ], + [ + InlineKeyboardButton( + self.texts["make_order_button"], + callback_data=self.texts["make_order_button"], + ) + ], + [ + InlineKeyboardButton( + self.texts["show_orders_button"], + callback_data=self.texts["show_orders_button"], + ) + ], + ] + + def build_menu(self) -> InlineKeyboardMarkup: + """ + Build the customer menu with inline buttons. + """ + buttons = self.create_customer_menu_buttons() return InlineKeyboardMarkup(buttons) - """def build_show_product_info_menu() -> InlineKeyboardMarkup: - buttons = [[InlineKeyboardButton(BACK_TO_CUSTOMER_MENU_BUTTON, callback_data=BACK_TO_CUSTOMER_MENU_BUTTON)], - [InlineKeyboardButton(BACK_BUTTON, callback_data=SHOW_PRODUCTS_BUTTON)]] - return InlineKeyboardMarkup(buttons)""" - - def build_show_product_info_menu(self, data) -> InlineKeyboardMarkup: - buttons = [[InlineKeyboardButton("Сделать заказ", callback_data=data)], - [InlineKeyboardButton(self.BACK_BUTTON, callback_data=self.SHOW_PRODUCTS_BUTTON)], - [InlineKeyboardButton(self.BACK_TO_MENU_BUTTON, callback_data=self.BACK_TO_MENU_BUTTON)]] - return InlineKeyboardMarkup(buttons) - - def build_make_orders_menu(self) -> InlineKeyboardMarkup: - db = Database() - my_products = db.get_all_products() + def __build_show_products_menu(self) -> InlineKeyboardMarkup: + """ + Build the menu to show products. + """ + my_products = self.db.get_all_products() buttons = [] if my_products is not None: for product in my_products: - button = InlineKeyboardButton(product.name, callback_data=f'chose_{product.name}') + button = InlineKeyboardButton( + product.name, callback_data=f"shown_{product.name}" + ) buttons.append([button]) else: - logging.getLogger(__name__).info(f"No products in database") - buttons.append([InlineKeyboardButton(self.BACK_TO_MENU_BUTTON, callback_data=self.BACK_TO_MENU_BUTTON)]) + logging.getLogger(__name__).info("No products in database") + buttons.append(self.back_to_menu_button) return InlineKeyboardMarkup(buttons) - def build_pre_approve_order_menu(self, data) -> InlineKeyboardMarkup: - buttons = [[InlineKeyboardButton(self.APPROVE_ORDERS_BUTTON, callback_data=data)], - [InlineKeyboardButton(self.BACK_BUTTON, callback_data=self.MAKE_ORDER_BUTTON)], - [InlineKeyboardButton(self.BACK_TO_MENU_BUTTON, callback_data=self.BACK_TO_MENU_BUTTON)]] + def __build_show_product_info_menu(self, data) -> InlineKeyboardMarkup: + """ + Build the menu to show product information. + """ + buttons = [ + [InlineKeyboardButton("Сделать заказ", callback_data="chose_" + data[6:])], + [ + InlineKeyboardButton( + self.texts["back_button"], + callback_data=self.texts["show_products_button"], + ) + ], + self.back_to_menu_button, + ] return InlineKeyboardMarkup(buttons) - def build_approve_order_menu(self, pre_data) -> InlineKeyboardMarkup: - buttons = ([InlineKeyboardButton(self.BACK_BUTTON, callback_data=pre_data)], - [InlineKeyboardButton(self.BACK_TO_MENU_BUTTON, callback_data=self.BACK_TO_MENU_BUTTON)]) + def __back_to_menu(self): + """ + Return to the main menu. + """ + return self.texts["menu"], self.build_menu() + + def __build_pre_approve_order_menu(self, data) -> InlineKeyboardMarkup: + """ + Build the menu to pre-approve an order. + """ + buttons = [ + [ + InlineKeyboardButton( + self.texts["approve_orders_button"], callback_data=data + ) + ], + [ + InlineKeyboardButton( + self.texts["back_button"], + callback_data=self.texts["make_order_button"], + ) + ], + self.back_to_menu_button, + ] + return InlineKeyboardMarkup(buttons) + def __build_approve_order_menu(self, pre_data) -> InlineKeyboardMarkup: + """ + Build the menu to approve an order. + """ + buttons = [ + [InlineKeyboardButton(self.texts["back_button"], callback_data=pre_data)], + self.back_to_menu_button, + ] return InlineKeyboardMarkup(buttons) - def build_show_orders_menu(self, id) -> InlineKeyboardMarkup: - db = Database() - my_orders = db.get_orders_by_customer_id(id) + def __build_show_orders_menu(self) -> InlineKeyboardMarkup: + """ + Build the menu to show orders. + """ + my_orders = self.db.get_orders_by_customer_id(self.tg_user_id) buttons = [] if my_orders is not None: for my_order in my_orders: - button = InlineKeyboardButton(my_order.date[:-7], callback_data=f'shown_{my_order.date}') + button = InlineKeyboardButton( + my_order.date[:-7], callback_data=my_order.date + ) buttons.append([button]) else: - logging.getLogger(__name__).info(f"No orders found for user") - - buttons.append([InlineKeyboardButton(self.BACK_TO_MENU_BUTTON, callback_data=self.BACK_TO_MENU_BUTTON)]) + logging.getLogger(__name__).info( + "No orders found for user %s", self.tg_user_id + ) + buttons.append(self.back_to_menu_button) return InlineKeyboardMarkup(buttons) - def build_show_order_info_menu(self) -> InlineKeyboardMarkup: - buttons = [[InlineKeyboardButton(self.BACK_BUTTON, callback_data=self.SHOW_ORDERS_BUTTON)], - [InlineKeyboardButton(self.BACK_TO_MENU_BUTTON, callback_data=self.BACK_TO_MENU_BUTTON)]] + def __build_show_order_info_menu(self) -> InlineKeyboardMarkup: + """ + Build the menu to show order information. + """ + buttons = [ + [ + InlineKeyboardButton( + self.texts["back_button"], + callback_data=self.texts["show_orders_button"], + ) + ], + self.back_to_menu_button, + ] return InlineKeyboardMarkup(buttons) - - def back_to_customer_menu(self, data, tg_user_id) -> [str, InlineKeyboardMarkup]: - text = '' - markup = None - # Обработка кнопки назад в меню - if data == self.BACK_TO_MENU_BUTTON: - logging.getLogger(__name__).info(f'{tg_user_id} return to the CUSTOMER_MENU') - text = self.CUSTOMER_MENU_TEXT - markup = self.build_customer_menu(self) - return text, markup - - def on_button_tap(self, data, tg_user_id) -> (str, InlineKeyboardMarkup): - text = '' - markup = None - - db = Database() - my_products: list[Product] = db.get_all_products() - product_names = [Product.get_name() for Product in my_products] - - my_orders: list[Order] = db.get_all_orders() - order_dates = [str(Order.get_order_date()) for Order in my_orders] - - # Обработка кнопок Барной карты - if data == self.SHOW_PRODUCTS_BUTTON: - logging.getLogger(__name__).info( - f'{tg_user_id} press the SHOW_PRODUCTS_BUTTON or return to SHOW_PRODUCTS menu') - text = self.SHOW_PRODUCTS_TEXT - markup = self.build_show_products_menu(self) - - - if data[6:] in product_names and data[:6] == "shown_": - logging.getLogger(__name__).info(f'{tg_user_id} watch for the {data[6:]}') - db = Database() - product = db.get_product_by_name(data[6:]) - text = f'{product.name}\nОписание:\n{product.description}\nЦена:\n{product.price}руб.' - markup = self.build_show_product_info_menu(self, str(data[1:])) - - # Обработка кнопок для создания заказа - if data == self.MAKE_ORDER_BUTTON: - logging.getLogger(__name__).info( - f'{tg_user_id} press the MAKE_ORDER_BUTTON or return to MAKE_ORDER menu') - text = self.MAKE_ORDER_TEXT - markup = self.build_make_orders_menu(self) - - - if (data[6:] in product_names and data[:6] == "chose_") or ( - data[5:] in product_names and data[:5] == 'hown_'): - if data[:5] == 'hown_': - data = 's' + data - logging.getLogger(__name__).info(f'{tg_user_id} chosen the {data}') - text = f'Выбран {data[6:]}.\nПодтвердите ваш заказ.' - print(str("next" + data)[:10]) - markup = self.build_pre_approve_order_menu(self, str('next' + data)) - - - if data[10:] in product_names and (data[:10] == "nextchose_" or data[:10] == "nextshown_"): - order = Order(str(datetime.datetime.now()), data[10:], tg_user_id, None, 'размещён') - db = Database() - db.insert_order(order) - text = f'Заказ оформлен!\nВремя заказа: {order.date[:-7]}\nПродукт:\n{order.product}\nId покупателя:\n{order.customer_id}' - markup = self.build_approve_order_menu(self, data[4:]) - - # Обработка кнопок просмотра заказов - if data == self.SHOW_ORDERS_BUTTON: - logging.getLogger(__name__).info( - f'{tg_user_id} press the SHOW_ORDERS_BUTTON or return to SHOW_ORDERS menu') - text = self.SHOW_ORDERS_TEXT - markup = self.build_show_orders_menu(self, tg_user_id) - - if data[6:] in order_dates and data[:6] == "shown_": - logging.getLogger(__name__).info(f'{tg_user_id} watch for the {data}') - db = Database() - order = db.get_order_by_date(data[6:]) - text = f''' - Заказ от: {order.date[:-7]}\n - Продукт: {order.product} - Id покупателя: {order.customer_id} - Id бармена: {order.barman_id} - Статус: {order.status} ''' - markup = self.build_show_order_info_menu(self) - - - return (text, markup) - - # Обработка нажатия кнопок - - - - + def __handle_show_products_button(self): + """ + Handle the show products button press. + """ + logging.getLogger(__name__).info( + "%s press the SHOW_PRODUCTS_BUTTON or return to SHOW_PRODUCTS menu", + self.tg_user_id, + ) + text = self.texts["show_products_text"] + markup = self.__build_show_products_menu() + return text, markup + + def __handle_make_order_button(self): + """ + Handle the make order button press. + """ + logging.getLogger(__name__).info( + "%s press the MAKE_ORDER_BUTTON or return to MAKE_ORDER menu", + self.tg_user_id, + ) + text = self.texts["make_order_text"] + markup = self.__build_show_products_menu() + return text, markup + + def __handle_show_orders_button(self): + """ + Handle the show orders button press. + """ + logging.getLogger(__name__).info( + "%s press the SHOW_ORDERS_BUTTON or return to SHOW_ORDERS menu", + self.tg_user_id, + ) + text = self.texts["show_orders_text"] + markup = self.__build_show_orders_menu() + return text, markup + + def __handle_product_info(self, data): + """ + Handle the product info button press. + """ + logging.getLogger(__name__).info( + "%s watch for the %s", self.tg_user_id, data[6:] + ) + product: Product = self.db.get_product_by_name(data[6:]) + text = ( + f"""{product.name}\n""" + f"""Описание:\n{product.description}\n""" + f"""Цена: {product.price}руб.""" + ) + markup = self.__build_show_product_info_menu(data) + return text, markup + + def __handle_pre_approve_order(self, data): + """ + Handle the pre-approve order button press. + """ + logging.getLogger(__name__).info("%s chosen the %s", self.tg_user_id, data) + text = f"Выбран {data[6:]},.\nПодтвердите ваш заказ." + markup = self.__build_pre_approve_order_menu(f"next{data}") + return text, markup + + def __handle_approve_order(self, data): + """ + Handle the approve order button press. + """ + logging.getLogger(__name__).info("%s approved the %s", self.tg_user_id, data) + order = Order( + str(datetime.datetime.now()), data[10:], self.tg_user_id, None, "размещён" + ) + self.db.insert_order(order) + text = ( + f"Заказ оформлен!\n" + f"Время заказа: {order.date[:-7]}\n" + f"Продукт:\n{order.product}\nСтатус: {order.status}" + ) + markup = self.__build_approve_order_menu(data[4:]) + return text, markup + + def __handle_order_info(self, data): + """ + Handle the order info button press. + """ + logging.getLogger(__name__).info("%s watch for the %s", self.tg_user_id, data) + order = self.db.get_order_by_date(data) + barman = self.db.get_user_by_id(order.barman_id) + text = ( + f"""Заказ от: {order.date[:-7]}\n""" + f"""Продукт: {order.product}\n""" + f"""Бармен: {barman if barman is None else barman.name}\n""" + f"""Статус: {order.status}""" + ) + markup = self.__build_show_order_info_menu() + return text, markup + + def on_button_tap(self, data) -> (str, InlineKeyboardMarkup): + """ + Handle button tap events. + """ + orders_dates = [order.date for order in self.db.get_all_orders()] + + top_menus_associations = { + self.texts["show_products_button"]: self.__handle_show_products_button, + self.texts["make_order_button"]: self.__handle_make_order_button, + self.texts["show_orders_button"]: self.__handle_show_orders_button, + self.texts["back_to_menu_button"]: self.__back_to_menu, + } + + for callback_prefix, action in [ + ("shown_", self.__handle_product_info), + ("chose_", self.__handle_pre_approve_order), + ("hown_", self.__handle_pre_approve_order), + ("nextchose_", self.__handle_approve_order), + ("nextshown_", self.__handle_approve_order), + ]: + if data.startswith(callback_prefix): + return action(data) + + if data in orders_dates: + return self.__handle_order_info(data) + + if data in top_menus_associations: + return top_menus_associations[data]() + + logging.getLogger(__name__).error("Incorrect button pressed") + return "Err0r", self.build_menu() diff --git a/bot/settings/__init__.py b/bot/settings/__init__.py new file mode 100644 index 0000000..9226e09 --- /dev/null +++ b/bot/settings/__init__.py @@ -0,0 +1,12 @@ +""" +This module initializes settings for the bot. +""" + +from bot.settings.load_texts import load_texts +from bot.settings.set_commands import set_commands + + +__all__ = [ + "load_texts", + "set_commands", +] diff --git a/bot/settings/load_texts.py b/bot/settings/load_texts.py new file mode 100644 index 0000000..00f6817 --- /dev/null +++ b/bot/settings/load_texts.py @@ -0,0 +1,20 @@ +""" +This module provides a function to load localized texts from TOML files. +""" + +import toml + + +def load_texts(locale="ru"): + """ + Load localized texts from a TOML file based on the given locale. + + Args: + locale (str): The locale code (e.g., "ru" or "en"). + + Returns: + dict: The loaded texts from the TOML file. + """ + locale = locale if locale in ["ru", "en"] else "ru" + with open(f"res/locales/{locale}.toml", encoding="utf-8") as f: + return toml.load(f) diff --git a/bot/settings/set_commands.py b/bot/settings/set_commands.py new file mode 100644 index 0000000..0323e33 --- /dev/null +++ b/bot/settings/set_commands.py @@ -0,0 +1,19 @@ +""" +This module provides a function to set bot commands based on the given locale. +""" + +from telegram import BotCommand +from bot.settings import load_texts + + +async def set_commands(bot, locale="ru"): + """ + Set bot commands based on the given locale. + """ + texts = load_texts(locale)["texts"] + commands = [ + BotCommand("start", texts["start_description"]), + BotCommand("help", texts["help_description"]), + BotCommand("menu", texts["menu_description"]), + ] + await bot.set_my_commands(commands=commands) diff --git a/poetry.lock b/poetry.lock index 825bc84..5e2a202 100644 --- a/poetry.lock +++ b/poetry.lock @@ -919,6 +919,17 @@ files = [ [package.extras] tests = ["pytest", "pytest-cov"] +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + [[package]] name = "tomlkit" version = "0.13.2" @@ -944,4 +955,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.13" -content-hash = "dce4d2b1f9df85cc3bf956f4252f5680faed629948870181c3fc61e730af27f6" +content-hash = "563788d94d4c4b4be30c1f186ca7a5e91a0b01359d0c4e5b19f9512f2c8f24ce" diff --git a/pyproject.toml b/pyproject.toml index bd26672..f0de402 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ package-mode = false [tool.poetry.dependencies] python = "^3.13" python-telegram-bot = "^21.4" +toml = "^0.10.2" [tool.poetry.group.dev.dependencies] commitizen = "^4.1.0" @@ -24,6 +25,7 @@ pytest-sugar = "^1.0.0" [tool.pytest.ini_options] addopts = "--flake8" +flake8-max-line-length = 100 [build-system] requires = ["poetry-core"] diff --git a/res/locales/ru.toml b/res/locales/ru.toml new file mode 100644 index 0000000..fd5a3d5 --- /dev/null +++ b/res/locales/ru.toml @@ -0,0 +1,22 @@ +[texts] +start = 'Привет! Я бот. Нажимай на кнопку "/help" для получения подсказок по командам.' +start_description = 'Запуск и перезапуск бота.' +help = """Список команд: +/start - запуск бота, +/help - показать список команд, +/menu - показать менюшечку.""" +help_description = 'Показывает список доступных команд.' +menu = 'Менюшечка' +menu_description = 'Показывает менюшечку.' +show_products_text = 'Барная карта Нажмите на напиток, чтобы показать подробности.' +make_order_text = 'Выбор напитка Нажмите на напиток, чтобы заказать' +show_orders_text = 'Мои заказы ' +show_products_button = 'Барная карта' +make_order_button = 'Сделать заказ' +show_orders_button = 'Мои заказы' +approve_orders_button = 'Подтвердить заказ.' +back_to_menu_button = 'Вернуться в главное меню' +back_button = 'Назад' +queue_button = "Очередь заказов" +queue_text = "Очередь" +complete_button = "Завершить заказ" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8ba90b2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import pytest +from unittest.mock import MagicMock +from telegram.ext import Application + + +@pytest.fixture +def mock_application(): + return MagicMock(spec=Application) diff --git a/tests/test_database/__init__.py b/tests/test_database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_database/test_database.py b/tests/test_database/test_database.py new file mode 100644 index 0000000..0c83913 --- /dev/null +++ b/tests/test_database/test_database.py @@ -0,0 +1,312 @@ +import pytest +import os + +from bot.database.database import Database +from bot.database.user import User +from bot.database.product import Product +from bot.database.order import Order + + +@pytest.fixture +def db_fixture(): + return Database(":memory:") + + +def test_fix_path_creates_directory_and_file(tmp_path, db_fixture): + test_path = tmp_path / "test_dir" / "test_file.db" + db_fixture.fix_path(str(test_path)) + assert os.path.exists(test_path) + assert os.path.isfile(test_path) + + +def test_insert_user(db_fixture): + user = User(id=1, name="Test User", type="customer") + db_fixture.insert_user(user) + assert db_fixture.get_user_by_id(1) == user + + +def test_insert_product(db_fixture): + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + db_fixture.insert_product(product) + retrieved_product = db_fixture.get_product_by_name("Test Product") + assert retrieved_product.name == product.name + assert retrieved_product.description == product.description + assert retrieved_product.price == product.price + + +def test_insert_order(db_fixture): + user = User(id=1, name="Test User", type="customer") + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + order = Order( + date="2023-01-01T00:00:00", + product="Test Product", + customer_id=1, + barman_id=2, + status="placed", + ) + db_fixture.insert_user(user) + db_fixture.insert_product(product) + db_fixture.insert_order(order) + assert db_fixture.get_order_by_date("2023-01-01T00:00:00") is not None + + +def test_delete_user(db_fixture): + user = User(id=1, name="Test User", type="customer") + db_fixture.insert_user(user) + db_fixture.delete_user(user) + assert db_fixture.get_user_by_id(1) is None + + +def test_delete_product(db_fixture): + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + db_fixture.insert_product(product) + db_fixture.delete_product(product) + assert db_fixture.get_product_by_name("Test Product") is None + + +def test_delete_order(db_fixture): + user = User(id=1, name="Test User", type="customer") + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + order = Order( + date="2023-01-02T00:00:00", + product="Test Product", + customer_id=1, + barman_id=2, + status="placed", + ) + db_fixture.insert_user(user) + db_fixture.insert_product(product) + db_fixture.insert_order(order) + db_fixture.delete_order(order) + assert db_fixture.get_order_by_date("2023-01-02T00:00:00") is None + + +def test_update_order(db_fixture): + user = User(id=1, name="Test User", type="customer") + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + order = Order( + date="2023-01-03T00:00:00", + product="Test Product", + customer_id=1, + barman_id=2, + status="placed", + ) + db_fixture.insert_user(user) + db_fixture.insert_product(product) + db_fixture.insert_order(order) + order.status = "completed" + db_fixture.update_order(order) + updated_order = db_fixture.get_order_by_date("2023-01-03T00:00:00") + assert updated_order.status == "completed" + + +def test_insert_user_with_invalid_data(db_fixture): + with pytest.raises(ValueError): + db_fixture.insert_user(None) + + +def test_insert_product_with_invalid_data(db_fixture): + with pytest.raises(ValueError): + db_fixture.insert_product(None) + + +def test_insert_order_with_invalid_data(db_fixture): + with pytest.raises(ValueError): + db_fixture.insert_order(None) + + +def test_update_user_with_invalid_data(db_fixture): + with pytest.raises(ValueError): + db_fixture.update_user(None) + + +def test_update_product_with_invalid_data(db_fixture): + with pytest.raises(ValueError): + db_fixture.update_product(None) + + +def test_update_order_with_invalid_data(db_fixture): + with pytest.raises(ValueError): + db_fixture.update_order(None) + + +def test_delete_user_with_invalid_data(db_fixture): + with pytest.raises(ValueError): + db_fixture.delete_user(None) + + +def test_delete_product_with_invalid_data(db_fixture): + with pytest.raises(ValueError): + db_fixture.delete_product(None) + + +def test_delete_order_with_invalid_data(db_fixture): + with pytest.raises(ValueError): + db_fixture.delete_order(None) + + +def test_get_all_products(db_fixture): + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + db_fixture.insert_product(product) + products = db_fixture.get_all_products() + assert len(products) == 1 + assert products[0].name == "Test Product" + + +def test_get_all_users(db_fixture): + user = User(id=1, name="Test User", type="customer") + db_fixture.insert_user(user) + users = db_fixture.get_all_users() + assert len(users) == 1 + assert users[0].name == "Test User" + + +def test_get_all_orders(db_fixture): + user = User(id=1, name="Test User", type="customer") + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + order = Order( + date="2023-01-01T00:00:00", + product="Test Product", + customer_id=1, + barman_id=2, + status="placed", + ) + db_fixture.insert_user(user) + db_fixture.insert_product(product) + db_fixture.insert_order(order) + orders = db_fixture.get_all_orders() + assert len(orders) == 1 + assert orders[0].date == "2023-01-01T00:00:00" + + +def test_get_user_by_id(db_fixture): + user = User(id=1, name="Test User", type="customer") + db_fixture.insert_user(user) + retrieved_user = db_fixture.get_user_by_id(1) + assert retrieved_user is not None + assert retrieved_user.name == "Test User" + + +def test_get_product_by_name(db_fixture): + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + db_fixture.insert_product(product) + retrieved_product = db_fixture.get_product_by_name("Test Product") + assert retrieved_product is not None + assert retrieved_product.name == "Test Product" + + +def test_get_order_by_date(db_fixture): + user = User(id=1, name="Test User", type="customer") + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + order = Order( + date="2023-01-01T00:00:00", + product="Test Product", + customer_id=1, + barman_id=2, + status="placed", + ) + db_fixture.insert_user(user) + db_fixture.insert_product(product) + db_fixture.insert_order(order) + retrieved_order = db_fixture.get_order_by_date("2023-01-01T00:00:00") + assert retrieved_order is not None + assert retrieved_order.date == "2023-01-01T00:00:00" + + +def test_get_orders_by_customer_id(db_fixture): + user = User(id=1, name="Test User", type="customer") + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + order = Order( + date="2023-01-01T00:00:00", + product="Test Product", + customer_id=1, + barman_id=2, + status="placed", + ) + db_fixture.insert_user(user) + db_fixture.insert_product(product) + db_fixture.insert_order(order) + orders = db_fixture.get_orders_by_customer_id(1) + assert len(orders) == 1 + assert orders[0].customer_id == 1 + + +def test_get_orders_queue(db_fixture): + user = User(id=1, name="Test User", type="customer") + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + order = Order( + date="2023-01-01T00:00:00", + product="Test Product", + customer_id=1, + barman_id=2, + status="размещён", + ) + db_fixture.insert_user(user) + db_fixture.insert_product(product) + db_fixture.insert_order(order) + orders = db_fixture.get_orders_queue() + assert len(orders) == 1 + assert orders[0].status == "размещён" + + +def test_update_user_with_valid_data(db_fixture): + user = User(id=1, name="Test User", type="customer") + db_fixture.insert_user(user) + user.name = "Updated User" + db_fixture.update_user(user) + updated_user = db_fixture.get_user_by_id(1) + assert updated_user.name == "Updated User" + + +def test_update_product_with_valid_data(db_fixture): + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + db_fixture.insert_product(product) + product.description = "Updated description" + db_fixture.update_product(product) + updated_product = db_fixture.get_product_by_name("Test Product") + assert updated_product.description == "Updated description" + + +def test_update_order_with_valid_data(db_fixture): + user = User(id=1, name="Test User", type="customer") + product = Product( + name="Test Product", description="A product for testing", price=100 + ) + order = Order( + date="2023-01-01T00:00:00", + product="Test Product", + customer_id=1, + barman_id=2, + status="placed", + ) + db_fixture.insert_user(user) + db_fixture.insert_product(product) + db_fixture.insert_order(order) + order.status = "completed" + db_fixture.update_order(order) + updated_order = db_fixture.get_order_by_date("2023-01-01T00:00:00") + assert updated_order.status == "completed" diff --git a/tests/test_database/test_order.py b/tests/test_database/test_order.py new file mode 100644 index 0000000..ce5e9c6 --- /dev/null +++ b/tests/test_database/test_order.py @@ -0,0 +1,58 @@ +import pytest +from bot.database.order import Order + + +@pytest.fixture +def order(): + return Order( + date="2023-01-01", + product="Coffee", + customer_id=1, + barman_id=2, + status="pending", + ) + + +def test_get_order_date(order): + assert order.get_order_date() == "2023-01-01" + + +def test_get_order_product(order): + assert order.get_order_product() == "Coffee" + + +def test_get_order_customer_id(order): + assert order.get_order_customer_id() == 1 + + +def test_get_order_barman_id(order): + assert order.get_order_barman_id() == 2 + + +def test_get_order_status(order): + assert order.get_order_status() == "pending" + + +def test_set_order_date(order): + order.set_order_date("2023-02-01") + assert order.get_order_date() == "2023-02-01" + + +def test_set_order_product(order): + order.set_order_product("Tea") + assert order.get_order_product() == "Tea" + + +def test_set_order_customer_id(order): + order.set_order_customer_id(3) + assert order.get_order_customer_id() == 3 + + +def test_set_order_barman_id(order): + order.set_order_barman_id(4) + assert order.get_order_barman_id() == 4 + + +def test_set_order_status(order): + order.set_order_status("completed") + assert order.get_order_status() == "completed" diff --git a/tests/test_database/test_product.py b/tests/test_database/test_product.py new file mode 100644 index 0000000..e4867c5 --- /dev/null +++ b/tests/test_database/test_product.py @@ -0,0 +1,30 @@ +import pytest +from bot.database.product import Product + + +@pytest.fixture +def product(): + return Product( + name="Test Product", description="A product for testing", price=100.0 + ) + + +def test_create_product(product): + assert product.get_name() == "Test Product" + assert product.get_description() == "A product for testing" + assert product.get_price() == 100.0 + + +def test_update_product_name(product): + product.set_name("Updated Product") + assert product.get_name() == "Updated Product" + + +def test_update_product_description(product): + product.set_description("An updated product description") + assert product.get_description() == "An updated product description" + + +def test_update_product_price(product): + product.set_price(150.0) + assert product.get_price() == 150.0 diff --git a/tests/test_database/test_user.py b/tests/test_database/test_user.py new file mode 100644 index 0000000..bbbb1e6 --- /dev/null +++ b/tests/test_database/test_user.py @@ -0,0 +1,38 @@ +import pytest +from bot.database.user import User + + +@pytest.fixture +def user(): + return User(id=1, name="Test User", type="customer") + + +def test_create_user(user): + assert user.get_user_id() == 1 + assert user.get_user_name() == "Test User" + assert user.get_user_type() == "customer" + + +def test_update_user_name(user): + user.set_user_name("Updated User") + assert user.get_user_name() == "Updated User" + + +def test_update_user_id(user): + user.set_user_id(2) + assert user.get_user_id() == 2 + + +def test_update_user_type(user): + user.set_user_type("admin") + assert user.get_user_type() == "admin" + + +def test_invalid_user_type(): + with pytest.raises(ValueError): + User(id=1, name="Invalid User", type="invalid") + + +def test_set_invalid_user_type(user): + with pytest.raises(ValueError): + user.set_user_type("invalid") diff --git a/tests/test_handlers/__init__.py b/tests/test_handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_handlers/test_callback_handler.py b/tests/test_handlers/test_callback_handler.py new file mode 100644 index 0000000..7a2a793 --- /dev/null +++ b/tests/test_handlers/test_callback_handler.py @@ -0,0 +1,37 @@ +# # tests/test_callback_handler.py +# +# import pytest +# from unittest.mock import AsyncMock, MagicMock +# from telegram import Update, User, CallbackQuery +# from bot.handlers.callback_handler import callback_handler +# from bot.database import Database, User as UserDB +# from bot.roles import role_associations +# from bot.settings import load_texts +# +# +# @pytest.mark.asyncio +# async def test_callback_handler(): +# db = MagicMock(Database) +# role_class = MagicMock() +# role_obj = MagicMock() +# role_associations["customer"] = role_class +# role_class.return_value = role_obj +# +# tg_user = User(id=123, first_name="Test", is_bot=False) +# callback_query = MagicMock(CallbackQuery) +# callback_query.data = "test_data" +# callback_query.answer = AsyncMock() +# callback_query.edit_message_text = AsyncMock() +# update = MagicMock(Update) +# update.effective_user = tg_user +# update.callback_query = callback_query +# db.insert_user(MagicMock(UserDB(123, "Test", "customer"))) +# db.get_user_by_id.return_value = MagicMock(UserDB(123, "Test", "customer")) +# load_texts.return_value = {"texts": {}} +# +# await callback_handler(update, MagicMock()) +# +# role_class.assert_called_once_with(db, tg_user.id, {}) +# role_obj.on_button_tap.assert_called_once_with("test_data") +# callback_query.answer.assert_called_once() +# callback_query.edit_message_text.assert_called_once() diff --git a/tests/test_handlers/test_help_handler.py b/tests/test_handlers/test_help_handler.py new file mode 100644 index 0000000..612912b --- /dev/null +++ b/tests/test_handlers/test_help_handler.py @@ -0,0 +1,23 @@ +import pytest +from unittest.mock import MagicMock +from telegram import User as TelegramUser, Update, Message +from bot.handlers.help_handler import help_handler +from bot.settings import load_texts + + +@pytest.mark.asyncio +async def test_help_handler(): + telegram_user = TelegramUser(id=1, first_name="Test", is_bot=False, language_code="ru") + + message = MagicMock(spec=Message) + message.from_user = telegram_user + message.text = "/help" + update = MagicMock(spec=Update) + update.effective_user = telegram_user + update.message = message + + context = MagicMock() + + await help_handler(update, context) + texts = load_texts(telegram_user.language_code)["texts"] + message.reply_text.assert_called_once_with(texts["help"]) diff --git a/tests/test_handlers/test_menu_handler.py b/tests/test_handlers/test_menu_handler.py new file mode 100644 index 0000000..7ddb692 --- /dev/null +++ b/tests/test_handlers/test_menu_handler.py @@ -0,0 +1,72 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock, patch +from telegram import User as TelegramUser, Update, Message +from telegram.constants import ParseMode + +from bot.database import Database +from bot.handlers.menu_handler import menu_handler +from bot.settings import load_texts + + +@pytest.mark.asyncio +async def test_menu_handler_customer(): + telegram_user = TelegramUser( + id=1, first_name="Test", is_bot=False, language_code="ru" + ) + message = MagicMock(spec=Message) + message.from_user = telegram_user + message.text = "/menu" + update = MagicMock(spec=Update) + update.effective_user = telegram_user + update.message = message + + context = MagicMock() + context.bot.send_message = AsyncMock() + + database = MagicMock(spec=Database) + user = MagicMock() + user.type = "customer" + + with patch("bot.handlers.menu_handler.Database", return_value=database): + with patch.object(database, "get_user_by_id", return_value=user): + with patch("bot.roles.Customer.build_menu", return_value="customer_menu"): + await menu_handler(update, context) + + context.bot.send_message.assert_called_once_with( + telegram_user.id, + load_texts(telegram_user.language_code)["texts"]["menu"], + parse_mode=ParseMode.HTML, + reply_markup="customer_menu", + ) + + +@pytest.mark.asyncio +async def test_menu_handler_barman(): + telegram_user = TelegramUser( + id=1, first_name="Test", is_bot=False, language_code="ru" + ) + message = MagicMock(spec=Message) + message.from_user = telegram_user + message.text = "/menu" + update = MagicMock(spec=Update) + update.effective_user = telegram_user + update.message = message + + context = MagicMock() + context.bot.send_message = AsyncMock() + + database = MagicMock(spec=Database()) + user = MagicMock() + user.type = "barman" + + with patch("bot.handlers.menu_handler.Database", return_value=database): + with patch.object(database, "get_user_by_id", return_value=user): + with patch("bot.roles.Barman.build_menu", return_value="barman_menu"): + await menu_handler(update, context) + + context.bot.send_message.assert_called_once_with( + telegram_user.id, + load_texts(telegram_user.language_code)["texts"]["menu"], + parse_mode=ParseMode.HTML, + reply_markup="barman_menu", + ) diff --git a/tests/test_handlers/test_start_handler.py b/tests/test_handlers/test_start_handler.py new file mode 100644 index 0000000..18895fb --- /dev/null +++ b/tests/test_handlers/test_start_handler.py @@ -0,0 +1,34 @@ +import pytest +from unittest.mock import MagicMock, AsyncMock, patch +from telegram import User as TelegramUser, Update, Message +from bot.handlers.start_handler import start_handler +from bot.database import Database, User + + +@pytest.mark.asyncio +async def test_start_handler(): + + telegram_user = TelegramUser(id=1, first_name="Test", is_bot=False) + + message = MagicMock(spec=Message) + message.from_user = telegram_user + message.text = "/start" + update = MagicMock(spec=Update) + update.effective_user = telegram_user + update.message = message + + context = MagicMock() + context.bot = AsyncMock() + + database = MagicMock(spec=Database) + user = User(id=1, name="Test", type="customer") + user.language_code = "ru" + + with patch("bot.handlers.start_handler.Database", return_value=database): + with patch("bot.handlers.start_handler.User", return_value=user): + await start_handler(update, context) + + database.insert_user.assert_called_once_with(user) + message.reply_text.assert_called_once_with( + 'Привет! Я бот. Нажимай на кнопку "/help" для получения подсказок по командам.' + ) diff --git a/tests/test_roles/test_barman.py b/tests/test_roles/test_barman.py new file mode 100644 index 0000000..acca971 --- /dev/null +++ b/tests/test_roles/test_barman.py @@ -0,0 +1,89 @@ +import pytest +from unittest.mock import MagicMock +from telegram import InlineKeyboardMarkup +from bot.roles.barman import Barman + + +@pytest.fixture +def barman(): + db = MagicMock() + tg_user_id = 12345 + texts = { + "queue_button": "Queue", + "queue_text": "Queue Text", + "complete_button": "Complete", + "back_to_menu_button": "Back to Menu", + "back_button": "Back", + "menu": "Menu", + "show_products_button": "Show Products", + "make_order_button": "Make Order", + "show_orders_button": "Show Orders", + } + return Barman(db, tg_user_id, texts) + + +def test_build_menu(barman): + menu = barman.build_menu() + assert isinstance(menu, InlineKeyboardMarkup) + + +def test_handle_queue_button(barman): + barman.db.get_orders_queue.return_value = [] + text, markup = barman._Barman__handle_queue_button() + assert text == "Queue Text" + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_handle_pre_complete_order(barman): + order = MagicMock() + order.date = "2023-10-10 10:10:10" + order.product = "Product1" + order.customer_id = 12345 + order.barman_id = 54321 + order.status = "размещён" + barman.db.get_order_by_date.return_value = order + + text, markup = barman._Barman__handle_pre_complete_order( + "complete_2023-10-10 10:10:10" + ) + assert "Product1" in text + # assert "2023-10-10 10:10" in text + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_handle_complete_order(barman): + order = MagicMock() + order.date = "2023-10-10 10:10:10" + order.product = "Product1" + order.customer_id = 12345 + order.barman_id = 54321 + order.status = "размещён" + barman.db.get_order_by_date.return_value = order + + text, markup = barman._Barman__handle_complete_order( + "ccomplete_2023-10-10 10:10:10" + ) + assert "Заказ завершён!!!" in text + assert "Product1" in text + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_on_button_tap_queue(barman): + barman.db.get_orders_queue.return_value = [] + text, markup = barman.on_button_tap("Queue") + assert text == "Queue Text" + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_on_button_tap_complete(barman): + order = MagicMock() + order.date = "2023-10-10 10:10:10" + order.product = "Product1" + order.customer_id = 12345 + order.barman_id = 54321 + order.status = "размещён" + barman.db.get_order_by_date.return_value = order + + text, markup = barman.on_button_tap("complete_2023-10-10 10:10:10") + assert "Product1" in text + assert isinstance(markup, InlineKeyboardMarkup) diff --git a/tests/test_roles/test_customer.py b/tests/test_roles/test_customer.py new file mode 100644 index 0000000..d4155bd --- /dev/null +++ b/tests/test_roles/test_customer.py @@ -0,0 +1,117 @@ +import pytest +from unittest.mock import MagicMock +from telegram import InlineKeyboardMarkup +from bot.roles.customer import Customer + + +@pytest.fixture +def customer(): + db = MagicMock() + tg_user_id = 12345 + texts = { + "menu": "Menu", + "show_products_text": "Show Products", + "make_order_text": "Make Order", + "show_orders_text": "Show Orders", + "show_orders_button": "Show Orders", + "show_products_button": "Show Products", + "make_order_button": "Make Order", + "approve_orders_button": "Approve Order", + "back_to_menu_button": "Back to Menu", + "back_button": "Back", + } + return Customer(db, tg_user_id, texts) + + +def test_build_menu(customer): + menu = customer.build_menu() + assert isinstance(menu, InlineKeyboardMarkup) + + +def test_handle_show_products_button(customer): + text, markup = customer._Customer__handle_show_products_button() + assert text == "Show Products" + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_handle_make_order_button(customer): + text, markup = customer._Customer__handle_make_order_button() + assert text == "Make Order" + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_handle_show_orders_button(customer): + text, markup = customer._Customer__handle_show_orders_button() + assert text == "Show Orders" + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_handle_product_info(customer): + product = MagicMock() + product.name = "Product1" + product.description = "Description1" + product.price = 100 + customer.db.get_product_by_name.return_value = product + + text, markup = customer._Customer__handle_product_info("shown_Product1") + assert "Product1" in text + assert "Description1" in text + assert "100руб." in text + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_handle_pre_approve_order(customer): + text, markup = customer._Customer__handle_pre_approve_order("chose_Product1") + assert "Product1" in text + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_handle_approve_order(customer): + order = MagicMock() + order.date = "2023-10-10 10:10:10" + order.product = "Product1" + order.customer_id = customer.tg_user_id + order.status = "размещён" + customer.db.get_order_by_date.return_value = order + + text, markup = customer._Customer__handle_approve_order("nextchose_Product1") + assert "Заказ оформлен!" in text + assert "Product1" in text + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_handle_order_info(customer): + order = MagicMock() + order.date = "2023-10-10 10:10:10" + order.product = "Product1" + order.barman_id = 54321 + order.status = "размещён" + barman = MagicMock() + barman.name = "Barman1" + customer.db.get_order_by_date.return_value = order + customer.db.get_user_by_id.return_value = barman + + text, markup = customer._Customer__handle_order_info("2023-10-10 10:10:10") + assert "Product1" in text + assert "Barman1" in text + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_back_to_menu(customer): + text, markup = customer._Customer__back_to_menu() + assert text == "Menu" + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_on_button_tap_top_menu(customer): + customer.db.get_all_orders.return_value = [] + text, markup = customer.on_button_tap("Show Products") + assert text == "Show Products" + assert isinstance(markup, InlineKeyboardMarkup) + + +def test_on_button_tap_invalid(customer): + customer.db.get_all_orders.return_value = [] + text, markup = customer.on_button_tap("Invalid Button") + assert text == "Err0r" + assert isinstance(markup, InlineKeyboardMarkup)