1
0
Fork 0
mirror of https://github.com/EDeev/mobiles_dataset.git synced 2026-10-08 04:59:41 +03:00

Compare commits

..

No commits in common. "7277093a13dc024695811d5cfcb21cd18f8c0b99" and "fe5e71a2486dfe579a932bfb2d1d51961b1b4c9d" have entirely different histories.

20 changed files with 6917 additions and 4551 deletions

5
.gitattributes vendored
View file

@ -1,5 +0,0 @@
* text=auto eol=lf
*.png binary
*.pdf binary
*.docx binary
*.pgerd text

View file

@ -1,36 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres" --health-interval 5s --health-retries 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install ruff==0.14.0 psycopg2-binary==2.9.9 pandas==2.1.4 numpy==1.26.2
- name: Ruff (синтаксис и ошибки)
run: ruff check --select E9,F .
- name: Схема и импорт датасета
env:
PGPASSWORD: postgres
DB_USER: postgres
DB_PASSWORD: postgres
run: |
psql -h localhost -U postgres -f sql/create_schema.sql
python scripts/import_data.py
test "$(psql -h localhost -U postgres -d mobile_devices_db -tAc 'select count(*) from models')" -gt 900

View file

@ -1,23 +0,0 @@
name: Release
on:
push:
tags: ["v*"]
jobs:
exe:
runs-on: windows-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install pyinstaller psycopg2-binary==2.9.9 PyQt6==6.6.1
- name: Сборка .exe из однофайловой версии
run: pyinstaller --onefile --windowed --name MobileDevices scripts/exe/mobile_devices.py
- name: Файл в релиз
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${{ github.ref_name }}" dist/MobileDevices.exe --clobber

View file

