commit ff50ea67845488f41d3b887a5c73e56ea6967370 Author: Daniel Haus Date: Wed Feb 11 14:57:06 2026 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/config.py b/config.py new file mode 100644 index 0000000..11c2488 --- /dev/null +++ b/config.py @@ -0,0 +1,8 @@ +DB_HOST = "localhost" +DB_PORT = 5432 +DB_NAME = "toy_store_a" +DB_USER = "postgres" +DB_PASSWORD = "1234" + +DISCOUNT_COLOR = "#dbe68a" +OUT_OF_STOCK_COLOR = "#565750" diff --git a/controllers/auth_controller.py b/controllers/auth_controller.py new file mode 100644 index 0000000..09bf310 --- /dev/null +++ b/controllers/auth_controller.py @@ -0,0 +1,48 @@ +from models.user_model import UserModel +from views.login_view import LoginView +from controllers.catalog_controller import CatalogController + + +class AuthController: + def __init__(self, database): + self.database = database + self.model = UserModel(database) + self.view = LoginView() + + self.view.login_button.clicked.connect( + self.login + ) + self.view.guest_button.clicked.connect( + self.login_as_guest + ) + + def show(self): + self.view.show() + + def login(self): + login = self.view.login_input.text() + password = self.view.password_input.text() + user = self.model.authenticate(login, password) + + if user: + self.open_catalog( + user["full_name"], + user["role_name"], + ) + else: + self.view.status_label.setText( + "Неверный логин или пароль" + ) + + def login_as_guest(self): + self.open_catalog("Гость", "Гость") + + def open_catalog(self, full_name: str, role: str): + self.catalog = CatalogController( + self.database, + full_name, + role, + self, + ) + self.catalog.show() + self.view.close() diff --git a/controllers/catalog_controller.py b/controllers/catalog_controller.py new file mode 100644 index 0000000..ba32ed4 --- /dev/null +++ b/controllers/catalog_controller.py @@ -0,0 +1,38 @@ +from models.toy_model import ToyModel +from views.catalog_view import CatalogView + + +class CatalogController: + def __init__(self, database, full_name, role, auth): + self.database = database + self.model = ToyModel(database) + self.view = CatalogView(full_name, role) + self.auth = auth + self.role = role + + self.view.refresh_button.clicked.connect( + self.load_data + ) + self.view.sort_button.clicked.connect( + self.sort_by_price + ) + self.view.logout_button.clicked.connect( + self.logout + ) + + self.load_data() + + def show(self): + self.view.show() + + def load_data(self): + toys = self.model.get_all() + self.view.load_data(toys) + + def sort_by_price(self): + toys = self.model.sort_by_price() + self.view.load_data(toys) + + def logout(self): + self.view.close() + self.auth.show() diff --git a/db.sql b/db.sql new file mode 100644 index 0000000..397beba --- /dev/null +++ b/db.sql @@ -0,0 +1,138 @@ +CREATE TABLE roles ( + role_id SERIAL PRIMARY KEY, + role_name VARCHAR(50) NOT NULL +); + +CREATE TABLE users ( + user_id SERIAL PRIMARY KEY, + login VARCHAR(50) UNIQUE NOT NULL, + password VARCHAR(100) NOT NULL, + full_name VARCHAR(150) NOT NULL, + role_id INTEGER REFERENCES roles(role_id) +); + +CREATE TABLE categories ( + category_id SERIAL PRIMARY KEY, + category_name VARCHAR(100) NOT NULL +); + +CREATE TABLE manufacturers ( + manufacturer_id SERIAL PRIMARY KEY, + manufacturer_name VARCHAR(100) NOT NULL +); + +CREATE TABLE suppliers ( + supplier_id SERIAL PRIMARY KEY, + supplier_name VARCHAR(100) NOT NULL +); + +CREATE TABLE age_groups ( + age_group_id SERIAL PRIMARY KEY, + age_label VARCHAR(20) NOT NULL +); + +CREATE TABLE toys ( + toy_id SERIAL PRIMARY KEY, + toy_name VARCHAR(150) NOT NULL, + category_id INTEGER REFERENCES categories(category_id), + manufacturer_id INTEGER REFERENCES manufacturers(manufacturer_id), + price NUMERIC(10,2) NOT NULL, + discount INTEGER DEFAULT 0, + image VARCHAR(255) +); + +CREATE TABLE toy_age_groups ( + toy_id INTEGER REFERENCES toys(toy_id), + age_group_id INTEGER REFERENCES age_groups(age_group_id), + PRIMARY KEY (toy_id, age_group_id) +); + +CREATE TABLE toy_suppliers ( + toy_id INTEGER REFERENCES toys(toy_id), + supplier_id INTEGER REFERENCES suppliers(supplier_id), + PRIMARY KEY (toy_id, supplier_id) +); + +CREATE TABLE stock ( + toy_id INTEGER PRIMARY KEY REFERENCES toys(toy_id), + quantity INTEGER NOT NULL +); + +INSERT INTO roles (role_name) VALUES +('Гость'), +('Покупатель'), +('Сотрудник'), +('Администратор'); + +INSERT INTO users (login, password, full_name, role_id) VALUES +('guest','guest','Гость',1), +('anna','111','Иванова Анна Сергеевна',2), +('oleg','222','Кузнецов Олег Петрович',2), +('manager','333','Смирнова Мария Андреевна',3), +('admin','admin','Сидоров Максим Игоревич',4); + +INSERT INTO categories (category_name) VALUES +('Мягкие игрушки'), +('Конструкторы'), +('Развивающие игрушки'), +('Настольные игры'), +('Роботы'); + +INSERT INTO manufacturers (manufacturer_name) VALUES +('Lego'), +('Hasbro'), +('Mattel'), +('PlaySmart'), +('Fisher Price'); + +INSERT INTO suppliers (supplier_name) VALUES +('ООО Радуга'), +('ИП Смайл'),('ToyImport'), +('KidsWorld'); + +INSERT INTO age_groups (age_label) VALUES +('0-1'), +('1-3'), +('3-5'), +('5-7'), +('7+'); + +INSERT INTO toys (toy_name, category_id, manufacturer_id, price, discount, image) VALUES +('Плюшевый мишка',1,4,1500,10,'bear.png'), +('Конструктор City',2,1,4200,25,'city.png'), +('Развивающий куб',3,5,2300,0,NULL), +('Робот трансформер',5,2,5200,30,'robot.png'), +('Настольная игра Лото',4,3,1800,5,NULL), +('Кукла классическая',1,3,2700,15,'doll.png'), +('Конструктор Junior',2,1,3100,0,NULL), +('Музыкальный телефон',3,5,2100,20,NULL), +('Робот на пульте',5,2,6400,35,'rc.png'), +('Игра Мемори',4,3,1600,0,NULL); + +INSERT INTO toy_age_groups (toy_id, age_group_id) VALUES +(1,2),(1,3), +(2,4),(2,5), +(3,2), +(4,4),(4,5), +(5,3),(5,4), +(6,3),(6,4), +(7,2),(7,3), +(8,2), +(9,5), +(10,3),(10,4); + +INSERT INTO toy_suppliers (toy_id, supplier_id) VALUES +(1,1),(1,2), +(2,3), +(3,1),(3,4), +(4,2),(4,3), +(5,1), +(6,4), +(7,3), +(8,2),(8,4), +(9,3), +(10,1),(10,2); + +INSERT INTO stock (toy_id, quantity) VALUES +(1,12),(2,5),(3,0),(4,7),(5,10), +(6,4),(7,8),(8,0),(9,3),(10,15); diff --git a/main.py b/main.py new file mode 100644 index 0000000..b1466ed --- /dev/null +++ b/main.py @@ -0,0 +1,16 @@ +import sys +from PyQt6.QtWidgets import QApplication +from models.database import Database +from controllers.auth_controller import AuthController + + +def main(): + app = QApplication(sys.argv) + database = Database() + controller = AuthController(database) + controller.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/database.py b/models/database.py new file mode 100644 index 0000000..8ec52a6 --- /dev/null +++ b/models/database.py @@ -0,0 +1,36 @@ +import psycopg2 +from psycopg2.extras import RealDictCursor +from config import ( + DB_HOST, + DB_PORT, + DB_NAME, + DB_USER, + DB_PASSWORD, +) + + +class Database: + def __init__(self): + self.connection = psycopg2.connect( + host=DB_HOST, + port=DB_PORT, + dbname=DB_NAME, + user=DB_USER, + password=DB_PASSWORD, + cursor_factory=RealDictCursor, + ) + + def fetch_all(self, query: str, params: tuple = ()): + with self.connection.cursor() as cursor: + cursor.execute(query, params) + return cursor.fetchall() + + def fetch_one(self, query: str, params: tuple = ()): + with self.connection.cursor() as cursor: + cursor.execute(query, params) + return cursor.fetchone() + + def execute(self, query: str, params: tuple = ()): + with self.connection.cursor() as cursor: + cursor.execute(query, params) + self.connection.commit() diff --git a/models/toy_model.py b/models/toy_model.py new file mode 100644 index 0000000..079744d --- /dev/null +++ b/models/toy_model.py @@ -0,0 +1,87 @@ +from models.database import Database + + +class ToyModel: + def __init__(self, database: Database): + self.database = database + + def get_all(self): + query = """ + SELECT t.toy_id, + t.toy_name, + c.category_name, + m.manufacturer_name, + t.price, + t.discount, + COALESCE(s.quantity, 0) AS quantity, + STRING_AGG(DISTINCT a.age_label, ', ') AS ages, + STRING_AGG(DISTINCT sup.supplier_name, ', ') AS suppliers + FROM toys t + JOIN categories c ON t.category_id = c.category_id + JOIN manufacturers m ON t.manufacturer_id = m.manufacturer_id + LEFT JOIN stock s ON t.toy_id = s.toy_id + LEFT JOIN toy_age_groups ta ON t.toy_id = ta.toy_id + LEFT JOIN age_groups a ON ta.age_group_id = a.age_group_id + LEFT JOIN toy_suppliers ts ON t.toy_id = ts.toy_id + LEFT JOIN suppliers sup ON ts.supplier_id = sup.supplier_id + GROUP BY t.toy_id, c.category_name, + m.manufacturer_name, s.quantity + ORDER BY t.toy_name + """ + return self.database.fetch_all(query) + + def search_by_age(self, age: str): + query = """ + SELECT * + FROM ( + SELECT t.toy_id, + t.toy_name, + c.category_name, + m.manufacturer_name, + t.price, + t.discount, + COALESCE(s.quantity, 0) AS quantity, + STRING_AGG(DISTINCT a.age_label, ', ') AS ages, + STRING_AGG(DISTINCT sup.supplier_name, ', ') AS suppliers + FROM toys t + JOIN categories c ON t.category_id = c.category_id + JOIN manufacturers m ON t.manufacturer_id = m.manufacturer_id + LEFT JOIN stock s ON t.toy_id = s.toy_id + LEFT JOIN toy_age_groups ta ON t.toy_id = ta.toy_id + LEFT JOIN age_groups a ON ta.age_group_id = a.age_group_id + LEFT JOIN toy_suppliers ts ON t.toy_id = ts.toy_id + LEFT JOIN suppliers sup ON ts.supplier_id = sup.supplier_id + GROUP BY t.toy_id, c.category_name, + m.manufacturer_name, s.quantity + ) sub + WHERE ages ILIKE %s + """ + return self.database.fetch_all(query, (f"%{age}%",)) + + def sort_by_price(self): + query = """ + SELECT * + FROM ( + SELECT t.toy_id, + t.toy_name, + c.category_name, + m.manufacturer_name, + t.price, + t.discount, + COALESCE(s.quantity, 0) AS quantity, + STRING_AGG(DISTINCT a.age_label, ', ') AS ages, + STRING_AGG(DISTINCT sup.supplier_name, ', ') AS suppliers + FROM toys t + JOIN categories c ON t.category_id = c.category_id + JOIN manufacturers m ON t.manufacturer_id = m.manufacturer_id + LEFT JOIN stock s ON t.toy_id = s.toy_id + LEFT JOIN toy_age_groups ta ON t.toy_id = ta.toy_id + LEFT JOIN age_groups a ON ta.age_group_id = a.age_group_id + LEFT JOIN toy_suppliers ts ON t.toy_id = ts.toy_id + LEFT JOIN suppliers sup ON ts.supplier_id = sup.supplier_id + GROUP BY t.toy_id, c.category_name, + m.manufacturer_name, s.quantity + ) sub + ORDER BY price + """ + return self.database.fetch_all(query) diff --git a/models/user_model.py b/models/user_model.py new file mode 100644 index 0000000..3d7af3e --- /dev/null +++ b/models/user_model.py @@ -0,0 +1,17 @@ +from models.database import Database + + +class UserModel: + def __init__(self, database: Database): + self.database = database + + def authenticate(self, login: str, password: str): + query = """ + SELECT u.user_id, + u.full_name, + r.role_name + FROM users u + JOIN roles r ON u.role_id = r.role_id + WHERE u.login = %s AND u.password = %s + """ + return self.database.fetch_one(query, (login, password)) diff --git a/views/catalog_view.py b/views/catalog_view.py new file mode 100644 index 0000000..0d317ee --- /dev/null +++ b/views/catalog_view.py @@ -0,0 +1,82 @@ +from PyQt6.QtWidgets import ( + QWidget, + QVBoxLayout, + QPushButton, + QLabel, + QTableWidget, + QTableWidgetItem, +) +from PyQt6.QtGui import QColor +from config import DISCOUNT_COLOR, OUT_OF_STOCK_COLOR + + +class CatalogView(QWidget): + def __init__(self, full_name: str, role: str): + super().__init__() + self.setWindowTitle("Каталог игрушек") + + self.layout = QVBoxLayout() + + self.user_label = QLabel( + f"{full_name} ({role})" + ) + + self.refresh_button = QPushButton("Обновить") + self.sort_button = QPushButton("Сортировать по цене") + self.logout_button = QPushButton("Выйти") + + self.table = QTableWidget() + + self.layout.addWidget(self.user_label) + self.layout.addWidget(self.refresh_button) + self.layout.addWidget(self.sort_button) + self.layout.addWidget(self.table) + self.layout.addWidget(self.logout_button) + + self.setLayout(self.layout) + + self.setFixedSize(860, 500) + + def load_data(self, toys: list): + headers = [ + "Название", + "Категория", + "Производитель", + "Возраст", + "Поставщик", + "Цена", + "Скидка", + "Остаток", + ] + + self.table.setColumnCount(len(headers)) + self.table.setHorizontalHeaderLabels(headers) + self.table.setRowCount(len(toys)) + + for row, toy in enumerate(toys): + values = [ + toy["toy_name"], + toy["category_name"], + toy["manufacturer_name"], + toy["ages"], + toy["suppliers"], + str(toy["price"]), + str(toy["discount"]), + str(toy["quantity"]), + ] + + for col, value in enumerate(values): + item = QTableWidgetItem(value) + self.table.setItem(row, col, item) + + if toy["discount"] >= 25: + for col in range(len(headers)): + self.table.item(row, col).setBackground( + QColor(DISCOUNT_COLOR) + ) + + if toy["quantity"] == 0: + for col in range(len(headers)): + self.table.item(row, col).setBackground( + QColor(OUT_OF_STOCK_COLOR) + ) diff --git a/views/login_view.py b/views/login_view.py new file mode 100644 index 0000000..6ea3d24 --- /dev/null +++ b/views/login_view.py @@ -0,0 +1,38 @@ +from PyQt6.QtWidgets import ( + QWidget, + QVBoxLayout, + QLineEdit, + QPushButton, + QLabel, +) + + +class LoginView(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("Авторизация") + + self.layout = QVBoxLayout() + + self.login_input = QLineEdit() + self.login_input.setPlaceholderText("Логин") + + self.password_input = QLineEdit() + self.password_input.setPlaceholderText("Пароль") + self.password_input.setEchoMode( + QLineEdit.EchoMode.Password + ) + + self.login_button = QPushButton("Войти") + self.guest_button = QPushButton("Войти как гость") + self.status_label = QLabel() + + self.layout.addWidget(self.login_input) + self.layout.addWidget(self.password_input) + self.layout.addWidget(self.login_button) + self.layout.addWidget(self.guest_button) + self.layout.addWidget(self.status_label) + + self.setLayout(self.layout) + + self.setFixedSize(240, 180)