Compare commits

..

2 Commits

33 changed files with 374 additions and 112 deletions

Binary file not shown.

View File

@ -1,11 +1,13 @@
from .manager import * from .manager import *
from .book import * from .book import *
from .book_category_statistics import *
from .member import * from .member import *
from .book_overview import * from .book_overview import *
__all__ = [ __all__ = [
*manager.__all__, *manager.__all__,
*book.__all__, *book.__all__,
*book_category_statistics.__all__,
*book_overview.__all__, *book_overview.__all__,
*member.__all__, *member.__all__,
] ]

View File

@ -1,14 +1,19 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional
import logging import logging
import time
from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError as SqlAlchemyDatabaseError from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError as SqlAlchemyDatabaseError
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlalchemy import delete
from utils.errors.database import DatabaseError, DuplicateEntryError, DatabaseConnectionError from utils.errors.database import DatabaseError, DuplicateEntryError, DatabaseConnectionError
from models import Book from models import Book
from database.manager import DatabaseManager from database.manager import DatabaseManager
from .author import get_or_create_author from .author import get_or_create_author
from .book_category import get_or_create_categories 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__) logger = logging.getLogger(__name__)
@ -84,8 +89,21 @@ def create_books(books: List[Dict[str, object]]) -> None:
) )
session.add(new_book) session.add(new_book)
user_config = UserConfig()
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() session.commit()
logger.info(f"Book {book['title']} successfully created.")
except IntegrityError as e: except IntegrityError as e:
logger.warning("Data already exists") logger.warning("Data already exists")
raise DuplicateEntryError("Data already exists in the database") from e 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}") logger.error(f"An error occurred when updating the book: {e}")
raise DatabaseError("An error occurred when updating the book") from 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"] __all__ = ["create_book", "create_books", "update_book", "fetch_all_books"]

View File

@ -1,23 +1,42 @@
from sqlalchemy.orm import Session 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): def update_category_statistics(session: Session) -> None:
book = session.query(Book).filter_by(id=book_id).first() """
Updates category statistics by calculating the count of books in each category.
if not book: :param session: SQLAlchemy session object.
raise ValueError(f"Book with ID {book_id} does not exist.") """
# 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: # Update or create statistics based on the query results
statistics = session.query(BookCategoryStatistics).filter_by(category_id=category.id).one_or_none() 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: if existing_statistics:
statistics.book_count += 1 # Update the existing count
session.add(statistics) existing_statistics.book_count = book_count
else: 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) session.add(new_statistics)

View File

@ -5,7 +5,7 @@ from sqlalchemy import create_engine, text
from sqlalchemy.exc import DatabaseError from sqlalchemy.exc import DatabaseError
from utils.config import DatabaseConfig from utils.config import DatabaseConfig, UserConfig
from utils.errors.database import DatabaseConnectionError from utils.errors.database import DatabaseConnectionError
@ -49,6 +49,14 @@ class DatabaseManager():
@classmethod @classmethod
def get_session(cls) -> Session: 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"] __all__ = ["DatabaseManager"]

View File

@ -2,6 +2,7 @@ import logging
from typing import List, Dict from typing import List, Dict
from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError as SqlAlchemyDatabaseError from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError as SqlAlchemyDatabaseError
from sqlalchemy import delete
from utils.errors.database import DatabaseError, DuplicateEntryError, DatabaseConnectionError from utils.errors.database import DatabaseError, DuplicateEntryError, DatabaseConnectionError
from models import Member from models import Member
@ -33,12 +34,29 @@ def create_member(new_member: Dict[str, str]):
def create_members(members: List[Dict[str, str]]): def create_members(members: List[Dict[str, str]]):
try: try:
with DatabaseManager.get_session() as session: 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() session.commit()
except IntegrityError as e: except IntegrityError as e:
session.rollback() 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: except DatabaseError as e:
session.rollback() session.rollback()
logger.critical("Connection with database interrupted") 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]): def update_member(member: Dict[str, str]):
pass 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"]

