[main] WIP category tab and showcase of phantom read
This commit is contained in:
parent
a129d88575
commit
e452822ffc
@ -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__,
|
||||
]
|
@ -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"]
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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"]
|
@ -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"]
|
||||
|
@ -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__
|
||||
]
|
||||
|
@ -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):
|
@ -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):
|
@ -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'
|
@ -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',
|
18
src/models/book_category_statistics_overview_model.py
Normal file
18
src/models/book_category_statistics_overview_model.py
Normal 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"]
|
@ -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):
|
@ -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'
|
@ -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):
|
@ -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):
|
@ -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)
|
@ -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"))
|
||||
|
@ -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)
|
||||
|
@ -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"]
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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"]
|
@ -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"]
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user