mirror of
https://github.com/EDeev/mobiles_dataset.git
synced 2026-06-15 11:01:02 +03:00
v. 1.1
This commit is contained in:
parent
dc9bdd8e76
commit
f1f3f8ee22
1 changed files with 938 additions and 0 deletions
938
scripts/exe/mobile_devices.py
Normal file
938
scripts/exe/mobile_devices.py
Normal file
|
|
@ -0,0 +1,938 @@
|
||||||
|
# main.py
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
# ui/main_window.py
|
||||||
|
import sys
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QTableWidget, QTableWidgetItem, QPushButton, QTabWidget,
|
||||||
|
QLabel, QLineEdit, QComboBox, QSpinBox, QMessageBox,
|
||||||
|
QDialog, QFormLayout, QDialogButtonBox, QHeaderView,
|
||||||
|
QToolBar, QStatusBar, QGroupBox, QTextEdit, QInputDialog
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
|
||||||
|
from PyQt6.QtGui import QAction, QIcon, QFont
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# db/database.py
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
import logging
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CURRENCY_MAP = {
|
||||||
|
'Pakistan': ('PKR', '₨'),
|
||||||
|
'India': ('INR', '₹'),
|
||||||
|
'China': ('CNY', '¥'),
|
||||||
|
'USA': ('USD', '$'),
|
||||||
|
'Dubai': ('AED', 'د.إ')
|
||||||
|
}
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
"""
|
||||||
|
Класс для управления подключением к PostgreSQL и выполнения операций
|
||||||
|
Использует паттерн Singleton для единственного экземпляра подключения
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self, host='localhost', port=5432, database='mobile_devices_db',
|
||||||
|
user='admin', password='password'):
|
||||||
|
if not hasattr(self, 'initialized'):
|
||||||
|
self.connection_params = {
|
||||||
|
'host': host,
|
||||||
|
'port': port,
|
||||||
|
'database': database,
|
||||||
|
'user': user,
|
||||||
|
'password': password
|
||||||
|
}
|
||||||
|
self.connection = None
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Установка соединения с БД"""
|
||||||
|
try:
|
||||||
|
self.connection = psycopg2.connect(**self.connection_params)
|
||||||
|
logger.info("✅ Подключение к БД установлено")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Ошибка подключения к БД: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Закрытие соединения с БД"""
|
||||||
|
if self.connection:
|
||||||
|
self.connection.close()
|
||||||
|
logger.info("🔒 Соединение с БД закрыто")
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_cursor(self, dict_cursor=True):
|
||||||
|
"""
|
||||||
|
Контекстный менеджер для безопасной работы с курсором
|
||||||
|
"""
|
||||||
|
cursor_factory = RealDictCursor if dict_cursor else None
|
||||||
|
cursor = self.connection.cursor(cursor_factory=cursor_factory)
|
||||||
|
try:
|
||||||
|
yield cursor
|
||||||
|
self.connection.commit()
|
||||||
|
except Exception as e:
|
||||||
|
self.connection.rollback()
|
||||||
|
logger.error(f"❌ Ошибка выполнения запроса: {e}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
# === CRUD операции для Companies ===
|
||||||
|
|
||||||
|
def get_all_companies(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Получение всех компаний"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT c.company_id, c.company_name, COUNT(m.model_id) as models_count
|
||||||
|
FROM companies c
|
||||||
|
LEFT JOIN models m ON c.company_id = m.company_id
|
||||||
|
GROUP BY c.company_id, c.company_name
|
||||||
|
ORDER BY c.company_name
|
||||||
|
""")
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
def add_company(self, company_name: str) -> int:
|
||||||
|
"""Добавление новой компании"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO companies (company_name) VALUES (%s) RETURNING company_id",
|
||||||
|
(company_name,)
|
||||||
|
)
|
||||||
|
return cursor.fetchone()['company_id']
|
||||||
|
|
||||||
|
def update_company(self, company_id: int, company_name: str):
|
||||||
|
"""Обновление названия компании"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE companies SET company_name = %s WHERE company_id = %s",
|
||||||
|
(company_name, company_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_company(self, company_id: int):
|
||||||
|
"""Удаление компании (каскадно удалит все модели)"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("DELETE FROM companies WHERE company_id = %s", (company_id,))
|
||||||
|
|
||||||
|
# === CRUD операции для Models ===
|
||||||
|
|
||||||
|
def get_all_models(self, company_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||||
|
"""Получение всех моделей (опционально по компании)"""
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
m.model_id, m.model_name, c.company_name,
|
||||||
|
m.mobile_weight, m.ram, m.front_camera,
|
||||||
|
m.back_camera, pr.processor_name, m.battery_capacity,
|
||||||
|
m.screen_size, m.launched_year,
|
||||||
|
COUNT(DISTINCT p.region_id) as price_regions
|
||||||
|
FROM models m
|
||||||
|
JOIN companies c ON m.company_id = c.company_id
|
||||||
|
LEFT JOIN processors pr ON m.processor_id = pr.processor_id
|
||||||
|
LEFT JOIN prices p ON m.model_id = p.model_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = []
|
||||||
|
if company_id:
|
||||||
|
query += " WHERE m.company_id = %s"
|
||||||
|
params.append(company_id)
|
||||||
|
|
||||||
|
query += " GROUP BY m.model_id, c.company_name, pr.processor_name ORDER BY c.company_name, m.model_name"
|
||||||
|
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute(query, params)
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
def get_model_by_id(self, model_id: int) -> Dict[str, Any]:
|
||||||
|
"""Получение модели по ID"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT m.*, c.company_name, pr.processor_name
|
||||||
|
FROM models m
|
||||||
|
JOIN companies c ON m.company_id = c.company_id
|
||||||
|
LEFT JOIN processors pr ON m.processor_id = pr.processor_id
|
||||||
|
WHERE m.model_id = %s
|
||||||
|
""", (model_id,))
|
||||||
|
return cursor.fetchone()
|
||||||
|
|
||||||
|
def add_model(self, model_data: Dict[str, Any]) -> int:
|
||||||
|
"""Добавление новой модели"""
|
||||||
|
# Сначала получаем или создаем процессор
|
||||||
|
processor_id = None
|
||||||
|
if model_data.get('processor_name'):
|
||||||
|
processor_id = self.get_or_create_processor(model_data['processor_name'])
|
||||||
|
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO models
|
||||||
|
(model_name, company_id, processor_id, mobile_weight,
|
||||||
|
ram, front_camera, back_camera, battery_capacity,
|
||||||
|
screen_size, launched_year)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
RETURNING model_id
|
||||||
|
""", (
|
||||||
|
model_data['model_name'],
|
||||||
|
model_data['company_id'],
|
||||||
|
processor_id,
|
||||||
|
model_data.get('mobile_weight'),
|
||||||
|
model_data.get('ram'),
|
||||||
|
model_data.get('front_camera'),
|
||||||
|
model_data.get('back_camera'),
|
||||||
|
model_data.get('battery_capacity'),
|
||||||
|
model_data.get('screen_size'),
|
||||||
|
model_data.get('launched_year')
|
||||||
|
))
|
||||||
|
return cursor.fetchone()['model_id']
|
||||||
|
|
||||||
|
def update_model(self, model_id: int, model_data: Dict[str, Any]):
|
||||||
|
"""Обновление модели"""
|
||||||
|
# Получаем или создаем процессор
|
||||||
|
processor_id = None
|
||||||
|
if model_data.get('processor_name'):
|
||||||
|
processor_id = self.get_or_create_processor(model_data['processor_name'])
|
||||||
|
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE models SET
|
||||||
|
model_name = %s,
|
||||||
|
company_id = %s,
|
||||||
|
processor_id = %s,
|
||||||
|
mobile_weight = %s,
|
||||||
|
ram = %s,
|
||||||
|
front_camera = %s,
|
||||||
|
back_camera = %s,
|
||||||
|
battery_capacity = %s,
|
||||||
|
screen_size = %s,
|
||||||
|
launched_year = %s
|
||||||
|
WHERE model_id = %s
|
||||||
|
""", (
|
||||||
|
model_data['model_name'],
|
||||||
|
model_data['company_id'],
|
||||||
|
processor_id,
|
||||||
|
model_data.get('mobile_weight'),
|
||||||
|
model_data.get('ram'),
|
||||||
|
model_data.get('front_camera'),
|
||||||
|
model_data.get('back_camera'),
|
||||||
|
model_data.get('battery_capacity'),
|
||||||
|
model_data.get('screen_size'),
|
||||||
|
model_data.get('launched_year'),
|
||||||
|
model_id
|
||||||
|
))
|
||||||
|
|
||||||
|
def delete_model(self, model_id: int):
|
||||||
|
"""Удаление модели"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("DELETE FROM models WHERE model_id = %s", (model_id,))
|
||||||
|
|
||||||
|
# === CRUD операции для Prices ===
|
||||||
|
|
||||||
|
def get_model_prices(self, model_id: int) -> List[Dict[str, Any]]:
|
||||||
|
"""Получение всех цен для модели"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT p.price_id, p.model_id, p.region_id,
|
||||||
|
r.region_name, p.price, p.currency
|
||||||
|
FROM prices p
|
||||||
|
JOIN regions r ON p.region_id = r.region_id
|
||||||
|
WHERE p.model_id = %s
|
||||||
|
ORDER BY r.region_name
|
||||||
|
""", (model_id,))
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
def add_or_update_price(self, model_id: int, region_id: int, price: float):
|
||||||
|
"""Добавление или обновление цены"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO prices (model_id, region_id, price)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
ON CONFLICT (model_id, region_id)
|
||||||
|
DO UPDATE SET price = EXCLUDED.price
|
||||||
|
""", (model_id, region_id, price))
|
||||||
|
|
||||||
|
def delete_price(self, price_id: int):
|
||||||
|
"""Удаление цены"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("DELETE FROM prices WHERE price_id = %s", (price_id,))
|
||||||
|
|
||||||
|
# === Вспомогательные методы ===
|
||||||
|
|
||||||
|
def get_or_create_processor(self, processor_name: str) -> int:
|
||||||
|
"""Получение или создание процессора"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT processor_id FROM processors WHERE processor_name = %s",
|
||||||
|
(processor_name,)
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return result['processor_id']
|
||||||
|
else:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO processors (processor_name) VALUES (%s) RETURNING processor_id",
|
||||||
|
(processor_name,)
|
||||||
|
)
|
||||||
|
return cursor.fetchone()['processor_id']
|
||||||
|
|
||||||
|
def get_all_regions(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Получение всех регионов"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("SELECT * FROM regions ORDER BY region_name")
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
def get_all_processors(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Получение всех процессоров"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("SELECT * FROM processors ORDER BY processor_name")
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
# === Методы для аналитики ===
|
||||||
|
|
||||||
|
def get_price_statistics(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Получение статистики цен по регионам"""
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
r.region_name,
|
||||||
|
COUNT(p.price_id) as models_count,
|
||||||
|
AVG(p.price) as avg_price,
|
||||||
|
MIN(p.price) as min_price,
|
||||||
|
MAX(p.price) as max_price
|
||||||
|
FROM prices p
|
||||||
|
JOIN regions r ON p.region_id = r.region_id
|
||||||
|
GROUP BY r.region_name
|
||||||
|
ORDER BY avg_price DESC
|
||||||
|
""")
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
def search_models(self, search_text: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Поиск моделей по тексту"""
|
||||||
|
search_pattern = f"%{search_text}%"
|
||||||
|
with self.get_cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT DISTINCT
|
||||||
|
m.model_id, m.model_name, c.company_name,
|
||||||
|
m.ram, m.battery_capacity, m.launched_year
|
||||||
|
FROM models m
|
||||||
|
JOIN companies c ON m.company_id = c.company_id
|
||||||
|
WHERE
|
||||||
|
m.model_name ILIKE %s OR
|
||||||
|
c.company_name ILIKE %s OR
|
||||||
|
m.ram ILIKE %s OR
|
||||||
|
m.battery_capacity ILIKE %s
|
||||||
|
ORDER BY c.company_name, m.model_name
|
||||||
|
LIMIT 100
|
||||||
|
""", (search_pattern, search_pattern, search_pattern, search_pattern))
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
class ModelDialog(QDialog):
|
||||||
|
"""Диалог для добавления/редактирования модели"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None, model_data=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.model_data = model_data
|
||||||
|
self.db = Database()
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
self.setWindowTitle("Добавить модель" if not self.model_data else "Редактировать модель")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setMinimumWidth(500)
|
||||||
|
|
||||||
|
layout = QFormLayout()
|
||||||
|
|
||||||
|
# Поля формы
|
||||||
|
self.company_combo = QComboBox()
|
||||||
|
companies = self.db.get_all_companies()
|
||||||
|
for company in companies:
|
||||||
|
self.company_combo.addItem(company['company_name'], company['company_id'])
|
||||||
|
|
||||||
|
self.model_name_edit = QLineEdit()
|
||||||
|
self.weight_edit = QLineEdit()
|
||||||
|
self.ram_edit = QLineEdit()
|
||||||
|
self.front_camera_edit = QLineEdit()
|
||||||
|
self.back_camera_edit = QLineEdit()
|
||||||
|
self.processor_edit = QLineEdit()
|
||||||
|
self.battery_edit = QLineEdit()
|
||||||
|
self.screen_edit = QLineEdit()
|
||||||
|
self.year_spin = QSpinBox()
|
||||||
|
self.year_spin.setRange(2000, 2030)
|
||||||
|
self.year_spin.setValue(2024)
|
||||||
|
|
||||||
|
# Добавляем поля в форму
|
||||||
|
layout.addRow("Компания:", self.company_combo)
|
||||||
|
layout.addRow("Название модели:", self.model_name_edit)
|
||||||
|
layout.addRow("Вес:", self.weight_edit)
|
||||||
|
layout.addRow("RAM:", self.ram_edit)
|
||||||
|
layout.addRow("Фронтальная камера:", self.front_camera_edit)
|
||||||
|
layout.addRow("Основная камера:", self.back_camera_edit)
|
||||||
|
layout.addRow("Процессор:", self.processor_edit)
|
||||||
|
layout.addRow("Батарея:", self.battery_edit)
|
||||||
|
layout.addRow("Размер экрана:", self.screen_edit)
|
||||||
|
layout.addRow("Год выпуска:", self.year_spin)
|
||||||
|
|
||||||
|
# Заполняем данные при редактировании
|
||||||
|
if self.model_data:
|
||||||
|
self.model_name_edit.setText(self.model_data.get('model_name', ''))
|
||||||
|
self.weight_edit.setText(self.model_data.get('mobile_weight', ''))
|
||||||
|
self.ram_edit.setText(self.model_data.get('ram', ''))
|
||||||
|
self.front_camera_edit.setText(self.model_data.get('front_camera', ''))
|
||||||
|
self.back_camera_edit.setText(self.model_data.get('back_camera', ''))
|
||||||
|
self.processor_edit.setText(self.model_data.get('processor_name', ''))
|
||||||
|
self.battery_edit.setText(self.model_data.get('battery_capacity', ''))
|
||||||
|
self.screen_edit.setText(self.model_data.get('screen_size', ''))
|
||||||
|
if self.model_data.get('launched_year'):
|
||||||
|
self.year_spin.setValue(self.model_data['launched_year'])
|
||||||
|
|
||||||
|
# Устанавливаем компанию
|
||||||
|
for i in range(self.company_combo.count()):
|
||||||
|
if self.company_combo.itemData(i) == self.model_data.get('company_id'):
|
||||||
|
self.company_combo.setCurrentIndex(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Кнопки
|
||||||
|
buttons = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Ok |
|
||||||
|
QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
|
buttons.accepted.connect(self.accept)
|
||||||
|
buttons.rejected.connect(self.reject)
|
||||||
|
|
||||||
|
layout.addRow(buttons)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def get_data(self) -> Dict[str, Any]:
|
||||||
|
"""Получение данных из формы"""
|
||||||
|
return {
|
||||||
|
'company_id': self.company_combo.currentData(),
|
||||||
|
'model_name': self.model_name_edit.text(),
|
||||||
|
'mobile_weight': self.weight_edit.text() or None,
|
||||||
|
'ram': self.ram_edit.text() or None,
|
||||||
|
'front_camera': self.front_camera_edit.text() or None,
|
||||||
|
'back_camera': self.back_camera_edit.text() or None,
|
||||||
|
'processor_name': self.processor_edit.text() or None,
|
||||||
|
'battery_capacity': self.battery_edit.text() or None,
|
||||||
|
'screen_size': self.screen_edit.text() or None,
|
||||||
|
'launched_year': self.year_spin.value()
|
||||||
|
}
|
||||||
|
|
||||||
|
class PriceDialog(QDialog):
|
||||||
|
"""Диалог для управления ценами модели"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None, model_id=None, model_name=""):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.model_id = model_id
|
||||||
|
self.model_name = model_name
|
||||||
|
self.db = Database()
|
||||||
|
self.init_ui()
|
||||||
|
self.load_prices()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
self.setWindowTitle(f"Цены для: {self.model_name}")
|
||||||
|
self.setModal(True)
|
||||||
|
self.setMinimumSize(600, 400)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Таблица цен
|
||||||
|
self.prices_table = QTableWidget()
|
||||||
|
self.prices_table.setColumnCount(4)
|
||||||
|
self.prices_table.setHorizontalHeaderLabels(["Регион", "Цена", "Валюта", "Действия"])
|
||||||
|
self.prices_table.setSortingEnabled(True)
|
||||||
|
|
||||||
|
# Настройка размеров столбцов
|
||||||
|
header = self.prices_table.horizontalHeader()
|
||||||
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||||
|
|
||||||
|
layout.addWidget(self.prices_table)
|
||||||
|
|
||||||
|
# Форма добавления цены
|
||||||
|
add_group = QGroupBox("Добавить/Обновить цену")
|
||||||
|
add_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
self.region_combo = QComboBox()
|
||||||
|
regions = self.db.get_all_regions()
|
||||||
|
for region in regions:
|
||||||
|
self.region_combo.addItem(region['region_name'], region['region_id'])
|
||||||
|
|
||||||
|
self.price_edit = QLineEdit()
|
||||||
|
self.price_edit.setPlaceholderText("Цена")
|
||||||
|
|
||||||
|
add_btn = QPushButton("Добавить/Обновить")
|
||||||
|
add_btn.clicked.connect(self.add_update_price)
|
||||||
|
|
||||||
|
add_layout.addWidget(QLabel("Регион:"))
|
||||||
|
add_layout.addWidget(self.region_combo)
|
||||||
|
add_layout.addWidget(QLabel("Цена:"))
|
||||||
|
add_layout.addWidget(self.price_edit)
|
||||||
|
add_layout.addWidget(add_btn)
|
||||||
|
|
||||||
|
add_group.setLayout(add_layout)
|
||||||
|
layout.addWidget(add_group)
|
||||||
|
|
||||||
|
# Кнопка закрытия
|
||||||
|
close_btn = QPushButton("Закрыть")
|
||||||
|
close_btn.clicked.connect(self.close)
|
||||||
|
layout.addWidget(close_btn)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def format_price(self, price: float, region_name: str) -> str:
|
||||||
|
"""Форматирование цены с правильным символом валюты"""
|
||||||
|
currency_code, currency_symbol = CURRENCY_MAP.get(region_name, ('USD', '$'))
|
||||||
|
return f"{currency_symbol}{price:,.2f}"
|
||||||
|
|
||||||
|
def load_prices(self):
|
||||||
|
"""Загрузка цен из БД"""
|
||||||
|
prices = self.db.get_model_prices(self.model_id)
|
||||||
|
self.prices_table.setRowCount(len(prices))
|
||||||
|
|
||||||
|
for row, price_data in enumerate(prices):
|
||||||
|
# Регион
|
||||||
|
region_item = QTableWidgetItem(price_data['region_name'])
|
||||||
|
region_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.prices_table.setItem(row, 0, region_item)
|
||||||
|
|
||||||
|
# Цена с правильной валютой
|
||||||
|
price_str = self.format_price(price_data['price'], price_data['region_name'])
|
||||||
|
price_item = QTableWidgetItem(price_str)
|
||||||
|
price_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.prices_table.setItem(row, 1, price_item)
|
||||||
|
|
||||||
|
# Код валюты
|
||||||
|
currency_code, _ = CURRENCY_MAP.get(price_data['region_name'], ('USD', '$'))
|
||||||
|
currency_item = QTableWidgetItem(currency_code)
|
||||||
|
currency_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.prices_table.setItem(row, 2, currency_item)
|
||||||
|
|
||||||
|
# Кнопка удаления
|
||||||
|
delete_btn = QPushButton("Удалить")
|
||||||
|
delete_btn.clicked.connect(lambda checked, pid=price_data['price_id']: self.delete_price(pid))
|
||||||
|
self.prices_table.setCellWidget(row, 3, delete_btn)
|
||||||
|
|
||||||
|
def add_update_price(self):
|
||||||
|
"""Добавление или обновление цены"""
|
||||||
|
try:
|
||||||
|
region_id = self.region_combo.currentData()
|
||||||
|
price = float(self.price_edit.text())
|
||||||
|
|
||||||
|
self.db.add_or_update_price(self.model_id, region_id, price)
|
||||||
|
self.load_prices()
|
||||||
|
self.price_edit.clear()
|
||||||
|
|
||||||
|
QMessageBox.information(self, "Успех", "Цена успешно обновлена!")
|
||||||
|
except ValueError:
|
||||||
|
QMessageBox.warning(self, "Ошибка", "Введите корректную цену!")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Ошибка", f"Ошибка при обновлении цены: {str(e)}")
|
||||||
|
|
||||||
|
def delete_price(self, price_id):
|
||||||
|
"""Удаление цены"""
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self, "Подтверждение",
|
||||||
|
"Вы уверены, что хотите удалить эту цену?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||||
|
)
|
||||||
|
|
||||||
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
|
try:
|
||||||
|
self.db.delete_price(price_id)
|
||||||
|
self.load_prices()
|
||||||
|
QMessageBox.information(self, "Успех", "Цена удалена!")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Ошибка", f"Ошибка при удалении: {str(e)}")
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
"""Главное окно приложения"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.db = Database()
|
||||||
|
self.init_ui()
|
||||||
|
self.connect_to_db()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
self.setWindowTitle("Датасет мобильных устройств")
|
||||||
|
self.setGeometry(100, 100, 1200, 600)
|
||||||
|
|
||||||
|
# Центральный виджет
|
||||||
|
central_widget = QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
# Основной layout
|
||||||
|
layout = QVBoxLayout(central_widget)
|
||||||
|
|
||||||
|
# Создаем панель инструментов
|
||||||
|
self.create_toolbar()
|
||||||
|
|
||||||
|
# Создаем вкладки
|
||||||
|
self.tabs = QTabWidget()
|
||||||
|
|
||||||
|
# Вкладка компаний
|
||||||
|
self.companies_tab = self.create_companies_tab()
|
||||||
|
self.tabs.addTab(self.companies_tab, "🏢 Компании")
|
||||||
|
|
||||||
|
# Вкладка моделей
|
||||||
|
self.models_tab = self.create_models_tab()
|
||||||
|
self.tabs.addTab(self.models_tab, "📱 Модели")
|
||||||
|
|
||||||
|
# Вкладка аналитики
|
||||||
|
self.analytics_tab = self.create_analytics_tab()
|
||||||
|
self.tabs.addTab(self.analytics_tab, "📊 Аналитика")
|
||||||
|
|
||||||
|
layout.addWidget(self.tabs)
|
||||||
|
|
||||||
|
# Статусная строка
|
||||||
|
self.status_bar = QStatusBar()
|
||||||
|
self.setStatusBar(self.status_bar)
|
||||||
|
self.status_bar.showMessage("Готов к работе")
|
||||||
|
|
||||||
|
def create_toolbar(self):
|
||||||
|
"""Создание панели инструментов"""
|
||||||
|
toolbar = QToolBar()
|
||||||
|
self.addToolBar(toolbar)
|
||||||
|
|
||||||
|
# Действия
|
||||||
|
refresh_action = QAction("🔄 Обновить", self)
|
||||||
|
refresh_action.triggered.connect(self.refresh_data)
|
||||||
|
toolbar.addAction(refresh_action)
|
||||||
|
|
||||||
|
toolbar.addSeparator()
|
||||||
|
|
||||||
|
add_company_action = QAction("➕ Добавить компанию", self)
|
||||||
|
add_company_action.triggered.connect(self.add_company)
|
||||||
|
toolbar.addAction(add_company_action)
|
||||||
|
|
||||||
|
toolbar.addSeparator()
|
||||||
|
|
||||||
|
add_model_action = QAction("➕ Добавить модель", self)
|
||||||
|
add_model_action.triggered.connect(self.add_model)
|
||||||
|
toolbar.addAction(add_model_action)
|
||||||
|
|
||||||
|
def create_companies_tab(self) -> QWidget:
|
||||||
|
"""Создание вкладки компаний"""
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
|
||||||
|
# Панель управления
|
||||||
|
control_panel = QHBoxLayout()
|
||||||
|
control_panel.addStretch()
|
||||||
|
layout.addLayout(control_panel)
|
||||||
|
|
||||||
|
# Таблица компаний
|
||||||
|
self.companies_table = QTableWidget()
|
||||||
|
self.companies_table.setColumnCount(3)
|
||||||
|
self.companies_table.setHorizontalHeaderLabels(["ID", "Название компании", "Кол-во моделей"])
|
||||||
|
self.companies_table.setSortingEnabled(True)
|
||||||
|
|
||||||
|
# Настройка размеров столбцов
|
||||||
|
header = self.companies_table.horizontalHeader()
|
||||||
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||||
|
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
|
layout.addWidget(self.companies_table)
|
||||||
|
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def create_models_tab(self) -> QWidget:
|
||||||
|
"""Создание вкладки моделей"""
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
|
||||||
|
# Панель поиска
|
||||||
|
search_panel = QHBoxLayout()
|
||||||
|
|
||||||
|
self.search_edit = QLineEdit()
|
||||||
|
self.search_edit.setPlaceholderText("🔍 Поиск по названию, компании, RAM...")
|
||||||
|
self.search_edit.textChanged.connect(self.search_models)
|
||||||
|
search_panel.addWidget(self.search_edit)
|
||||||
|
|
||||||
|
layout.addLayout(search_panel)
|
||||||
|
|
||||||
|
# Таблица моделей
|
||||||
|
self.models_table = QTableWidget()
|
||||||
|
self.models_table.setColumnCount(11)
|
||||||
|
self.models_table.setHorizontalHeaderLabels([
|
||||||
|
"ID", "Компания", "Модель", "RAM", "Батарея",
|
||||||
|
"Экран", "Год", "Регионов с ценами", "Действия", "", ""
|
||||||
|
])
|
||||||
|
self.models_table.setSortingEnabled(True)
|
||||||
|
|
||||||
|
# Настройка размеров столбцов
|
||||||
|
header = self.models_table.horizontalHeader()
|
||||||
|
# ID - минимальный размер
|
||||||
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
# Компания и Модель - расширяемые
|
||||||
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||||
|
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||||
|
# Характеристики - по содержимому
|
||||||
|
for i in range(3, 8):
|
||||||
|
header.setSectionResizeMode(i, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
# Кнопки действий - фиксированные
|
||||||
|
for i in range(8, 11):
|
||||||
|
header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed)
|
||||||
|
header.resizeSection(i, 120)
|
||||||
|
|
||||||
|
layout.addWidget(self.models_table)
|
||||||
|
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def create_analytics_tab(self) -> QWidget:
|
||||||
|
"""Создание вкладки аналитики"""
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
|
||||||
|
# Статистика по регионам
|
||||||
|
stats_group = QGroupBox("📊 Статистика цен по регионам")
|
||||||
|
stats_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
self.stats_text = QTextEdit()
|
||||||
|
self.stats_text.setReadOnly(True)
|
||||||
|
self.stats_text.setFont(QFont("Consolas", 10))
|
||||||
|
stats_layout.addWidget(self.stats_text)
|
||||||
|
|
||||||
|
refresh_stats_btn = QPushButton("🔄 Обновить статистику")
|
||||||
|
refresh_stats_btn.clicked.connect(self.update_statistics)
|
||||||
|
stats_layout.addWidget(refresh_stats_btn)
|
||||||
|
|
||||||
|
stats_group.setLayout(stats_layout)
|
||||||
|
layout.addWidget(stats_group)
|
||||||
|
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def connect_to_db(self):
|
||||||
|
"""Подключение к БД"""
|
||||||
|
if self.db.connect():
|
||||||
|
self.status_bar.showMessage("✅ Подключено к БД")
|
||||||
|
self.refresh_data()
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к БД!")
|
||||||
|
|
||||||
|
def refresh_data(self):
|
||||||
|
"""Обновление всех данных"""
|
||||||
|
self.load_companies()
|
||||||
|
self.load_models()
|
||||||
|
self.update_statistics()
|
||||||
|
self.status_bar.showMessage("✅ Данные обновлены")
|
||||||
|
|
||||||
|
def load_companies(self):
|
||||||
|
"""Загрузка списка компаний"""
|
||||||
|
companies = self.db.get_all_companies()
|
||||||
|
self.companies_table.setRowCount(len(companies))
|
||||||
|
|
||||||
|
for row, company in enumerate(companies):
|
||||||
|
# ID
|
||||||
|
id_item = QTableWidgetItem(str(company['company_id']))
|
||||||
|
id_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.companies_table.setItem(row, 0, id_item)
|
||||||
|
|
||||||
|
# Название
|
||||||
|
name_item = QTableWidgetItem(company['company_name'])
|
||||||
|
name_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
self.companies_table.setItem(row, 1, name_item)
|
||||||
|
|
||||||
|
# Количество моделей
|
||||||
|
count_item = QTableWidgetItem(str(company['models_count']))
|
||||||
|
count_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.companies_table.setItem(row, 2, count_item)
|
||||||
|
|
||||||
|
def load_models(self, search_text=""):
|
||||||
|
"""Загрузка списка моделей"""
|
||||||
|
if search_text:
|
||||||
|
models = self.db.search_models(search_text)
|
||||||
|
else:
|
||||||
|
models = self.db.get_all_models()
|
||||||
|
|
||||||
|
self.models_table.setRowCount(len(models))
|
||||||
|
|
||||||
|
for row, model in enumerate(models):
|
||||||
|
# ID
|
||||||
|
id_item = QTableWidgetItem(str(model['model_id']))
|
||||||
|
id_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.models_table.setItem(row, 0, id_item)
|
||||||
|
|
||||||
|
# Компания
|
||||||
|
company_item = QTableWidgetItem(model['company_name'])
|
||||||
|
company_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
self.models_table.setItem(row, 1, company_item)
|
||||||
|
|
||||||
|
# Модель
|
||||||
|
model_item = QTableWidgetItem(model['model_name'])
|
||||||
|
model_item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||||
|
self.models_table.setItem(row, 2, model_item)
|
||||||
|
|
||||||
|
# RAM
|
||||||
|
ram_item = QTableWidgetItem(model.get('ram', ''))
|
||||||
|
ram_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.models_table.setItem(row, 3, ram_item)
|
||||||
|
|
||||||
|
# Батарея
|
||||||
|
battery_item = QTableWidgetItem(model.get('battery_capacity', ''))
|
||||||
|
battery_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.models_table.setItem(row, 4, battery_item)
|
||||||
|
|
||||||
|
# Экран
|
||||||
|
screen_item = QTableWidgetItem(model.get('screen_size', ''))
|
||||||
|
screen_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.models_table.setItem(row, 5, screen_item)
|
||||||
|
|
||||||
|
# Год
|
||||||
|
year_item = QTableWidgetItem(str(model.get('launched_year', '')))
|
||||||
|
year_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.models_table.setItem(row, 6, year_item)
|
||||||
|
|
||||||
|
# Регионов с ценами
|
||||||
|
regions_item = QTableWidgetItem(str(model.get('price_regions', 0)))
|
||||||
|
regions_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.models_table.setItem(row, 7, regions_item)
|
||||||
|
|
||||||
|
# Кнопки действий
|
||||||
|
edit_btn = QPushButton("✏️ Редакт.")
|
||||||
|
edit_btn.clicked.connect(lambda checked, mid=model['model_id']: self.edit_model(mid))
|
||||||
|
self.models_table.setCellWidget(row, 8, edit_btn)
|
||||||
|
|
||||||
|
price_btn = QPushButton("💰 Цены")
|
||||||
|
price_btn.clicked.connect(
|
||||||
|
lambda checked, mid=model['model_id'], name=model['model_name']:
|
||||||
|
self.manage_prices(mid, name)
|
||||||
|
)
|
||||||
|
self.models_table.setCellWidget(row, 9, price_btn)
|
||||||
|
|
||||||
|
delete_btn = QPushButton("🗑️ Удалить")
|
||||||
|
delete_btn.clicked.connect(lambda checked, mid=model['model_id']: self.delete_model(mid))
|
||||||
|
self.models_table.setCellWidget(row, 10, delete_btn)
|
||||||
|
|
||||||
|
def add_company(self):
|
||||||
|
"""Добавление новой компании"""
|
||||||
|
name, ok = QInputDialog.getText(self, "Новая компания", "Введите название компании:")
|
||||||
|
if ok and name:
|
||||||
|
try:
|
||||||
|
self.db.add_company(name)
|
||||||
|
self.refresh_data()
|
||||||
|
QMessageBox.information(self, "Успех", f"Компания '{name}' добавлена!")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Ошибка", f"Ошибка при добавлении: {str(e)}")
|
||||||
|
|
||||||
|
def add_model(self):
|
||||||
|
"""Добавление новой модели"""
|
||||||
|
dialog = ModelDialog(self)
|
||||||
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
try:
|
||||||
|
model_data = dialog.get_data()
|
||||||
|
self.db.add_model(model_data)
|
||||||
|
self.refresh_data()
|
||||||
|
QMessageBox.information(self, "Успех", "Модель добавлена!")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Ошибка", f"Ошибка при добавлении: {str(e)}")
|
||||||
|
|
||||||
|
def edit_model(self, model_id):
|
||||||
|
"""Редактирование модели"""
|
||||||
|
model_data = self.db.get_model_by_id(model_id)
|
||||||
|
dialog = ModelDialog(self, model_data)
|
||||||
|
|
||||||
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
try:
|
||||||
|
updated_data = dialog.get_data()
|
||||||
|
self.db.update_model(model_id, updated_data)
|
||||||
|
self.refresh_data()
|
||||||
|
QMessageBox.information(self, "Успех", "Модель обновлена!")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Ошибка", f"Ошибка при обновлении: {str(e)}")
|
||||||
|
|
||||||
|
def delete_model(self, model_id):
|
||||||
|
"""Удаление модели"""
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self, "Подтверждение",
|
||||||
|
"Вы уверены, что хотите удалить эту модель?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||||
|
)
|
||||||
|
|
||||||
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
|
try:
|
||||||
|
self.db.delete_model(model_id)
|
||||||
|
self.refresh_data()
|
||||||
|
QMessageBox.information(self, "Успех", "Модель удалена!")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Ошибка", f"Ошибка при удалении: {str(e)}")
|
||||||
|
|
||||||
|
def manage_prices(self, model_id, model_name):
|
||||||
|
"""Управление ценами модели"""
|
||||||
|
dialog = PriceDialog(self, model_id, model_name)
|
||||||
|
dialog.exec()
|
||||||
|
self.refresh_data()
|
||||||
|
|
||||||
|
def search_models(self, text):
|
||||||
|
"""Поиск моделей"""
|
||||||
|
self.load_models(text)
|
||||||
|
|
||||||
|
def update_statistics(self):
|
||||||
|
"""Обновление статистики"""
|
||||||
|
try:
|
||||||
|
stats = self.db.get_price_statistics()
|
||||||
|
|
||||||
|
stats_text = "📊 СТАТИСТИКА ЦЕН ПО РЕГИОНАМ\n" + "="*60 + "\n\n"
|
||||||
|
|
||||||
|
for stat in stats:
|
||||||
|
region_name = stat['region_name']
|
||||||
|
currency_code, currency_symbol = CURRENCY_MAP.get(region_name, ('USD', '$'))
|
||||||
|
|
||||||
|
stats_text += f"🌍 {region_name} ({currency_code}):\n"
|
||||||
|
stats_text += f" • Моделей с ценами: {stat['models_count']}\n"
|
||||||
|
stats_text += f" • Средняя цена: {currency_symbol}{stat['avg_price']:,.2f}\n"
|
||||||
|
stats_text += f" • Минимальная цена: {currency_symbol}{stat['min_price']:,.2f}\n"
|
||||||
|
stats_text += f" • Максимальная цена: {currency_symbol}{stat['max_price']:,.2f}\n\n"
|
||||||
|
|
||||||
|
self.stats_text.setText(stats_text)
|
||||||
|
except Exception as e:
|
||||||
|
self.stats_text.setText(f"Ошибка при загрузке статистики: {str(e)}")
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Обработка закрытия окна"""
|
||||||
|
self.db.disconnect()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# Настройки приложения
|
||||||
|
app.setApplicationName("Mobile Devices Manager")
|
||||||
|
app.setOrganizationName("Moscow Polytech")
|
||||||
|
|
||||||
|
# Устанавливаем стиль
|
||||||
|
app.setStyle('Fusion')
|
||||||
|
|
||||||
|
# Создаем и показываем главное окно
|
||||||
|
window = MainWindow()
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Запускаем цикл обработки событий
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Add table
Reference in a new issue