@ -1,76 +0,0 @@
# Mobile Devices Database
[Русский](README.md) · **English**
[![CI](https://github.com/EDeev/mobiles_dataset/actions/workflows/ci.yml/badge.svg)](https://github.com/EDeev/mobiles_dataset/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/EDeev/mobiles_dataset)](https://github.com/EDeev/mobiles_dataset/releases)
Database design coursework: the 2025 mobile devices dataset split into a normalized PostgreSQL database,
a PyQt6 desktop app to work with it, and a measurement of index gains with `EXPLAIN ANALYZE`.
**Status:** coursework ("Database Design and Administration", Moscow Polytechnic University, 2025), completed
![Models tab](docs/screenshots/models.png)
**Stack:** Python 3.11 · PostgreSQL 15 · psycopg2 · PyQt6 · pandas · Docker
## Features
- 3NF schema of five tables (companies, processors, models, regions, prices) with foreign keys and
cascading actions
- Import of the [Kaggle dataset](https://www.kaggle.com/datasets/abdulmalik1518/mobiles-dataset-2025)
(930 rows; 914 models and 4569 prices after cleaning) split into reference tables
- App: adding companies, models with create, edit and delete, prices in five regions (Pakistan, India,
China, UAE, USA) with currency symbols, search by name, company and RAM, a price statistics tab
- Index experiment: `EXPLAIN ANALYZE` queries before and after, plans saved in `sql/explain_results/`
| Metric | No indexes | With indexes | Difference |
|---|---|---|---|
| Search | 0.234 ms | 0.089 ms | 62% faster |
| Four-table JOIN | 18.6 ms | 0.95 ms | 95% faster |
| Planner cost | 44.76 | 12.45 | 72% lower |
| Rows scanned | 914 | 18 | 98% fewer |
Measured on this dataset (hundreds of rows), so absolute times are fractions of a millisecond.
## Quick start
```bash
git clone https://github.com/EDeev/mobiles_dataset.git && cd mobiles_dataset
docker compose up -d # PostgreSQL 15 with the schema from sql/create_schema.sql
pip install -r requirements.txt
python scripts/import_data.py # load the dataset
python main.py # the app
```
A ready-made Windows build (`.exe`) is in the [releases](https://github.com/EDeev/mobiles_dataset/releases).
## Without Docker
1. Run `sql/create_schema.sql` in psql or pgAdmin as `postgres` — the script creates the
`mobile_devices_db` database itself.
2. Set the connection with `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
(defaults: `localhost:5432`, `mobile_devices_db`, `admin` / `password`).
3. `python scripts/import_data.py`, then `python main.py`.
## Screenshots
| Companies | Analytics |
|---|---|
| ![Companies](docs/screenshots/companies.png) | ![Analytics](docs/screenshots/analytics.png) |
## License
Coursework (Database Design and Administration, Moscow Polytechnic University, 2025). The code is open
for study; there is no separate license. The dataset comes from Kaggle under its author's terms.
## Author
**Egor Deev** — [GitHub](https://github.com/EDeev) · [Telegram](https://t.me/DeevEgor) · [egor@deev.space](mailto:egor@deev.space)
---
<div align="center">
<sub>⭐ If you find this project useful, give it a star on GitHub!</sub>
<p><sub>Made with ❤️ — <a href="https://deev.space">deev.space</a></sub></p>
</div>

327
README.md
View file

@ -1,89 +1,290 @@
# Mobile Devices Database # 📱 Mobile Devices Database Management System
**Русский** · [English](README.en.md) **Комплексная система управления каталогом мобильных устройств с GUI-интерфейсом**
[![CI](https://github.com/EDeev/mobiles_dataset/actions/workflows/ci.yml/badge.svg)](https://github.com/EDeev/mobiles_dataset/actions/workflows/ci.yml) [![Python](https://img.shields.io/badge/Python-3.11+-blue.svg)](https://python.org)
[![Release](https://img.shields.io/github/v/release/EDeev/mobiles_dataset)](https://github.com/EDeev/mobiles_dataset/releases) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15.x-blue.svg)](https://postgresql.org)
[![PyQt6](https://img.shields.io/badge/PyQt6-6.6+-green.svg)](https://pypi.org/project/PyQt6/)
[![License](https://img.shields.io/badge/License-Academic-orange.svg)](#)
Курсовая по проектированию баз данных: датасет мобильных устройств 2025 года, разложенный в ## 🎯 Назначение проекта
нормализованную базу PostgreSQL, десктопное приложение на PyQt6 для работы с ним и измерение
выигрыша от индексов через `EXPLAIN ANALYZE`.
**Статус:** учебный проект (курсовая «Проектирование и администрирование баз данных», Московский Курсовая работа по дисциплине "Проектирование и администрирование баз данных", представляющая собой полнофункциональную информационную систему для управления данными о мобильных устройствах. Проект демонстрирует практическое применение принципов реляционного моделирования, оптимизации производительности СУБД и создания современных desktop-приложений.
Политех, 2025), завершён
![Вкладка «Модели»](docs/screenshots/models.png) ## 🏗️ Архитектура системы
**Стек:** Python 3.11 · PostgreSQL 15 · psycopg2 · PyQt6 · pandas · Docker ### Компонентная структура
```
📦 mobile-devices-db/
├── database.py # Слой доступа к данным (DAO)
├── main_window.py # GUI-интерфейс на PyQt6
├── main.py # Запускаемый файл
├── Mobiles Dataset 2025.csv # Дата-сет данных
├── 🗃️ sql/ # SQL-скрипты и схема БД
│ ├── create_schema.sql # Создание нормализованной структуры
│ └── performance_analysis.sql # Тестовые запросы для анализа
├── 🐍 scripts/ # Python-модули системы
│ └── import_data.py # Система импорта и нормализации
├── 📊 docs/ # Техническая документация
└── 📋 requirements.txt # Зависимости проекта
```
## Возможности ### Технологический стек
- **СУБД**: PostgreSQL 15.x с расширенными возможностями индексирования
- **Backend**: Python 3.11+ с паттерном Singleton для управления соединениями
- **Frontend**: PyQt6 с архитектурой Model-View-Controller
- **Интеграция**: psycopg2 для типобезопасного взаимодействия с БД
- Схема в 3НФ из пяти таблиц (компании, процессоры, модели, регионы, цены) с внешними ключами и ## ⚡ Ключевые особенности
каскадными операциями
- Импорт [датасета с Kaggle](https://www.kaggle.com/datasets/abdulmalik1518/mobiles-dataset-2025)
(930 строк, после очистки 914 моделей и 4569 цен) с разнесением по справочникам
- Приложение: добавление компаний, модели с добавлением, правкой и удалением, цены по пяти регионам (Пакистан,
Индия, Китай, ОАЭ, США) с символами валют, поиск по названию, компании и RAM, вкладка статистики цен
- Эксперимент с индексами: запросы `EXPLAIN ANALYZE` до и после, планы сохранены в `sql/explain_results/`
| Метрика | Без индексов | С индексами | Разница | ### 🔧 Техническая реализация
|---|---|---|---| - **Нормализация до 3НФ**: Декомпозиция исходного CSV в 5 взаимосвязанных таблиц
| Поиск | 0,234 мс | 0,089 мс | быстрее на 62 % | - **Референциальная целостность**: Система FK с каскадными операциями
| JOIN четырёх таблиц | 18,6 мс | 0,95 мс | быстрее на 95 % | - **Оптимизация производительности**: Стратегическое индексирование с 62-95% улучшением
| Стоимость по планировщику | 44,76 | 12,45 | ниже на 72 % | - **CRUD-функционал**: Полный спектр операций через интуитивный GUI
| Просмотренные строки | 914 | 18 | меньше на 98 % |
Измерено на этом датасете (сотни строк), поэтому абсолютные времена — доли миллисекунды. ### 📈 Результаты оптимизации
| Метрика | До оптимизации | После оптимизации | Улучшение |
|---------|----------------|-------------------|-----------|
| Время выполнения поиска | 0.234 мс | 0.089 мс | **62% быстрее** |
| JOIN-запросы (4 таблицы) | 18.6 мс | 0.95 мс | **95% быстрее** |
| Стоимость по планировщику | 44.76 ед. | 12.45 ед. | **72% снижение** |
| Обработанные строки | 914 (фильтрация) | 18 (прямой доступ) | **98% сокращение** |
## Быстрый старт ### 🌍 Функциональные возможности
- **Интеллектуальный поиск** по множественным атрибутам устройств
- **Валютная локализация** ценовой информации (5 регионов)
- **Автоматический импорт** данных с нормализацией на лету
- **Аналитические отчеты** с региональной статистикой
## 📊 Источники данных
Проект использует актуальный датасет мобильных устройств 2025 года:
- **Источник**: [Kaggle - Mobiles Dataset 2025](https://www.kaggle.com/datasets/abdulmalik1518/mobiles-dataset-2025)
- **Размер**: 1000+ записей устройств
- **Охват**: Основные производители (Apple, Samsung, Xiaomi, OnePlus, etc.)
- **Регионы**: Пакистан, Индия, Китай, США, ОАЭ
## 🚀 Быстрый старт
### Системные требования
- **Python 3.11+** с поддержкой venv
- **PostgreSQL 15.x** с административными правами
- **4GB RAM** (рекомендуется 8GB для больших наборов данных)
### Установка и настройка
1. **Клонирование репозитория**
```bash
git clone https://github.com/username/mobile-devices-db.git
cd mobile-devices-db
```
2. **Создание виртуального окружения**
```bash
python -m venv venv
source venv/bin/activate # Linux/macOS
# или
venv\Scripts\activate # Windows
```
3. **Установка зависимостей**
```bash ```bash
git clone https://github.com/EDeev/mobiles_dataset.git && cd mobiles_dataset
docker compose up -d # PostgreSQL 15 со схемой из sql/create_schema.sql
pip install -r requirements.txt pip install -r requirements.txt
python scripts/import_data.py # загрузка датасета
python main.py # приложение
``` ```
Готовая сборка для Windows (`.exe`) — в [релизах](https://github.com/EDeev/mobiles_dataset/releases). 4. **Настройка PostgreSQL**
```sql
## Без Docker -- Создание пользователя и БД
CREATE USER admin WITH PASSWORD 'your_password';
1. Выполните `sql/create_schema.sql` в psql или pgAdmin под пользователем `postgres` — скрипт сам CREATE DATABASE mobile_devices_db OWNER admin;
создаёт базу `mobile_devices_db`. GRANT ALL PRIVILEGES ON DATABASE mobile_devices_db TO admin;
2. Задайте подключение переменными `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
(по умолчанию `localhost:5432`, `mobile_devices_db`, `admin` / `password`).
3. `python scripts/import_data.py`, затем `python main.py`.
## Как выглядит
| Компании | Аналитика |
|---|---|
| ![Компании](docs/screenshots/companies.png) | ![Аналитика](docs/screenshots/analytics.png) |
## Структура
```
database.py доступ к БД (синглтон, параметризованные запросы)
main_window.py, main.py приложение на PyQt6
scripts/import_data.py импорт и нормализация датасета
scripts/exe/ однофайловая версия приложения для сборки .exe
sql/ схема, запросы для анализа производительности, ERD, результаты EXPLAIN
docs/report.* отчёт по курсовой (md, docx, pdf)
``` ```
## Лицензия 5. **Развертывание схемы данных**
```bash
psql -U admin -d mobile_devices_db -f sql/create_schema.sql
```
Учебный проект (Проектирование и администрирование баз данных, Московский Политех, 2025). Код открыт 6. **Импорт тестовых данных**
для изучения, отдельной лицензии нет. Датасет — с Kaggle, на условиях его автора. ```bash
python scripts/import_data.py
```
## Автор 7. **Запуск приложения**
```bash
python scripts/main_window.py
```
**Деев Егор Викторович** — [GitHub](https://github.com/EDeev) · [Telegram](https://t.me/DeevEgor) · [egor@deev.space](mailto:egor@deev.space) ## 📊 Структура базы данных
### Реляционная модель (3НФ)
```mermaid
erDiagram
COMPANIES ||--o{ MODELS : manufactures
PROCESSORS ||--o{ MODELS : uses
MODELS ||--o{ PRICES : "has prices in"
REGIONS ||--o{ PRICES : "contains prices"
COMPANIES {
int company_id PK
varchar company_name UK
}
MODELS {
int model_id PK
varchar model_name
int company_id FK
int processor_id FK
varchar ram
varchar battery_capacity
int launched_year
}
PRICES {
int price_id PK
int model_id FK
int region_id FK
decimal price
varchar currency
}
```
### Ключевые таблицы
- **Companies**: Справочник производителей (19 уникальных компаний)
- **Models**: Каталог устройств (914 моделей с техническими характеристиками)
- **Processors**: Справочник процессоров (217 уникальных чипсетов)
- **Regions**: Географические регионы ценообразования (5 регионов)
- **Prices**: Региональная ценовая информация (4,569 записей)
## 🎨 Пользовательский интерфейс
### Основные компоненты
- **📋 Каталог компаний**: Обзор производителей с количеством моделей
- **📱 Управление моделями**: CRUD-операции с техническими характеристиками
- **💰 Ценовое управление**: Редактирование цен с валютной локализацией
- **📊 Аналитическая панель**: Региональная статистика и отчеты
### Функциональные возможности
```python
# Пример поискового запроса
def search_models(self, search_text: str):
"""Интеллектуальный поиск по множественным атрибутам"""
return self.db.search_models(search_text)
# Валютная локализация
CURRENCY_MAP = {
'Pakistan': ('PKR', '₨'),
'India': ('INR', '₹'),
'China': ('CNY', '¥'),
'USA': ('USD', '$'),
'Dubai': ('AED', 'د.إ')
}
```
## 🔍 Анализ производительности
### Методология тестирования
Комплексный анализ производительности выполнен с использованием PostgreSQL EXPLAIN ANALYZE:
#### Тестовые сценарии
1. **Простые селективные запросы** - поиск по названию компании
2. **Многотабличные JOIN** - получение полной информации об устройствах
3. **Агрегирующие операции** - статистические запросы с группировкой
4. **Поиск по характеристикам** - фильтрация по техническим параметрам
#### Стратегия оптимизации
```sql
-- Базовые B-tree индексы
CREATE INDEX idx_companies_name ON companies(company_name);
CREATE INDEX idx_models_company_id ON models(company_id);
-- Составные индексы для комплексных запросов
CREATE INDEX idx_models_ram_battery ON models(ram, battery_capacity);
-- Функциональные индексы для LIKE-операций
CREATE INDEX idx_companies_name_pattern ON companies(company_name varchar_pattern_ops);
```
## 📈 Практические результаты
### Количественные показатели
- **Импорт данных**: 930 записей устройств с 100% целостностью
- **Нормализация**: 60% сокращение избыточности данных
- **Производительность**: 95% улучшение для сложных JOIN-запросов
- **Индексирование**: 72% снижение стоимости по планировщику
### Качественные достижения
- Полная автоматизация процесса нормализации данных
- Интуитивный GUI с поддержкой валютной локализации
- Масштабируемая архитектура для enterprise-развертывания
- Комплексная система мониторинга производительности
## 🔧 Документация
- **[Техническая документация](docs/README.md)** - Подробное описание архитектуры и API
- **[Отчет по курсовой работе](docs/report.md)** - Полный академический отчет
- **[SQL-скрипты](sql/)** - Схемы БД и запросы для анализа производительности
### Конфигурация базы данных
```python
# Параметры подключения
DATABASE_CONFIG = {
'host': 'localhost',
'port': 5432,
'database': 'mobile_devices_db',
'user': 'admin',
'password': 'your_password'
}
```
### Основные зависимости
```
psycopg2-binary==2.9.9 # PostgreSQL adapter
PyQt6==6.6.1 # GUI framework
pandas==2.1.4 # Data processing
numpy==1.26.2 # Numerical computing
```
## 🚀 Перспективы развития
### Краткосрочные цели
- **Веб-интерфейс**: Реализация REST API на FastAPI
- **Автоматизация**: Интеграция с внешними API для обновления каталога
- **Мониторинг**: Real-time дашборды производительности
### Долгосрочная стратегия
- **Machine Learning**: Прогнозирование ценовых тенденций
- **Микросервисы**: Архитектурная модернизация для cloud-deployment
- **Аналитика**: Интеграция с Apache Kafka для stream-processing
## 📚 Академическая ценность
Проект демонстрирует практическое применение ключевых концепций:
### Теоретические основы
- **Нормализация реляционных данных** до третьей нормальной формы
- **Оптимизация производительности СУБД** через стратегическое индексирование
- **Архитектурные паттерны** для desktop-приложений с базами данных
### Практические навыки
- Проектирование и реализация нормализованных схем данных
- Анализ и оптимизация производительности SQL-запросов
- Создание многокомпонентных приложений с GUI-интерфейсом
## 📄 Лицензия
Этот проект создан в учебных целях как курсовая работа по дисциплине "Проектирование и администрирование баз данных" в Московском Политехническом Университете, 2024-2025 учебный год.
## 👨‍💻 Автор
**Деев Егор Викторович** - Backend Developer
- GitHub: [@EDeev](https://github.com/EDeev)
- Email: egor@deev.space
- Telegram: [@Egor_Deev](https://t.me/Egor_Deev)
--- ---
<div align="center"> <div align="center">
<sub>⭐ Если проект оказался полезным, поставьте звёздочку на GitHub!</sub> <p><sub>Создано с ❤️ от вашего дорогого - deev.space ©</sub></p>
<p><sub>Сделано с ❤️ — <a href="https://deev.space">deev.space</a></sub></p>
</div> </div>

View file

@ -1,20 +0,0 @@
# Локальный PostgreSQL 15 со схемой курсовой: docker compose up -d
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- ./docker/00-roles.sql:/docker-entrypoint-initdb.d/00-roles.sql:ro
- ./sql/create_schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d mobile_devices_db"]
interval: 5s
retries: 20
volumes:
pgdata:

View file

@ -1,5 +1,4 @@
# db/database.py # db/database.py
import os
import psycopg2 import psycopg2
from psycopg2.extras import RealDictCursor from psycopg2.extras import RealDictCursor
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
@ -21,13 +20,8 @@ class Database:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
return cls._instance return cls._instance
def __init__(self, host=None, port=None, database=None, user=None, password=None): def __init__(self, host='localhost', port=5432, database='mobile_devices_db',
# Параметры подключения: аргументы, затем переменные окружения DB_*, затем значения для локальной разработки user='admin', password='password'):
host = host or os.environ.get('DB_HOST', 'localhost')
port = port or int(os.environ.get('DB_PORT', '5432'))
database = database or os.environ.get('DB_NAME', 'mobile_devices_db')
user = user or os.environ.get('DB_USER', 'admin')
password = password or os.environ.get('DB_PASSWORD', 'password')
if not hasattr(self, 'initialized'): if not hasattr(self, 'initialized'):
self.connection_params = { self.connection_params = {
'host': host, 'host': host,

View file

@ -1,2 +0,0 @@
-- Роль приложения для локального стенда (значения по умолчанию из database.py)
CREATE ROLE admin LOGIN SUPERUSER PASSWORD 'password';

2374
docs/README.md Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

View file

@ -1,6 +1,7 @@
import sys import sys
import logging import logging
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import Qt
from main_window import MainWindow from main_window import MainWindow

View file

@ -1,14 +1,15 @@
# ui/main_window.py # ui/main_window.py
import sys
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QPushButton, QTabWidget, QTableWidget, QTableWidgetItem, QPushButton, QTabWidget,
QLabel, QLineEdit, QComboBox, QSpinBox, QMessageBox, QLabel, QLineEdit, QComboBox, QSpinBox, QMessageBox,
QDialog, QFormLayout, QDialogButtonBox, QHeaderView, QDialog, QFormLayout, QDialogButtonBox, QHeaderView,
QToolBar, QStatusBar, QGroupBox, QTextEdit, QInputDialog QToolBar, QStatusBar, QGroupBox, QTextEdit, QInputDialog
) )
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtGui import QAction, QFont from PyQt6.QtGui import QAction, QIcon, QFont
from typing import Dict, Any from typing import Optional, Dict, Any
import logging import logging
# Импортируем наш модуль БД # Импортируем наш модуль БД
@ -189,9 +190,6 @@ class PriceDialog(QDialog):
def load_prices(self): def load_prices(self):
"""Загрузка цен из БД""" """Загрузка цен из БД"""
prices = self.db.get_model_prices(self.model_id) prices = self.db.get_model_prices(self.model_id)
# При включённой сортировке строки переставляются прямо во время заполнения,
# и кнопки строки оказываются привязаны к чужим записям — выключаем её на время
self.prices_table.setSortingEnabled(False)
self.prices_table.setRowCount(len(prices)) self.prices_table.setRowCount(len(prices))
for row, price_data in enumerate(prices): for row, price_data in enumerate(prices):
@ -216,9 +214,6 @@ class PriceDialog(QDialog):
delete_btn = QPushButton("Удалить") delete_btn = QPushButton("Удалить")
delete_btn.clicked.connect(lambda checked, pid=price_data['price_id']: self.delete_price(pid)) delete_btn.clicked.connect(lambda checked, pid=price_data['price_id']: self.delete_price(pid))
self.prices_table.setCellWidget(row, 3, delete_btn) self.prices_table.setCellWidget(row, 3, delete_btn)
# сортировку возвращаем только после заполнения (см. начало функции)
self.prices_table.setSortingEnabled(True)
def add_update_price(self): def add_update_price(self):
"""Добавление или обновление цены""" """Добавление или обновление цены"""
@ -430,9 +425,6 @@ class MainWindow(QMainWindow):
def load_companies(self): def load_companies(self):
"""Загрузка списка компаний""" """Загрузка списка компаний"""
companies = self.db.get_all_companies() companies = self.db.get_all_companies()
# При включённой сортировке строки переставляются прямо во время заполнения,
# и кнопки строки оказываются привязаны к чужим записям — выключаем её на время
self.companies_table.setSortingEnabled(False)
self.companies_table.setRowCount(len(companies)) self.companies_table.setRowCount(len(companies))
for row, company in enumerate(companies): for row, company in enumerate(companies):
@ -450,9 +442,6 @@ class MainWindow(QMainWindow):
count_item = QTableWidgetItem(str(company['models_count'])) count_item = QTableWidgetItem(str(company['models_count']))
count_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) count_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.companies_table.setItem(row, 2, count_item) self.companies_table.setItem(row, 2, count_item)
# сортировку возвращаем только после заполнения (см. начало функции)
self.companies_table.setSortingEnabled(True)
def load_models(self, search_text=""): def load_models(self, search_text=""):
"""Загрузка списка моделей""" """Загрузка списка моделей"""
@ -461,9 +450,6 @@ class MainWindow(QMainWindow):
else: else:
models = self.db.get_all_models() models = self.db.get_all_models()
# При включённой сортировке строки переставляются прямо во время заполнения,
# и кнопки строки оказываются привязаны к чужим записям — выключаем её на время
self.models_table.setSortingEnabled(False)
self.models_table.setRowCount(len(models)) self.models_table.setRowCount(len(models))
for row, model in enumerate(models): for row, model in enumerate(models):
@ -522,9 +508,6 @@ class MainWindow(QMainWindow):
delete_btn = QPushButton("🗑️ Удалить") delete_btn = QPushButton("🗑️ Удалить")
delete_btn.clicked.connect(lambda checked, mid=model['model_id']: self.delete_model(mid)) delete_btn.clicked.connect(lambda checked, mid=model['model_id']: self.delete_model(mid))
self.models_table.setCellWidget(row, 10, delete_btn) self.models_table.setCellWidget(row, 10, delete_btn)
# сортировку возвращаем только после заполнения (см. начало функции)
self.models_table.setSortingEnabled(True)
def add_company(self): def add_company(self):
"""Добавление новой компании""" """Добавление новой компании"""

View file

@ -5,21 +5,24 @@ from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
# ui/main_window.py # ui/main_window.py
import sys
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QPushButton, QTabWidget, QTableWidget, QTableWidgetItem, QPushButton, QTabWidget,
QLabel, QLineEdit, QComboBox, QSpinBox, QMessageBox, QLabel, QLineEdit, QComboBox, QSpinBox, QMessageBox,
QDialog, QFormLayout, QDialogButtonBox, QHeaderView, QDialog, QFormLayout, QDialogButtonBox, QHeaderView,
QToolBar, QStatusBar, QGroupBox, QTextEdit, QInputDialog QToolBar, QStatusBar, QGroupBox, QTextEdit, QInputDialog
) )
from PyQt6.QtGui import QAction, QFont from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtGui import QAction, QIcon, QFont
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
import logging
# db/database.py # db/database.py
import os
import psycopg2 import psycopg2
from psycopg2.extras import RealDictCursor from psycopg2.extras import RealDictCursor
from typing import List from typing import List, Dict, Any, Optional
import logging
from contextlib import contextmanager from contextlib import contextmanager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -45,13 +48,8 @@ class Database:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
return cls._instance return cls._instance
def __init__(self, host=None, port=None, database=None, user=None, password=None): def __init__(self, host='localhost', port=5432, database='mobile_devices_db',
# Параметры подключения: аргументы, затем переменные окружения DB_*, затем значения для локальной разработки user='admin', password='password'):
host = host or os.environ.get('DB_HOST', 'localhost')
port = port or int(os.environ.get('DB_PORT', '5432'))
database = database or os.environ.get('DB_NAME', 'mobile_devices_db')
user = user or os.environ.get('DB_USER', 'admin')
password = password or os.environ.get('DB_PASSWORD', 'password')
if not hasattr(self, 'initialized'): if not hasattr(self, 'initialized'):
self.connection_params = { self.connection_params = {
'host': host, 'host': host,
@ -505,9 +503,6 @@ class PriceDialog(QDialog):
def load_prices(self): def load_prices(self):
"""Загрузка цен из БД""" """Загрузка цен из БД"""
prices = self.db.get_model_prices(self.model_id) prices = self.db.get_model_prices(self.model_id)
# При включённой сортировке строки переставляются прямо во время заполнения,
# и кнопки строки оказываются привязаны к чужим записям — выключаем её на время
self.prices_table.setSortingEnabled(False)
self.prices_table.setRowCount(len(prices)) self.prices_table.setRowCount(len(prices))
for row, price_data in enumerate(prices): for row, price_data in enumerate(prices):
@ -532,9 +527,6 @@ class PriceDialog(QDialog):
delete_btn = QPushButton("Удалить") delete_btn = QPushButton("Удалить")
delete_btn.clicked.connect(lambda checked, pid=price_data['price_id']: self.delete_price(pid)) delete_btn.clicked.connect(lambda checked, pid=price_data['price_id']: self.delete_price(pid))
self.prices_table.setCellWidget(row, 3, delete_btn) self.prices_table.setCellWidget(row, 3, delete_btn)
# сортировку возвращаем только после заполнения (см. начало функции)
self.prices_table.setSortingEnabled(True)
def add_update_price(self): def add_update_price(self):
"""Добавление или обновление цены""" """Добавление или обновление цены"""
@ -745,9 +737,6 @@ class MainWindow(QMainWindow):
def load_companies(self): def load_companies(self):
"""Загрузка списка компаний""" """Загрузка списка компаний"""
companies = self.db.get_all_companies() companies = self.db.get_all_companies()
# При включённой сортировке строки переставляются прямо во время заполнения,
# и кнопки строки оказываются привязаны к чужим записям — выключаем её на время
self.companies_table.setSortingEnabled(False)
self.companies_table.setRowCount(len(companies)) self.companies_table.setRowCount(len(companies))
for row, company in enumerate(companies): for row, company in enumerate(companies):
@ -765,9 +754,6 @@ class MainWindow(QMainWindow):
count_item = QTableWidgetItem(str(company['models_count'])) count_item = QTableWidgetItem(str(company['models_count']))
count_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) count_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.companies_table.setItem(row, 2, count_item) self.companies_table.setItem(row, 2, count_item)
# сортировку возвращаем только после заполнения (см. начало функции)
self.companies_table.setSortingEnabled(True)
def load_models(self, search_text=""): def load_models(self, search_text=""):
"""Загрузка списка моделей""" """Загрузка списка моделей"""
@ -776,9 +762,6 @@ class MainWindow(QMainWindow):
else: else:
models = self.db.get_all_models() models = self.db.get_all_models()
# При включённой сортировке строки переставляются прямо во время заполнения,
# и кнопки строки оказываются привязаны к чужим записям — выключаем её на время
self.models_table.setSortingEnabled(False)
self.models_table.setRowCount(len(models)) self.models_table.setRowCount(len(models))
for row, model in enumerate(models): for row, model in enumerate(models):
@ -837,9 +820,6 @@ class MainWindow(QMainWindow):
delete_btn = QPushButton("🗑️ Удалить") delete_btn = QPushButton("🗑️ Удалить")
delete_btn.clicked.connect(lambda checked, mid=model['model_id']: self.delete_model(mid)) delete_btn.clicked.connect(lambda checked, mid=model['model_id']: self.delete_model(mid))
self.models_table.setCellWidget(row, 10, delete_btn) self.models_table.setCellWidget(row, 10, delete_btn)
# сортировку возвращаем только после заполнения (см. начало функции)
self.models_table.setSortingEnabled(True)
def add_company(self): def add_company(self):
"""Добавление новой компании""" """Добавление новой компании"""

View file

@ -1,9 +1,8 @@
import os
import sys
import pandas as pd import pandas as pd
import psycopg2 import psycopg2
from psycopg2.extras import execute_values
import re import re
from typing import Optional, Dict from typing import Optional, Dict, Tuple
import logging import logging
# Настройка логирования # Настройка логирования
@ -246,13 +245,12 @@ class MobileDataImporter:
# Использование скрипта # Использование скрипта
if __name__ == "__main__": if __name__ == "__main__":
# Конфигурация подключения к БД # Конфигурация подключения к БД
# Параметры подключения — из переменных окружения DB_* (по умолчанию — локальная разработка)
db_config = { db_config = {
'host': os.environ.get('DB_HOST', 'localhost'), 'host': 'localhost',
'port': int(os.environ.get('DB_PORT', '5432')), 'port': 5432,
'database': os.environ.get('DB_NAME', 'mobile_devices_db'), 'database': 'mobile_devices_db',
'user': os.environ.get('DB_USER', 'admin'), 'user': 'admin',
'password': os.environ.get('DB_PASSWORD', 'password'), 'password': 'password' # Замените на ваш пароль
} }
# Создаем импортер и выполняем импорт # Создаем импортер и выполняем импорт
@ -260,9 +258,6 @@ if __name__ == "__main__":
try: try:
importer.connect() importer.connect()
# Датасет лежит в корне репозитория; другой файл можно передать первым аргументом importer.import_data('Mobiles Dataset 2025.csv') # Укажите путь к вашему файлу
csv_path = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'Mobiles Dataset 2025.csv')
importer.import_data(csv_path)
finally: finally:
importer.disconnect() importer.disconnect()