View File

@ -1,21 +1,23 @@
from .author import * from .author_model import *
from .book import * from .book_model import *
from .book_category import * from .book_category_model import *
from .book_category_link import * from .book_category_link_model import *
from .book_category_statistics import * from .book_category_statistics_model import *
from .book_overview import * from .book_category_statistics_overview_model import *
from .member import * from .book_overview_model import *
from .librarian import * from .member_model import *
from .loan import * from .librarian_model import *
from .loan_model import *
__all__ = [ __all__ = [
*author.__all__, *author_model.__all__,
*book.__all__, *book_model.__all__,
*book_category.__all__, *book_category_model.__all__,
*book_category_link.__all__, *book_category_link_model.__all__,
*book_category_statistics.__all__, *book_category_statistics_model.__all__,
*book_overview.__all__, *book_category_statistics_overview_model.__all__,
*member.__all__, *book_overview_model.__all__,
*librarian.__all__, *member_model.__all__,
*loan.__all__ *librarian_model.__all__,
*loan_model.__all__
] ]

View File

@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String, TIMESTAMP, UniqueConstraint, func from sqlalchemy import Column, Integer, String, TIMESTAMP, UniqueConstraint, func
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base from .base_model import Base
class Author(Base): class Author(Base):

View File

@ -1,9 +1,9 @@
from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .book import Book from .book_model import Book
from .book_category import BookCategory from .book_category_model import BookCategory
from .base import Base from .base_model import Base
class BookCategoryLink(Base): class BookCategoryLink(Base):

View File

@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String, TIMESTAMP, ForeignKey, UniqueConstraint, func from sqlalchemy import Column, Integer, String, TIMESTAMP, ForeignKey, UniqueConstraint, func
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base from .base_model import Base
class BookCategory(Base): class BookCategory(Base):
__tablename__ = 'book_category' __tablename__ = 'book_category'

View File

@ -3,13 +3,13 @@ from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.dialects.mysql import INTEGER from sqlalchemy.dialects.mysql import INTEGER
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base from .base_model import Base
class BookCategoryStatistics(Base): class BookCategoryStatistics(Base):
__tablename__ = 'book_category_statistics' __tablename__ = 'book_category_statistics'
book_category_id = Column(Integer, ForeignKey('book_category.id', ondelete="cascade"), primary_key=True) 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( category = relationship(
'BookCategory', 'BookCategory',

View File

@ -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"<BookCategoryStatisticsOverview(book_category_id={self.book_category_id}, book_count={self.book_count})>")
__all__ = ["BookCategoryStatisticsOverview"]

View File

@ -3,7 +3,7 @@ import enum
from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, func from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, func
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base from .base_model import Base
class BookStatusEnum(enum.Enum): class BookStatusEnum(enum.Enum):

View File

@ -1,8 +1,7 @@
from sqlalchemy import Column, String, TIMESTAMP, Integer, Text, Enum from sqlalchemy import Column, String, TIMESTAMP, Integer, Text, Enum
from .base import Base from .base_model import Base
from .book_model import BookStatusEnum
from models.book import BookStatusEnum
class BooksOverview(Base): class BooksOverview(Base):
__tablename__ = 'books_overview' __tablename__ = 'books_overview'

View File

@ -3,7 +3,7 @@ import enum
from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, func from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, func
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base from .base_model import Base
class LibrarianStatusEnum(enum.Enum): class LibrarianStatusEnum(enum.Enum):

View File

@ -3,10 +3,10 @@ import enum
from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, Float, func from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, Float, func
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base from .base_model import Base
from .book import Book from .book_model import Book
from .member import Member from .member_model import Member
from .librarian import Librarian from .librarian_model import Librarian
class LoanStatusEnum(enum.Enum): class LoanStatusEnum(enum.Enum):

View File

@ -3,7 +3,7 @@ import enum
from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, func from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, func
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from .base import Base from .base_model import Base
class MemberStatusEnum(enum.Enum): class MemberStatusEnum(enum.Enum):
@ -13,7 +13,7 @@ class MemberStatusEnum(enum.Enum):
class Member(Base): class Member(Base):
__tablename__ = 'member' __tablename__ = 'member'
__table_args__ = (UniqueConstraint('id'),) __table_args__ = (UniqueConstraint('id'), UniqueConstraint('email'), UniqueConstraint('phone'))
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
first_name = Column(String(50), nullable=False) first_name = Column(String(50), nullable=False)

View File

@ -17,10 +17,8 @@ from models import Book
from database import fetch_all_books, create_books from database import fetch_all_books, create_books
from assets import asset_manager from assets import asset_manager
# Initialize logger and XML Schema
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: try:
logger.debug("Loading XSD schema") logger.debug("Loading XSD schema")
SCHEMA = XMLSchema(asset_manager.get_asset("book_import_scheme.xsd")) SCHEMA = XMLSchema(asset_manager.get_asset("book_import_scheme.xsd"))

View File

@ -126,7 +126,7 @@ class BookEditor(QDialog):
"Could not connect to the database", "Could not connect to the database",
QMessageBox.StandardButton.Ok) QMessageBox.StandardButton.Ok)
except DatabaseError as e: except DatabaseError as e:
QMessageBox.critical(self.parent, QMessageBox.critical(None,
"An error occurred", "An error occurred",
f"Could not save the book because of the following error: {e}", f"Could not save the book because of the following error: {e}",
QMessageBox.StandardButton.Ok) QMessageBox.StandardButton.Ok)

