From 12828b268851dd07f324292b756638871a3905dc Mon Sep 17 00:00:00 2001 From: Thastertyn Date: Thu, 9 Jan 2025 21:43:15 +0100 Subject: [PATCH] [main] Tweaks to design and bringed back sqlalchemy --- src/app.py | 9 +++-- src/ui/book_editor/book_editor.py | 43 ++++++++++++++------ src/ui/dashboard/book_card.py | 65 +++++++++++++++++++++++-------- src/ui/dashboard/dashboard.py | 50 ++++++++++++++---------- src/ui/settings.py | 6 --- src/ui/window.py | 3 +- src/utils/database.py | 30 ++++++++------ 7 files changed, 135 insertions(+), 71 deletions(-) diff --git a/src/app.py b/src/app.py index 53c9378..e0e0e51 100644 --- a/src/app.py +++ b/src/app.py @@ -1,6 +1,7 @@ import sys import os import logging +from typing import Optional from PySide6.QtWidgets import QMessageBox, QApplication @@ -13,14 +14,14 @@ from ui.window import LibraryWindow class LibraryApp(): - def __init__(self, user_config: UserConfig | None = None): + def __init__(self, user_config: Optional[UserConfig] = None): setup_logger() self.logger = logging.getLogger(__name__) self.logger.info("Starting") - + self.qt_app = QApplication([]) self.user_config = user_config - + try: self.database_manger = DatabaseManager() except DatabaseConfigError as e: @@ -37,7 +38,7 @@ class LibraryApp(): self.window.show() status = self.qt_app.exec() self.cleanup() - self.logger.info("Exitting") + self.logger.info("Exiting") return status def show_error(self, text: str, detail_text: str = ""): diff --git a/src/ui/book_editor/book_editor.py b/src/ui/book_editor/book_editor.py index fb321af..1083ac0 100644 --- a/src/ui/book_editor/book_editor.py +++ b/src/ui/book_editor/book_editor.py @@ -1,19 +1,40 @@ from PySide6.QtGui import QGuiApplication, QAction from PySide6.QtQml import QQmlApplicationEngine from PySide6 import QtWidgets, QtCore +from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel -class BookEditor(QtWidgets.QWidget): - def __init__(self): - super().__init__() +from models.book_overview import BooksOverview - # Central widget and layout - main_layout = QtWidgets.QVBoxLayout(self) +class BookEditor(QDialog): + def __init__(self, book_overview: BooksOverview, parent=None): + super().__init__(parent) - # Title label - title_label = QtWidgets.QLabel("Books", self) - title_label.setAlignment(QtCore.Qt.AlignCenter) - title_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #0078D4;") - main_layout.addWidget(title_label) + self.setWindowTitle("Edit a book") + self.setMinimumSize(400, 300) - self.setLayout(main_layout) + # Create layout + layout = QVBoxLayout(self) + data_mode_layout = QHBoxLayout() + + self.data_mode_label = QLabel("Data mode:") + data_mode_layout.addWidget(self.data_mode_label) + + + + # Buttons + button_layout = QtWidgets.QHBoxLayout() + + self.save_button = QtWidgets.QPushButton("Save") + self.save_button.clicked.connect(self.save_book) + button_layout.addWidget(self.save_button) + + self.cancel_button = QtWidgets.QPushButton("Discard") + self.cancel_button.clicked.connect(self.reject) + button_layout.addWidget(self.cancel_button) + + layout.addLayout(button_layout) + + + def save_book(self): + pass \ No newline at end of file diff --git a/src/ui/dashboard/book_card.py b/src/ui/dashboard/book_card.py index 2d166ae..bd2f9ac 100644 --- a/src/ui/dashboard/book_card.py +++ b/src/ui/dashboard/book_card.py @@ -1,9 +1,11 @@ -from PySide6.QtGui import QGuiApplication, QAction +from PySide6.QtGui import QGuiApplication, QAction, Qt from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QWidget, QMenu, QSizePolicy, QLayout +from PySide6.QtCore import qDebug + +from ui.book_editor.book_editor import BookEditor from models.book import BookStatusEnum - from models.book_overview import BooksOverview STATUS_TO_COLOR_MAP = { @@ -11,22 +13,36 @@ STATUS_TO_COLOR_MAP = { BookStatusEnum.borrowed: "#702525", BookStatusEnum.reserved: "#bc7613" } + class BookCard(QWidget): def __init__(self, book_overview: BooksOverview): super().__init__() - # Create the layout for the card - layout = QHBoxLayout(self) - layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize) # Ensure minimum size is respected - self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) # Avoid vertical stretching + self.book_overview = book_overview - layout.setContentsMargins(10, 10, 10, 10) # Optional: Add margins for spacing + self.setAttribute(Qt.WidgetAttribute.WA_Hover, True) # Enable hover events + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) # Enable styling for background + + # Set initial stylesheet with hover behavior + self.setStyleSheet(""" + BookCard:hover { + background-color: palette(highlight); + } + """) + + # Layout setup + layout = QHBoxLayout(self) + layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize) + self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + + layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(10) # Left-side content left_side = QVBoxLayout() layout.addLayout(left_side) title_label = QLabel(book_overview.title) + title_label.setStyleSheet("font-size: 20px; font-weight: bold;") author_label = QLabel("By: " + book_overview.author_name) isbn_label = QLabel("ISBN: " + (book_overview.isbn or "Not Available")) left_side.addWidget(title_label) @@ -36,33 +52,50 @@ class BookCard(QWidget): # Right-side content right_side = QVBoxLayout() layout.addLayout(right_side) - status_label = QLabel(str(book_overview.status)) - status_label.setStyleSheet(f"color: {STATUS_TO_COLOR_MAP[book_overview.status]}") + + status_label = QLabel(str(book_overview.status.value.capitalize())) + status_label.setStyleSheet(f"color: {STATUS_TO_COLOR_MAP[book_overview.status]}; font-size: 20px; font-weight: bold;") + status_label.setAlignment(Qt.AlignmentFlag.AlignRight) + right_side.addWidget(status_label) if book_overview.librarian_name and book_overview.borrower_name: borrower_label = QLabel("Borrowed: " + book_overview.borrower_name) + borrower_label.setAlignment(Qt.AlignmentFlag.AlignRight) + librarian_label = QLabel("By: " + book_overview.librarian_name) + librarian_label.setAlignment(Qt.AlignmentFlag.AlignRight) + right_side.addWidget(borrower_label) right_side.addWidget(librarian_label) self.setLayout(layout) + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self.contextMenuEvent(event) + else: + super().mousePressEvent(event) def contextMenuEvent(self, event): - """Override to create a custom right-click menu.""" context_menu = QMenu(self) - # Add actions to the menu action_edit_book = context_menu.addAction("Edit Book") action_edit_author = context_menu.addAction("Edit Author") - action_mark_returned = context_menu.addAction("Mark as Returned") - # Execute the menu and get the selected action + if self.book_overview.status == BookStatusEnum.borrowed: + action_mark_returned = context_menu.addAction("Mark as Returned") + + if self.book_overview.status == BookStatusEnum.reserved: + action_remove_reservation = context_menu.addAction("Remove reservation") + action = context_menu.exec_(self.mapToGlobal(event.pos())) + if action == action_edit_book: - self.label.setText("Edit Book selected") + BookEditor(self.book_overview).exec() elif action == action_edit_author: - self.label.setText("Edit Author selected") + print("Edit Author selected") elif action == action_mark_returned: - self.label.setText("Mark as Returned selected") \ No newline at end of file + print("Mark as Returned selected") + elif action == action_remove_reservation: + print("Remove reservation selected") \ No newline at end of file diff --git a/src/ui/dashboard/dashboard.py b/src/ui/dashboard/dashboard.py index 990a4be..d149fea 100644 --- a/src/ui/dashboard/dashboard.py +++ b/src/ui/dashboard/dashboard.py @@ -1,15 +1,16 @@ from PySide6.QtGui import QAction from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QScrollArea, - QFrame, QPushButton, QMessageBox, QVBoxLayout, QScrollArea + QFrame, QPushButton, QMessageBox, QVBoxLayout ) from PySide6.QtCore import Qt from .book_card import BookCard from models.book_overview import BooksOverview -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from utils.database import DatabaseManager + + class LibraryDashboard(QWidget): def __init__(self): super().__init__() @@ -20,7 +21,8 @@ class LibraryDashboard(QWidget): # Title label title_label = QLabel("Dashboard", self) title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #0078D4;") + title_label.setStyleSheet( + "font-size: 20px; font-weight: bold; color: #0078D4;") main_layout.addWidget(title_label) # Search bar @@ -36,17 +38,21 @@ class LibraryDashboard(QWidget): # Container widget for the scroll area self.scroll_widget = QWidget() self.scroll_layout = QVBoxLayout(self.scroll_widget) - self.scroll_layout.setSpacing(5) # Optional: Adjust spacing between cards - self.scroll_layout.setContentsMargins(0, 0, 0, 0) + self.scroll_layout.setSpacing(5) # Set gap between individual cards + self.scroll_layout.setContentsMargins(0, 0, 0, 0) # Remove spacing from all sides which is present by default + + # Align the cards to the top + self.scroll_layout.setAlignment(Qt.AlignTop) # Example cards - # self.books = self.fetch_books_from_db() - # self.book_cards = [] + self.books = self.fetch_books_from_db() + self.book_cards = [] - # for book in self.books: - # card = BookCard(book) - # self.scroll_layout.addWidget(card) - # self.book_cards.append(card) + for book in self.books: + card = BookCard(book) + + self.scroll_layout.addWidget(card) + self.book_cards.append(card) self.scroll_widget.setLayout(self.scroll_layout) self.scroll_area.setWidget(self.scroll_widget) @@ -67,21 +73,23 @@ class LibraryDashboard(QWidget): def filter_books(self, text): """Filter the cards based on the search input.""" for card, book in zip(self.book_cards, self.books): - card.setVisible(text.lower() in book["title"].lower() or text.lower() in book["description"].lower()) + card.setVisible(text.lower() in book.title.lower()) def register_member(self): - QMessageBox.information(self, "Add Member", "Open dialog to register a new member.") + QMessageBox.information( + self, "Add Member", "Open dialog to register a new member.") def add_borrow_record(self): - QMessageBox.information(self, "Add Borrow Record", "Open dialog to add a borrow record.") + QMessageBox.information(self, "Add Borrow Record", + "Open dialog to add a borrow record.") def fetch_books_from_db(self): - """Fetch all books from the database.""" try: - session = SessionLocal() - books = session.query(BooksOverview).all() - return books + with DatabaseManager.get_session() as session: + books = session.query(BooksOverview).all() + return books except Exception as e: - QMessageBox.critical(self, "Database Error", f"Failed to fetch books: {e}") - return [] \ No newline at end of file + QMessageBox.critical(self, "Database Error", + f"Failed to fetch books: {e}") + return [] diff --git a/src/ui/settings.py b/src/ui/settings.py index 0ab0554..1396aa7 100644 --- a/src/ui/settings.py +++ b/src/ui/settings.py @@ -11,12 +11,6 @@ class SettingsDialog(QtWidgets.QDialog): self.setWindowTitle("Settings") self.setMinimumSize(400, 100) - # Position the dialog relative to the parent (main window) - if parent: - x = parent.geometry().x() + parent.geometry().width() // 2 - self.width() // 2 - y = parent.geometry().y() + parent.geometry().height() // 2 - self.height() // 2 - self.move(x, y) - # Create layout layout = QtWidgets.QVBoxLayout(self) diff --git a/src/ui/window.py b/src/ui/window.py index 3028367..150c72d 100644 --- a/src/ui/window.py +++ b/src/ui/window.py @@ -26,7 +26,7 @@ class LibraryWindow(QtWidgets.QMainWindow): self.setCentralWidget(central_widget) central_widget.addTab(LibraryDashboard(), "Dashboard") - central_widget.addTab(BookEditor(), "Books") + # central_widget.addTab(BookEditor(), "Books") central_widget.addTab(MemberEditor(), "Members") @@ -74,6 +74,7 @@ class LibraryWindow(QtWidgets.QMainWindow): # Edit menu edit_menu = menu_bar.addMenu("Edit") + # Preferences menu preferences_action = QAction("Preferences", self) preferences_action.setShortcut("Ctrl+,") preferences_action.triggered.connect(self.edit_preferences) diff --git a/src/utils/database.py b/src/utils/database.py index 859b6f0..2400b4a 100644 --- a/src/utils/database.py +++ b/src/utils/database.py @@ -1,27 +1,33 @@ import logging -from mysql.connector import connect, cursor +from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy import create_engine from utils.config import DatabaseConfig from utils.errors.database_config_error import DatabaseConfigError class DatabaseManager(): + + _instance: 'DatabaseManager' = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + def __init__(self) -> None: self.logger = logging.getLogger(__name__) self.logger.info("Reading database config") self.database_config = DatabaseConfig() - self.connection = connect( - host=self.database_config.host, - port=self.database_config.port, - database=self.database_config.name, - user=self.database_config.user, - password=self.database_config.password - ) - - def get_cursor(self) -> cursor.MySQLCursorAbstract: - return self.connection.cursor(dictionary=True) + self.engine = create_engine(f'mysql+mysqlconnector://{self.database_config.user}:{ + self.database_config.password}@{self.database_config.host}/{self.database_config.name}') + self.session_local = sessionmaker(bind=self.engine) def cleanup(self) -> None: self.logger.debug("Closing connection") - self.connection.close() + self.engine.dispose() + + @classmethod + def get_session(cls) -> Session: + return DatabaseManager._instance.session_local()