Initial commit

This commit is contained in:
Daniel Haus 2026-02-11 14:57:06 +03:00
commit ff50ea6784
12 changed files with 509 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__pycache__

8
config.py Normal file
View file

@ -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"

View file

@ -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()

View file

@ -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()

138
db.sql Normal file
View file

@ -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);

16
main.py Normal file
View file

@ -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()

0
models/__init__.py Normal file
View file

36
models/database.py Normal file
View file

@ -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()

87
models/toy_model.py Normal file
View file

@ -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)

17
models/user_model.py Normal file
View file

@ -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))

82
views/catalog_view.py Normal file
View file

@ -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)
)

38
views/login_view.py Normal file
View file

@ -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)