From e452822ffc16eeb5125385e2b9e953bbb124047f Mon Sep 17 00:00:00 2001 From: Thastertyn Date: Fri, 17 Jan 2025 15:03:50 +0100 Subject: [PATCH] [main] WIP category tab and showcase of phantom read --- src/database/__init__.py | 2 + src/database/book.py | 34 ++++++- src/database/book_category_statistics.py | 43 ++++++--- ...gory_statistics_overview_model_overview.py | 0 src/database/manager.py | 12 ++- src/database/member.py | 39 +++++++- src/models/__init__.py | 38 ++++---- src/models/{author.py => author_model.py} | 2 +- src/models/{base.py => base_model.py} | 0 ...ry_link.py => book_category_link_model.py} | 6 +- ...ook_category.py => book_category_model.py} | 2 +- ...s.py => book_category_statistics_model.py} | 4 +- ...book_category_statistics_overview_model.py | 18 ++++ src/models/{book.py => book_model.py} | 2 +- ...ook_overview.py => book_overview_model.py} | 5 +- .../{librarian.py => librarian_model.py} | 2 +- src/models/{loan.py => loan_model.py} | 8 +- src/models/{member.py => member_model.py} | 4 +- src/services/book_service.py | 2 - src/ui/editor/book_editor.py | 2 +- src/ui/editor/member_editor.py | 30 +++--- .../main_tabs/book_overview_list/book_card.py | 3 +- .../book_overview_list/overview_list.py | 6 +- .../__init__.py | 0 .../category_overview_card.py | 58 +++++++++++ .../category_overview_list.py | 96 +++++++++++++++++++ src/ui/main_tabs/member_list/member_card.py | 27 +++--- src/ui/main_tabs/member_list/member_list.py | 14 +-- src/ui/menu_bar.py | 12 ++- src/ui/window.py | 2 +- src/utils/config.py | 10 +- src/utils/errors/database.py | 3 +- 32 files changed, 374 insertions(+), 112 deletions(-) create mode 100644 src/database/book_category_statistics_overview_model_overview.py rename src/models/{author.py => author_model.py} (96%) rename src/models/{base.py => base_model.py} (100%) rename src/models/{book_category_link.py => book_category_link_model.py} (84%) rename src/models/{book_category.py => book_category_model.py} (96%) rename src/models/{book_category_statistics.py => book_category_statistics_model.py} (83%) create mode 100644 src/models/book_category_statistics_overview_model.py rename src/models/{book.py => book_model.py} (98%) rename src/models/{book_overview.py => book_overview_model.py} (95%) rename src/models/{librarian.py => librarian_model.py} (97%) rename src/models/{loan.py => loan_model.py} (91%) rename src/models/{member.py => member_model.py} (88%) create mode 100644 src/ui/main_tabs/category_statistics_overview_list/__init__.py create mode 100644 src/ui/main_tabs/category_statistics_overview_list/category_overview_card.py create mode 100644 src/ui/main_tabs/category_statistics_overview_list/category_overview_list.py diff --git a/src/database/__init__.py b/src/database/__init__.py index f873297..cd6d5e1 100644 --- a/src/database/__init__.py +++ b/src/database/__init__.py @@ -1,11 +1,13 @@ from .manager import * from .book import * +from .book_category_statistics import * from .member import * from .book_overview import * __all__ = [ *manager.__all__, *book.__all__, + *book_category_statistics.__all__, *book_overview.__all__, *member.__all__, ] \ No newline at end of file diff --git a/src/database/book.py b/src/database/book.py index 439cc01..61c3335 100644 --- a/src/database/book.py +++ b/src/database/book.py @@ -1,14 +1,19 @@ from typing import Dict, List, Optional import logging +import time from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError as SqlAlchemyDatabaseError from sqlalchemy.orm import joinedload +from sqlalchemy import delete from utils.errors.database import DatabaseError, DuplicateEntryError, DatabaseConnectionError from models import Book from database.manager import DatabaseManager from .author import get_or_create_author from .book_category import get_or_create_categories +from .book_category_statistics import update_category_statistics + +from utils.config import UserConfig logger = logging.getLogger(__name__) @@ -83,9 +88,22 @@ def create_books(books: List[Dict[str, object]]) -> None: categories=categories ) session.add(new_book) + + user_config = UserConfig() - session.commit() - logger.info(f"Book {book['title']} successfully created.") + if user_config.simulate_slowdown: + logger.debug("Simulating slowdown before updating statistics for 10 seconds") + time.sleep(10) + else: + logger.debug("Performing category statistics update normally") + + update_category_statistics(session) + + session.commit() + # logger.info(f"Book {book['title']} successfully created.") + + logger.debug("Committing all changes") + session.commit() except IntegrityError as e: logger.warning("Data already exists") raise DuplicateEntryError("Data already exists in the database") from e @@ -142,5 +160,17 @@ def update_book(book: Dict[str, object]) -> None: logger.error(f"An error occurred when updating the book: {e}") raise DatabaseError("An error occurred when updating the book") from e +def delete_book(book_id: int) -> None: + try: + with DatabaseManager.get_session() as session: + stmt = delete(Book).where(Book.id == book_id) + session.execute(stmt) + session.commit() + except SqlAlchemyDatabaseError as e: + logger.critical("Connection with database interrupted") + raise DatabaseConnectionError("Connection with database interrupted") from e + except SQLAlchemyError as e: + logger.error(f"An error occurred when updating the book: {e}") + raise DatabaseError("An error occurred when updating the book") from e __all__ = ["create_book", "create_books", "update_book", "fetch_all_books"] diff --git a/src/database/book_category_statistics.py b/src/database/book_category_statistics.py index 4428495..cb93359 100644 --- a/src/database/book_category_statistics.py +++ b/src/database/book_category_statistics.py @@ -1,23 +1,42 @@ from sqlalchemy.orm import Session -from sqlalchemy import update +from sqlalchemy import func -from models import BookCategoryStatistics, Book +from models import BookCategoryStatistics, BookCategoryLink -def update_category_statistics(session: Session, book_id: int): - book = session.query(Book).filter_by(id=book_id).first() +def update_category_statistics(session: Session) -> None: + """ + Updates category statistics by calculating the count of books in each category. - if not book: - raise ValueError(f"Book with ID {book_id} does not exist.") + :param session: SQLAlchemy session object. + """ + # Calculate the book count for each category using a query + category_counts = ( + session.query( + BookCategoryLink.book_category_id, + func.count(BookCategoryLink.book_id).label('book_count') + ) + .group_by(BookCategoryLink.book_category_id) + .all() + ) - for category in book.categories: - statistics = session.query(BookCategoryStatistics).filter_by(category_id=category.id).one_or_none() + # Update or create statistics based on the query results + for category_id, book_count in category_counts: + existing_statistics = ( + session.query(BookCategoryStatistics) + .filter_by(book_category_id=category_id) + .one_or_none() + ) - if statistics: - statistics.book_count += 1 - session.add(statistics) + if existing_statistics: + # Update the existing count + existing_statistics.book_count = book_count else: - new_statistics = BookCategoryStatistics(category_id=category.id, book_count=1) + # Create new statistics for the category + new_statistics = BookCategoryStatistics( + book_category_id=category_id, + book_count=book_count + ) session.add(new_statistics) diff --git a/src/database/book_category_statistics_overview_model_overview.py b/src/database/book_category_statistics_overview_model_overview.py new file mode 100644 index 0000000..e69de29 diff --git a/src/database/manager.py b/src/database/manager.py index 558a079..b50c0a6 100644 --- a/src/database/manager.py +++ b/src/database/manager.py @@ -5,7 +5,7 @@ from sqlalchemy import create_engine, text from sqlalchemy.exc import DatabaseError -from utils.config import DatabaseConfig +from utils.config import DatabaseConfig, UserConfig from utils.errors.database import DatabaseConnectionError @@ -49,6 +49,14 @@ class DatabaseManager(): @classmethod def get_session(cls) -> Session: - return DatabaseManager._instance.Session() + user_config = UserConfig() + + # Get the transaction level as a string (e.g., "READ COMMITTED", "SERIALIZABLE") + isolation_level = user_config.transaction_level.value + + # Create a session with the appropriate transaction isolation level + session = cls._instance.Session() + session.connection(execution_options={"isolation_level": isolation_level}) + return session __all__ = ["DatabaseManager"] \ No newline at end of file diff --git a/src/database/member.py b/src/database/member.py index f1a6cb3..ff100e1 100644 --- a/src/database/member.py +++ b/src/database/member.py @@ -2,6 +2,7 @@ import logging from typing import List, Dict from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError as SqlAlchemyDatabaseError +from sqlalchemy import delete from utils.errors.database import DatabaseError, DuplicateEntryError, DatabaseConnectionError from models import Member @@ -33,12 +34,29 @@ def create_member(new_member: Dict[str, str]): def create_members(members: List[Dict[str, str]]): try: with DatabaseManager.get_session() as session: - session.add_all(members) + for member_dict in members: + member = Member( + first_name=member_dict["first_name"], + last_name=member_dict["last_name"], + email=member_dict["email"], + phone=member_dict["phone_number"] + ) + session.add(member) + session.commit() except IntegrityError as e: session.rollback() - logger.warning("Data already exists") - raise DuplicateEntryError("Data already exists in the database") from e + + if "email" in str(e.orig): + logger.warning("Email is already in use") + raise DuplicateEntryError("Email", "Email is already in use") from e + elif "phone" in str(e.orig): + logger.warning("Phone number is already in use") + raise DuplicateEntryError("Phone number", "Phone number is already in use") from e + else: + logger.error("Member exists already in the database") + raise DatabaseError("Member exists already") from e + except DatabaseError as e: session.rollback() logger.critical("Connection with database interrupted") @@ -51,4 +69,17 @@ def create_members(members: List[Dict[str, str]]): def update_member(member: Dict[str, str]): pass -__all__ = ["create_member", "create_members", "fetch_all_members"] +def delete_member(member_id: int) -> None: + try: + with DatabaseManager.get_session() as session: + stmt = delete(Member).where(Member.id == member_id) + session.execute(stmt) + session.commit() + except SqlAlchemyDatabaseError as e: + logger.critical("Connection with database interrupted") + raise DatabaseConnectionError("Connection with database interrupted") from e + except SQLAlchemyError as e: + logger.error(f"An error occurred when deleting member: {e}") + raise DatabaseError("An error occurred when deleting member") from e + +__all__ = ["create_member", "create_members", "fetch_all_members", "delete_member"] diff --git a/src/models/__init__.py b/src/models/__init__.py index 70d192d..490c7b5 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -1,21 +1,23 @@ -from .author import * -from .book import * -from .book_category import * -from .book_category_link import * -from .book_category_statistics import * -from .book_overview import * -from .member import * -from .librarian import * -from .loan import * +from .author_model import * +from .book_model import * +from .book_category_model import * +from .book_category_link_model import * +from .book_category_statistics_model import * +from .book_category_statistics_overview_model import * +from .book_overview_model import * +from .member_model import * +from .librarian_model import * +from .loan_model import * __all__ = [ - *author.__all__, - *book.__all__, - *book_category.__all__, - *book_category_link.__all__, - *book_category_statistics.__all__, - *book_overview.__all__, - *member.__all__, - *librarian.__all__, - *loan.__all__ + *author_model.__all__, + *book_model.__all__, + *book_category_model.__all__, + *book_category_link_model.__all__, + *book_category_statistics_model.__all__, + *book_category_statistics_overview_model.__all__, + *book_overview_model.__all__, + *member_model.__all__, + *librarian_model.__all__, + *loan_model.__all__ ] diff --git a/src/models/author.py b/src/models/author_model.py similarity index 96% rename from src/models/author.py rename to src/models/author_model.py index a15ec32..5a0198a 100644 --- a/src/models/author.py +++ b/src/models/author_model.py @@ -1,7 +1,7 @@ from sqlalchemy import Column, Integer, String, TIMESTAMP, UniqueConstraint, func from sqlalchemy.orm import relationship -from .base import Base +from .base_model import Base class Author(Base): diff --git a/src/models/base.py b/src/models/base_model.py similarity index 100% rename from src/models/base.py rename to src/models/base_model.py diff --git a/src/models/book_category_link.py b/src/models/book_category_link_model.py similarity index 84% rename from src/models/book_category_link.py rename to src/models/book_category_link_model.py index 27be8b8..121d07c 100644 --- a/src/models/book_category_link.py +++ b/src/models/book_category_link_model.py @@ -1,9 +1,9 @@ from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy.orm import relationship -from .book import Book -from .book_category import BookCategory -from .base import Base +from .book_model import Book +from .book_category_model import BookCategory +from .base_model import Base class BookCategoryLink(Base): diff --git a/src/models/book_category.py b/src/models/book_category_model.py similarity index 96% rename from src/models/book_category.py rename to src/models/book_category_model.py index 6f5f4b8..fd1b54c 100644 --- a/src/models/book_category.py +++ b/src/models/book_category_model.py @@ -1,7 +1,7 @@ from sqlalchemy import Column, Integer, String, TIMESTAMP, ForeignKey, UniqueConstraint, func from sqlalchemy.orm import relationship -from .base import Base +from .base_model import Base class BookCategory(Base): __tablename__ = 'book_category' diff --git a/src/models/book_category_statistics.py b/src/models/book_category_statistics_model.py similarity index 83% rename from src/models/book_category_statistics.py rename to src/models/book_category_statistics_model.py index fc655be..39791fb 100644 --- a/src/models/book_category_statistics.py +++ b/src/models/book_category_statistics_model.py @@ -3,13 +3,13 @@ from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy.dialects.mysql import INTEGER from sqlalchemy.orm import relationship -from .base import Base +from .base_model import Base class BookCategoryStatistics(Base): __tablename__ = 'book_category_statistics' book_category_id = Column(Integer, ForeignKey('book_category.id', ondelete="cascade"), primary_key=True) - count = Column(INTEGER(unsigned=True), nullable=False, default=0) + book_count = Column(INTEGER(unsigned=True), nullable=False, default=0) category = relationship( 'BookCategory', diff --git a/src/models/book_category_statistics_overview_model.py b/src/models/book_category_statistics_overview_model.py new file mode 100644 index 0000000..518815b --- /dev/null +++ b/src/models/book_category_statistics_overview_model.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, String, TIMESTAMP, Integer, Text, Enum +from sqlalchemy.dialects.mysql import INTEGER + +from .base_model import Base + + +class BookCategoryStatisticsOverview(Base): + __tablename__ = 'book_category_statistics_overview' + __table_args__ = {'extend_existing': True} + + name = Column(String) + book_count = Column(INTEGER(unsigned=True), default=0) + + def __repr__(self): + return (f"") + + +__all__ = ["BookCategoryStatisticsOverview"] diff --git a/src/models/book.py b/src/models/book_model.py similarity index 98% rename from src/models/book.py rename to src/models/book_model.py index acdbdbb..452943b 100644 --- a/src/models/book.py +++ b/src/models/book_model.py @@ -3,7 +3,7 @@ import enum from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, func from sqlalchemy.orm import relationship -from .base import Base +from .base_model import Base class BookStatusEnum(enum.Enum): diff --git a/src/models/book_overview.py b/src/models/book_overview_model.py similarity index 95% rename from src/models/book_overview.py rename to src/models/book_overview_model.py index 3248dd5..635f754 100644 --- a/src/models/book_overview.py +++ b/src/models/book_overview_model.py @@ -1,8 +1,7 @@ from sqlalchemy import Column, String, TIMESTAMP, Integer, Text, Enum -from .base import Base - -from models.book import BookStatusEnum +from .base_model import Base +from .book_model import BookStatusEnum class BooksOverview(Base): __tablename__ = 'books_overview' diff --git a/src/models/librarian.py b/src/models/librarian_model.py similarity index 97% rename from src/models/librarian.py rename to src/models/librarian_model.py index 05179d8..9f108ac 100644 --- a/src/models/librarian.py +++ b/src/models/librarian_model.py @@ -3,7 +3,7 @@ import enum from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, func from sqlalchemy.orm import relationship -from .base import Base +from .base_model import Base class LibrarianStatusEnum(enum.Enum): diff --git a/src/models/loan.py b/src/models/loan_model.py similarity index 91% rename from src/models/loan.py rename to src/models/loan_model.py index 5fdb59e..88526e2 100644 --- a/src/models/loan.py +++ b/src/models/loan_model.py @@ -3,10 +3,10 @@ import enum from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, Float, func from sqlalchemy.orm import relationship -from .base import Base -from .book import Book -from .member import Member -from .librarian import Librarian +from .base_model import Base +from .book_model import Book +from .member_model import Member +from .librarian_model import Librarian class LoanStatusEnum(enum.Enum): diff --git a/src/models/member.py b/src/models/member_model.py similarity index 88% rename from src/models/member.py rename to src/models/member_model.py index 535a15f..8234822 100644 --- a/src/models/member.py +++ b/src/models/member_model.py @@ -3,7 +3,7 @@ import enum from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, func from sqlalchemy.orm import relationship -from .base import Base +from .base_model import Base class MemberStatusEnum(enum.Enum): @@ -13,7 +13,7 @@ class MemberStatusEnum(enum.Enum): class Member(Base): __tablename__ = 'member' - __table_args__ = (UniqueConstraint('id'),) + __table_args__ = (UniqueConstraint('id'), UniqueConstraint('email'), UniqueConstraint('phone')) id = Column(Integer, primary_key=True, autoincrement=True) first_name = Column(String(50), nullable=False) diff --git a/src/services/book_service.py b/src/services/book_service.py index efbf580..da289bf 100644 --- a/src/services/book_service.py +++ b/src/services/book_service.py @@ -17,10 +17,8 @@ from models import Book from database import fetch_all_books, create_books from assets import asset_manager -# Initialize logger and XML Schema logger = logging.getLogger(__name__) - try: logger.debug("Loading XSD schema") SCHEMA = XMLSchema(asset_manager.get_asset("book_import_scheme.xsd")) diff --git a/src/ui/editor/book_editor.py b/src/ui/editor/book_editor.py index 27b2395..e2f9187 100644 --- a/src/ui/editor/book_editor.py +++ b/src/ui/editor/book_editor.py @@ -126,7 +126,7 @@ class BookEditor(QDialog): "Could not connect to the database", QMessageBox.StandardButton.Ok) except DatabaseError as e: - QMessageBox.critical(self.parent, + QMessageBox.critical(None, "An error occurred", f"Could not save the book because of the following error: {e}", QMessageBox.StandardButton.Ok) diff --git a/src/ui/editor/member_editor.py b/src/ui/editor/member_editor.py index bc13b52..678fbd0 100644 --- a/src/ui/editor/member_editor.py +++ b/src/ui/editor/member_editor.py @@ -9,7 +9,7 @@ from PySide6.QtWidgets import QVBoxLayout, QFormLayout, QLineEdit, QHBoxLayout, from models import Member -from database.member import create_new_member, update_member +from database.member import create_member, update_member from utils.errors.database import DatabaseError, DatabaseConnectionError, DuplicateEntryError @@ -86,40 +86,41 @@ class MemberEditor(QDialog): QMessageBox.information(None, "Success", "Member created successfully", - QMessageBox.StandardButton.Ok) + QMessageBox.StandardButton.Ok, + QMessageBox.StandardButton.NoButton) else: member_object["id"] = self.member_id update_member(book_object) QMessageBox.information(None, "Success", "Member updated successfully", - QMessageBox.StandardButton.Ok) - - + QMessageBox.StandardButton.Ok, + QMessageBox.StandardButton.NoButton) + self.accept() except ValueError as e: QMessageBox.critical(None, "Invalid Input", f"Input validation failed: {e}", - QMessageBox.StandardButton.Ok) + QMessageBox.StandardButton.Ok, + QMessageBox.StandardButton.NoButton) except DuplicateEntryError as e: QMessageBox.critical(None, - "ISBN is already in use", - "The ISBN provided is already in use", - QMessageBox.StandardButton.Ok) + f"Duplicate {e.duplicate_entry_name}", + f"The {e.duplicate_entry_name} is already in use", + QMessageBox.StandardButton.Ok, + QMessageBox.StandardButton.NoButton) except DatabaseConnectionError as e: QMessageBox.critical(None, - "Failed to save", + "Connection error", "Could not connect to the database", QMessageBox.StandardButton.Ok) except DatabaseError as e: - QMessageBox.critical(self.parent, - "An error occurred", + QMessageBox.critical(None, + "Unknown database error", f"Could not save the book because of the following error: {e}", QMessageBox.StandardButton.Ok) - self.accept() - def parse_inputs(self) -> Dict: first_name = self.first_name_input.text().strip() if not first_name or len(first_name) > 50: @@ -146,4 +147,5 @@ class MemberEditor(QDialog): "phone_number": phone_number } + __all__ = ["MemberEditor"] diff --git a/src/ui/main_tabs/book_overview_list/book_card.py b/src/ui/main_tabs/book_overview_list/book_card.py index cfe8775..bcd89ee 100644 --- a/src/ui/main_tabs/book_overview_list/book_card.py +++ b/src/ui/main_tabs/book_overview_list/book_card.py @@ -91,8 +91,7 @@ class BookCard(QWidget): 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") - action_remove_reservation = context_menu.addAction( - "Remove reservation") + action_remove_reservation = context_menu.addAction("Remove reservation") context_menu.addSeparator() delete_book_action = context_menu.addAction("Delete Book") delete_book_action.triggered.connect(self.delete_book) diff --git a/src/ui/main_tabs/book_overview_list/overview_list.py b/src/ui/main_tabs/book_overview_list/overview_list.py index 26a74f1..03c82ca 100644 --- a/src/ui/main_tabs/book_overview_list/overview_list.py +++ b/src/ui/main_tabs/book_overview_list/overview_list.py @@ -15,8 +15,9 @@ from ui.editor import MemberEditor class BookOverviewList(QWidget): - def __init__(self): - super().__init__() + def __init__(self, parent = None): + self.parent = parent + super().__init__(parent=parent) # Central widget and layout main_layout = QVBoxLayout(self) @@ -78,6 +79,7 @@ class BookOverviewList(QWidget): def register_member(self): MemberEditor().exec() + self.parent.refresh_member_cards() def add_borrow_record(self): QMessageBox.information(self, "Add Borrow Record", diff --git a/src/ui/main_tabs/category_statistics_overview_list/__init__.py b/src/ui/main_tabs/category_statistics_overview_list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/main_tabs/category_statistics_overview_list/category_overview_card.py b/src/ui/main_tabs/category_statistics_overview_list/category_overview_card.py new file mode 100644 index 0000000..54088db --- /dev/null +++ b/src/ui/main_tabs/category_statistics_overview_list/category_overview_card.py @@ -0,0 +1,58 @@ +from PySide6.QtGui import QGuiApplication, QAction, Qt +from PySide6.QtQml import QQmlApplicationEngine +from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QWidget, QMenu, QSizePolicy, QLayout, QMessageBox +from PySide6.QtCore import qDebug + +from ui.editor import BookEditor + +from models import BookCategoryStatisticsOverview + +from database.manager import DatabaseManager + + +class BookCategoryStatisticsOverviewCard(QWidget): + def __init__(self, book_category_statistics_overview: BookCategoryStatisticsOverview): + super().__init__() + + self.book_category_statistics_overview = book_category_statistics_overview + + self.setAttribute(Qt.WidgetAttribute.WA_Hover, True) # Enable hover events + # Enable styling for background + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + + # Set initial stylesheet with hover behavior + self.setStyleSheet(""" + BookCategoryStatisticsOverviewCard: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) + category_name_label = QLabel(book_category_statistics_overview.name) + category_name_label.setStyleSheet("font-size: 20px; font-weight: bold;") + left_side.addWidget(category_name_label) + + # Right-side content + right_side = QVBoxLayout() + layout.addLayout(right_side) + + status_label = QLabel(book_category_statistics_overview.book_count) + status_label.setStyleSheet(f"font-size: 20px; font-weight: bold;") + status_label.setAlignment(Qt.AlignmentFlag.AlignRight) + + right_side.addWidget(status_label) + + self.setLayout(layout) + +__all__ = ["BookCategoryStatisticsOverviewCard"] diff --git a/src/ui/main_tabs/category_statistics_overview_list/category_overview_list.py b/src/ui/main_tabs/category_statistics_overview_list/category_overview_list.py new file mode 100644 index 0000000..10b8e83 --- /dev/null +++ b/src/ui/main_tabs/category_statistics_overview_list/category_overview_list.py @@ -0,0 +1,96 @@ +from PySide6.QtGui import QAction +from PySide6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QScrollArea, + QFrame, QPushButton, QMessageBox, QVBoxLayout +) +from PySide6.QtCore import Qt + +from .book_card import BookCard +from models import BooksOverview + +from database.manager import DatabaseManager +from database.book_overview import fetch_all_book_overviews + +from ui.editor import MemberEditor + + +class BookCategoryStatisticsOverview(QWidget): + def __init__(self, parent = None): + self.parent = parent + super().__init__(parent=parent) + + # Central widget and layout + main_layout = QVBoxLayout(self) + + # Title label + title_label = QLabel("Category statistics", self) + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet( + "font-size: 20px; font-weight: bold; color: #0078D4;") + main_layout.addWidget(title_label) + + # Search bar + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Search in categories...") + self.search_input.textChanged.connect(self.filter_categories) + main_layout.addWidget(self.search_input) + + # Scrollable area for cards + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + + # Container widget for the scroll area + self.scroll_widget = QWidget() + self.scroll_layout = QVBoxLayout(self.scroll_widget) + 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) + self.books = [] + self.book_cards = [] + self.redraw_cards() + + self.scroll_widget.setLayout(self.scroll_layout) + self.scroll_area.setWidget(self.scroll_widget) + main_layout.addWidget(self.scroll_area) + + main_layout.addLayout(button_layout) + + def filter_categories(self, text): + """Filter the cards based on the search input.""" + for card, book in zip(self.book_cards, self.books): + + title_contains_text = text.lower() in book.title.lower() + author_name_contains_text = text.lower() in book.author_name.lower() + isbn_contains_text = text.lower() in book.isbn + + card.setVisible(title_contains_text or author_name_contains_text or isbn_contains_text) + + + def clear_layout(self, layout): + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.deleteLater() + else: + sub_layout = item.layout() + if sub_layout is not None: + self.clear_layout(sub_layout) + del item + + def redraw_cards(self): + self.clear_layout(self.scroll_layout) + self.book_cards = [] + + self.books = fetch_all_book_overviews() + + for book in self.books: + card = BookCard(book) + + self.scroll_layout.addWidget(card) + self.book_cards.append(card) + + +__all__ = ["BookCategoryStatisticsOverview"] diff --git a/src/ui/main_tabs/member_list/member_card.py b/src/ui/main_tabs/member_list/member_card.py index 59cab69..60eb3f9 100644 --- a/src/ui/main_tabs/member_list/member_card.py +++ b/src/ui/main_tabs/member_list/member_card.py @@ -6,8 +6,11 @@ from PySide6.QtCore import qDebug from models import Member, MemberStatusEnum from database.manager import DatabaseManager +from database import delete_member from sqlalchemy import delete +from utils.errors import DatabaseConnectionError, DatabaseError + STATUS_TO_COLOR_MAP = { MemberStatusEnum.active: "#3c702e", MemberStatusEnum.inactive: "#702525" @@ -96,19 +99,17 @@ class MemberCard(QWidget): self.update_member_status(MemberStatusEnum.active) def delete_member(self): - self.make_sure() - # if not self.make_sure(): - # return - - # with DatabaseManager.get_session() as session: - # try: - # stmt = delete(Member).where(Member.id == self.member.id) - # session.execute(stmt) - # session.commit() - # self.setVisible(False) - # except Exception as e: - # session.rollback() - # print(e) + if not self.make_sure(): + return + + try: + delete_member(self.member.id) + self.setVisible(False) + except DatabaseConnectionError as e: + QMessageBox.critical(None, "Failed", "Connection with database failed", QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.NoButton) + except DatabaseError as e: + QMessageBox.critical(None, "Failed", f"An error occured when deleting member: {e}", QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.NoButton) + def update_member_status(self, new_status): with DatabaseManager.get_session() as session: diff --git a/src/ui/main_tabs/member_list/member_list.py b/src/ui/main_tabs/member_list/member_list.py index 6e650ca..9c86374 100644 --- a/src/ui/main_tabs/member_list/member_list.py +++ b/src/ui/main_tabs/member_list/member_list.py @@ -57,27 +57,19 @@ class MemberList(QWidget): register_member_button.clicked.connect(self.register_member) button_layout.addWidget(register_member_button) - delete_member_button = QPushButton("Delete Member") - delete_member_button.clicked.connect(self.delete_member) - button_layout.addWidget(delete_member_button) - main_layout.addLayout(button_layout) def filter_members(self, text): """Filter the cards based on the search input.""" for card, member in zip(self.member_cards, self.members): - name_contains_text = text.lower() in member.name.lower() - id_contains_text = text.lower() in str(member.id) + first_name_contains_text = text.lower() in member.first_name.lower() + last_name_contains_text = text.lower() in member.last_name.lower() - card.setVisible(name_contains_text or id_contains_text) + card.setVisible(first_name_contains_text or last_name_contains_text) def register_member(self): MemberEditor().exec() - def delete_member(self): - QMessageBox.information(self, "Delete Member", - "Open dialog to delete a member.") - def clear_layout(self, layout): while layout.count(): item = layout.takeAt(0) diff --git a/src/ui/menu_bar.py b/src/ui/menu_bar.py index 151b9f1..e2e3835 100644 --- a/src/ui/menu_bar.py +++ b/src/ui/menu_bar.py @@ -6,7 +6,7 @@ from ui.settings import SettingsDialog from ui.import_preview import PreviewDialog from ui.editor import BookEditor, MemberEditor -from utils.errors import ExportError, ExportFileError +from utils.errors import ExportError, ExportFileError, InvalidContentsError from services import book_service, book_overview_service @@ -107,10 +107,11 @@ class MenuBar(QMenuBar): self.parent.refresh_member_cards() def import_books(self): - self.import_data("Book", book_service) + self.import_data("Book", None, book_service) def import_members(self): - self.import_data("Member", memb) + # self.import_data("Member", memb) + pass def export_books(self): self.export_data("Book", book_service) @@ -147,6 +148,11 @@ class MenuBar(QMenuBar): self.parent.refresh_book_cards() else: QMessageBox.information(self, "Canceled", "Import was canceled.", QMessageBox.Ok) + except InvalidContentsError as e: + QMessageBox.critical(self, + "Invalid file", + "The file you selected is invalid", + QMessageBox.StandardButton.Ok) except ImportError as e: QMessageBox.critical(self, "Error importing books", diff --git a/src/ui/window.py b/src/ui/window.py index 1370b05..de256e8 100644 --- a/src/ui/window.py +++ b/src/ui/window.py @@ -22,7 +22,7 @@ class LibraryWindow(QMainWindow): central_widget = QTabWidget() self.setCentralWidget(central_widget) - self.dashboard = BookOverviewList() + self.dashboard = BookOverviewList(self) self.member_list = MemberList() central_widget.addTab(self.dashboard, "Dashboard") central_widget.addTab(self.member_list, "Members") diff --git a/src/utils/config.py b/src/utils/config.py index e5ece7d..da9d05c 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -82,6 +82,8 @@ class UserConfig: self._transaction_level = TransactionLevel.insecure if not hasattr(self, "_simulate_slowdown"): self._simulate_slowdown = False + if not hasattr(self, "logger"): + self.logger = logging.getLogger(__name__) @property def transaction_level(self) -> TransactionLevel: @@ -93,6 +95,7 @@ class UserConfig: raise TypeError( f"Invalid value for 'transaction_level'. Must be a TransactionLevel enum, got {type(value).__name__}." ) + self.logger.debug(f"Transaction isolation level set to: {value}") self._transaction_level = value @property @@ -105,14 +108,9 @@ class UserConfig: raise TypeError( f"Invalid value for 'simulate_slowdown'. Must be a boolean, got {type(value).__name__}." ) + self.logger.debug(f"Slowdown simulation set to: {value}") self._simulate_slowdown = value @classmethod def get_friendly_name(cls, option: str) -> str: return cls._metadata.get(option, {}).get("friendly_name", option) - - def __dict__(self) -> dict: - return { - "transaction_level": self.transaction_level, - "simulate_slowdown": self.simulate_slowdown, - } diff --git a/src/utils/errors/database.py b/src/utils/errors/database.py index 82b50f7..6a7b51a 100644 --- a/src/utils/errors/database.py +++ b/src/utils/errors/database.py @@ -20,8 +20,9 @@ class DatabaseConnectionError(DatabaseError): class DuplicateEntryError(DatabaseError): - def __init__(self, message: str): + def __init__(self, duplicate_entry_name: str, message: str): super().__init__(message) + self.duplicate_entry_name = duplicate_entry_name self.message = message