View File

@ -9,7 +9,7 @@ from PySide6.QtWidgets import QVBoxLayout, QFormLayout, QLineEdit, QHBoxLayout,
from models import Member 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 from utils.errors.database import DatabaseError, DatabaseConnectionError, DuplicateEntryError
@ -86,40 +86,41 @@ class MemberEditor(QDialog):
QMessageBox.information(None, QMessageBox.information(None,
"Success", "Success",
"Member created successfully", "Member created successfully",
QMessageBox.StandardButton.Ok) QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
else: else:
member_object["id"] = self.member_id member_object["id"] = self.member_id
update_member(book_object) update_member(book_object)
QMessageBox.information(None, QMessageBox.information(None,
"Success", "Success",
"Member updated successfully", "Member updated successfully",
QMessageBox.StandardButton.Ok) QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
self.accept() self.accept()
except ValueError as e: except ValueError as e:
QMessageBox.critical(None, QMessageBox.critical(None,
"Invalid Input", "Invalid Input",
f"Input validation failed: {e}", f"Input validation failed: {e}",
QMessageBox.StandardButton.Ok) QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
except DuplicateEntryError as e: except DuplicateEntryError as e:
QMessageBox.critical(None, QMessageBox.critical(None,
"ISBN is already in use", f"Duplicate {e.duplicate_entry_name}",
"The ISBN provided is already in use", f"The {e.duplicate_entry_name} is already in use",
QMessageBox.StandardButton.Ok) QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
except DatabaseConnectionError as e: except DatabaseConnectionError as e:
QMessageBox.critical(None, QMessageBox.critical(None,
"Failed to save", "Connection error",
"Could not connect to the database", "Could not connect to the database",
QMessageBox.StandardButton.Ok) QMessageBox.StandardButton.Ok)
except DatabaseError as e: except DatabaseError as e:
QMessageBox.critical(self.parent, QMessageBox.critical(None,
"An error occurred", "Unknown database error",
f"Could not save the book because of the following error: {e}", f"Could not save the book because of the following error: {e}",
QMessageBox.StandardButton.Ok) QMessageBox.StandardButton.Ok)
self.accept()
def parse_inputs(self) -> Dict: def parse_inputs(self) -> Dict:
first_name = self.first_name_input.text().strip() first_name = self.first_name_input.text().strip()
if not first_name or len(first_name) > 50: if not first_name or len(first_name) > 50:
@ -146,4 +147,5 @@ class MemberEditor(QDialog):
"phone_number": phone_number "phone_number": phone_number
} }
__all__ = ["MemberEditor"] __all__ = ["MemberEditor"]

View File

@ -91,8 +91,7 @@ class BookCard(QWidget):
action_edit_book = context_menu.addAction("Edit Book") action_edit_book = context_menu.addAction("Edit Book")
action_edit_author = context_menu.addAction("Edit Author") action_edit_author = context_menu.addAction("Edit Author")
action_mark_returned = context_menu.addAction("Mark as Returned") action_mark_returned = context_menu.addAction("Mark as Returned")
action_remove_reservation = context_menu.addAction( action_remove_reservation = context_menu.addAction("Remove reservation")
"Remove reservation")
context_menu.addSeparator() context_menu.addSeparator()
delete_book_action = context_menu.addAction("Delete Book") delete_book_action = context_menu.addAction("Delete Book")
delete_book_action.triggered.connect(self.delete_book) delete_book_action.triggered.connect(self.delete_book)

View File

@ -15,8 +15,9 @@ from ui.editor import MemberEditor
class BookOverviewList(QWidget): class BookOverviewList(QWidget):
def __init__(self): def __init__(self, parent = None):
super().__init__() self.parent = parent
super().__init__(parent=parent)
# Central widget and layout # Central widget and layout
main_layout = QVBoxLayout(self) main_layout = QVBoxLayout(self)
@ -78,6 +79,7 @@ class BookOverviewList(QWidget):
def register_member(self): def register_member(self):
MemberEditor().exec() MemberEditor().exec()
self.parent.refresh_member_cards()
def add_borrow_record(self): def add_borrow_record(self):
QMessageBox.information(self, "Add Borrow Record", QMessageBox.information(self, "Add Borrow Record",

View File

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

View File

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

View File

@ -6,8 +6,11 @@ from PySide6.QtCore import qDebug
from models import Member, MemberStatusEnum from models import Member, MemberStatusEnum
from database.manager import DatabaseManager from database.manager import DatabaseManager
from database import delete_member
from sqlalchemy import delete from sqlalchemy import delete
from utils.errors import DatabaseConnectionError, DatabaseError
STATUS_TO_COLOR_MAP = { STATUS_TO_COLOR_MAP = {
MemberStatusEnum.active: "#3c702e", MemberStatusEnum.active: "#3c702e",
MemberStatusEnum.inactive: "#702525" MemberStatusEnum.inactive: "#702525"
@ -96,19 +99,17 @@ class MemberCard(QWidget):
self.update_member_status(MemberStatusEnum.active) self.update_member_status(MemberStatusEnum.active)
def delete_member(self): def delete_member(self):
self.make_sure() if not self.make_sure():
# if not self.make_sure(): return
# 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)
# 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)
def update_member_status(self, new_status): def update_member_status(self, new_status):
with DatabaseManager.get_session() as session: with DatabaseManager.get_session() as session:

View File

@ -57,27 +57,19 @@ class MemberList(QWidget):
register_member_button.clicked.connect(self.register_member) register_member_button.clicked.connect(self.register_member)
button_layout.addWidget(register_member_button) 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) main_layout.addLayout(button_layout)
def filter_members(self, text): def filter_members(self, text):
"""Filter the cards based on the search input.""" """Filter the cards based on the search input."""
for card, member in zip(self.member_cards, self.members): for card, member in zip(self.member_cards, self.members):
name_contains_text = text.lower() in member.name.lower() first_name_contains_text = text.lower() in member.first_name.lower()
id_contains_text = text.lower() in str(member.id) 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): def register_member(self):
MemberEditor().exec() MemberEditor().exec()
def delete_member(self):
QMessageBox.information(self, "Delete Member",
"Open dialog to delete a member.")
def clear_layout(self, layout): def clear_layout(self, layout):
while layout.count(): while layout.count():
item = layout.takeAt(0) item = layout.takeAt(0)

View File

@ -6,7 +6,7 @@ from ui.settings import SettingsDialog
from ui.import_preview import PreviewDialog from ui.import_preview import PreviewDialog
from ui.editor import BookEditor, MemberEditor 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 from services import book_service, book_overview_service
@ -107,10 +107,11 @@ class MenuBar(QMenuBar):
self.parent.refresh_member_cards() self.parent.refresh_member_cards()
def import_books(self): def import_books(self):
self.import_data("Book", book_service) self.import_data("Book", None, book_service)
def import_members(self): def import_members(self):
self.import_data("Member", memb) # self.import_data("Member", memb)
pass
def export_books(self): def export_books(self):
self.export_data("Book", book_service) self.export_data("Book", book_service)
@ -147,6 +148,11 @@ class MenuBar(QMenuBar):
self.parent.refresh_book_cards() self.parent.refresh_book_cards()
else: else:
QMessageBox.information(self, "Canceled", "Import was canceled.", QMessageBox.Ok) 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: except ImportError as e:
QMessageBox.critical(self, QMessageBox.critical(self,
"Error importing books", "Error importing books",

View File

@ -22,7 +22,7 @@ class LibraryWindow(QMainWindow):
central_widget = QTabWidget() central_widget = QTabWidget()
self.setCentralWidget(central_widget) self.setCentralWidget(central_widget)
self.dashboard = BookOverviewList() self.dashboard = BookOverviewList(self)
self.member_list = MemberList() self.member_list = MemberList()
central_widget.addTab(self.dashboard, "Dashboard") central_widget.addTab(self.dashboard, "Dashboard")
central_widget.addTab(self.member_list, "Members") central_widget.addTab(self.member_list, "Members")

View File

@ -82,6 +82,8 @@ class UserConfig:
self._transaction_level = TransactionLevel.insecure self._transaction_level = TransactionLevel.insecure
if not hasattr(self, "_simulate_slowdown"): if not hasattr(self, "_simulate_slowdown"):
self._simulate_slowdown = False self._simulate_slowdown = False
if not hasattr(self, "logger"):
self.logger = logging.getLogger(__name__)
@property @property
def transaction_level(self) -> TransactionLevel: def transaction_level(self) -> TransactionLevel:
@ -93,6 +95,7 @@ class UserConfig:
raise TypeError( raise TypeError(
f"Invalid value for 'transaction_level'. Must be a TransactionLevel enum, got {type(value).__name__}." 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 self._transaction_level = value
@property @property
@ -105,14 +108,9 @@ class UserConfig:
raise TypeError( raise TypeError(
f"Invalid value for 'simulate_slowdown'. Must be a boolean, got {type(value).__name__}." 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 self._simulate_slowdown = value
@classmethod @classmethod
def get_friendly_name(cls, option: str) -> str: def get_friendly_name(cls, option: str) -> str:
return cls._metadata.get(option, {}).get("friendly_name", option) return cls._metadata.get(option, {}).get("friendly_name", option)
def __dict__(self) -> dict:
return {
"transaction_level": self.transaction_level,
"simulate_slowdown": self.simulate_slowdown,
}

View File

@ -20,8 +20,9 @@ class DatabaseConnectionError(DatabaseError):
class DuplicateEntryError(DatabaseError): class DuplicateEntryError(DatabaseError):
def __init__(self, message: str): def __init__(self, duplicate_entry_name: str, message: str):
super().__init__(message) super().__init__(message)
self.duplicate_entry_name = duplicate_entry_name
self.message = message self.message = message