[main] Major changes for submit

This commit is contained in:
Thastertyn 2025-01-20 06:49:32 +01:00
parent e452822ffc
commit 104d42201d
40 changed files with 511 additions and 484 deletions

View File

@ -2,4 +2,7 @@ DATABASE_HOST=
DATABASE_PORT=
DATABASE_NAME=
DATABASE_USER=
DATABASE_PASSWORD=
DATABASE_PASSWORD=
TRANSACTION_LEVEL=
VERBOSITY=

Binary file not shown.

View File

@ -6,9 +6,8 @@
<xs:element name="book" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="title"> <!-- Book title-->
<xs:element name="title"> <!-- Book title -->
<xs:simpleType>
<!-- Allow for a string 2-100 characters long -->
<xs:restriction base="xs:string">
<xs:minLength value="2" />
<xs:maxLength value="100" />
@ -21,14 +20,14 @@
<xs:element name="first_name">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:maxLength value="50"/>
<xs:maxLength value="50" />
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="last_name">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:maxLength value="50"/>
<xs:maxLength value="50" />
</xs:restriction>
</xs:simpleType>
</xs:element>
@ -50,6 +49,16 @@
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="price"> <!-- Price -->
<xs:simpleType>
<xs:restriction base="xs:decimal">
<xs:totalDigits value="7" /> <!-- Max 99999.99 -->
<xs:fractionDigits value="2" />
<xs:minInclusive value="0.00" />
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="is_damaged" type="xs:boolean" /> <!-- Is damaged -->
<xs:element name="categories"> <!-- Categories list -->
<xs:complexType>
<xs:sequence>
@ -64,5 +73,4 @@
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
</xs:schema>

View File

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

View File

@ -1,37 +1,24 @@
from typing import Dict, List, Optional
import logging
import time
from typing import Dict, List
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
import logging
logger = logging.getLogger(__name__)
def fetch_all_books() -> List[Book]:
"""
Fetches all books from the database.
:return: A list of all books in the database.
:raises DatabaseConnectionError: If the connection to the database is interrupted.
:raises DatabaseError: If any other error occurs while fetching books.
"""
with DatabaseManager.get_session() as session:
try:
return session.query(Book) \
.options(
joinedload(Book.author),
joinedload(Book.categories)
joinedload(Book.author),
joinedload(Book.categories)
) \
.all()
except SqlAlchemyDatabaseError as e:
@ -41,68 +28,41 @@ def fetch_all_books() -> List[Book]:
logger.error(f"An error occurred when fetching all books: {e}")
raise DatabaseError("An error occurred when fetching all books") from e
def create_book(book: Dict[str, object], skip_existing: bool = True) -> None:
create_books([book], skip_existing)
def create_book(book: Dict[str, object]) -> None:
"""
Creates a new book in the database.
:param book: A dictionary containing the book details (title, description, year_published, ISBN, author, and categories).
:raises DuplicateEntryError: If a book with the same ISBN already exists in the database.
:raises DatabaseConnectionError: If the connection to the database is interrupted.
:raises DatabaseError: If any other error occurs while creating the book.
"""
create_books([book])
def create_books(books: List[Dict[str, object]]) -> None:
"""
Creates multiple books in the database.
:param books: A list of dictionaries, each containing the details of a book.
:raises DuplicateEntryError: If a book with the same ISBN already exists in the database.
:raises DatabaseConnectionError: If the connection to the database is interrupted.
:raises DatabaseError: If any other error occurs while creating the books.
"""
def create_books(books: List[Dict[str, object]], skip_existing: bool = True) -> None:
try:
with DatabaseManager.get_session() as session:
for book in books:
logger.debug(f"Attempting to create a new book: {book['title']}")
# Check if the book already exists
existing_book = session.query(Book).filter_by(isbn=book["isbn"]).first()
if existing_book:
logger.warning(f"Book with ISBN {book['isbn']} already exists. Skipping.")
continue
if skip_existing:
logger.warning(f"Book with ISBN {book['isbn']} already exists. Skipping.")
continue
else:
logger.error(f"Book with ISBN {book['isbn']} already exists.")
raise DuplicateEntryError(f"Book with ISBN {book['isbn']} already exists.")
author = get_or_create_author(session, book["author"])
categories = get_or_create_categories(session, book["categories"])
# Create the new book
new_book = Book(
title=book["title"],
description=book["description"],
year_published=book["year_published"],
isbn=book["isbn"],
price=book["price"],
is_damaged=book["is_damaged"],
author=author,
categories=categories
)
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()
except IntegrityError as e:
logger.warning("Data already exists")
@ -114,41 +74,34 @@ def create_books(books: List[Dict[str, object]]) -> None:
logger.error(f"An error occurred when creating the book: {e}")
raise DatabaseError("An error occurred when creating the book") from e
def update_book(book: Dict[str, object]) -> None:
"""
Updates an existing book in the database. Reuses existing authors and categories if they exist.
:param book: A dictionary containing the updated book details, including the book ID.
:raises DatabaseError: If the book is not found in the database.
:raises DuplicateEntryError: If an attempt is made to update the book with duplicate data.
:raises DatabaseConnectionError: If the connection to the database is interrupted.
"""
try:
with DatabaseManager.get_session() as session:
logger.debug(f"Updating book {book['title']}")
# Find the existing book
existing_book = session.query(Book).get(book["id"])
existing_book = session.query(Book).filter_by(isbn=book["isbn"]).first()
if not existing_book:
logger.warning(f"Book with ID {book['id']} not found")
logger.warning(f"Book with ISBN {book['isbn']} not found")
raise DatabaseError("Book not found in the database")
# Get or create the author
author = get_or_create_author(session, book["author"])
# Get or create the categories
categories = get_or_create_categories(session, book["categories"])
session.commit()
# Update the book details
existing_book.title = book["title"]
existing_book.description = book["description"]
existing_book.year_published = book["year_published"]
existing_book.isbn = book["isbn"]
existing_book.price = book["price"]
existing_book.is_damaged = book["is_damaged"]
existing_book.status = book["status"]
existing_book.author = author
existing_book.categories = categories
update_category_statistics(session, ignore_config=True)
session.commit()
logger.info(f"{book['title']} successfully updated.")
except IntegrityError as e:
logger.warning("Data already exists")
@ -163,9 +116,12 @@ def update_book(book: Dict[str, object]) -> None:
def delete_book(book_id: int) -> None:
try:
with DatabaseManager.get_session() as session:
logger.debug(f"Deleting book id {book_id}")
stmt = delete(Book).where(Book.id == book_id)
session.execute(stmt)
update_category_statistics(session, ignore_config=True)
session.commit()
logger.info(f"Successfully deleted book with id {book_id}")
except SqlAlchemyDatabaseError as e:
logger.critical("Connection with database interrupted")
raise DatabaseConnectionError("Connection with database interrupted") from e
@ -173,4 +129,4 @@ def delete_book(book_id: int) -> None:
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", "delete_book"]

View File

@ -4,6 +4,9 @@ import logging
from models import BookCategory
from database.manager import DatabaseManager
from sqlalchemy.orm import Session
from sqlalchemy import func
logger = logging.getLogger(__name__)
@ -36,4 +39,10 @@ def get_or_create_categories(session, category_names: List[str]) -> List[BookCat
processed_categories[category_name] = new_category
filtered_categories.append(new_category)
return filtered_categories
return filtered_categories
def get_total_count(session: Session) -> int:
return session.query(func.count(BookCategory.id)).scalar()
__all__ = ["get_total_count", "get_or_create_categories"]

View File

@ -1,43 +1,56 @@
from sqlalchemy import func, insert
from sqlalchemy.orm import Session
from sqlalchemy import func
from models import BookCategory, BookCategoryStatistics, BookCategoryLink
import logging
from models import BookCategoryStatistics, BookCategoryLink
logger = logging.getLogger(__name__)
def update_category_statistics(session: Session) -> None:
def update_category_statistics(session: Session, ignore_config: bool = False) -> None:
"""
Updates category statistics by calculating the count of books in each category.
:param session: SQLAlchemy session object.
"""
# Calculate the book count for each category using a query
# Fetch book counts per category using a join between book_category and book_category_link
category_counts = (
session.query(
BookCategoryLink.book_category_id,
BookCategory.id.label('book_category_id'),
func.count(BookCategoryLink.book_id).label('book_count')
)
.group_by(BookCategoryLink.book_category_id)
.join(BookCategoryLink, BookCategoryLink.book_category_id == BookCategory.id, isouter=True)
.group_by(BookCategory.id)
.all()
)
# Update or create statistics based on the query results
# Iterate over the results and update or insert the category statistics
for category_id, book_count in category_counts:
# Try to get the existing statistics or create a new one if it doesn't exist
existing_statistics = (
session.query(BookCategoryStatistics)
.filter_by(book_category_id=category_id)
.filter(BookCategoryStatistics.book_category_id == category_id)
.one_or_none()
)
if existing_statistics:
# Update the existing count
# If statistics exist, update the count
existing_statistics.book_count = book_count
logger.debug(f"Updated category {category_id} with count {book_count}")
else:
# Create new statistics for the category
# If statistics don't exist, create a new one
new_statistics = BookCategoryStatistics(
book_category_id=category_id,
book_count=book_count
)
session.add(new_statistics)
logger.debug(f"Inserted new statistics for category {category_id} with count {book_count}")
try:
# Commit the transaction
session.commit()
logger.debug("Category statistics updated successfully")
except Exception as e:
# In case of error, rollback the transaction
logger.error(f"An error occurred while updating category statistics: {e}")
session.rollback()
__all__ = ["update_category_statistics"]

View File

@ -0,0 +1,55 @@
from typing import List, Tuple
import time
import logging
from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError as SqlAlchemyDatabaseError
from utils.errors.database import DatabaseError, DuplicateEntryError, DatabaseConnectionError
from models import BookCategoryStatisticsOverview
from database.manager import DatabaseManager
from database import get_total_count
from utils.config import UserConfig
logger = logging.getLogger(__name__)
def fetch_all_book_category_statistics_overviews() -> List[BookCategoryStatisticsOverview]:
with DatabaseManager.get_session() as session:
try:
return session.query(BookCategoryStatisticsOverview).all()
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 fetching all category overviews: {e}")
raise DatabaseError("An error occurred when fetching all category overviews") from e
def fetch_all_book_category_statistics_overviews_with_count() -> Tuple[List[BookCategoryStatisticsOverview], int]:
with DatabaseManager.get_session() as session:
try:
category_list = session.query(BookCategoryStatisticsOverview).all()
user_config = UserConfig()
if user_config.simulate_slowdown:
logger.debug("Simulating slowdown after fetching statistics for 10 seconds")
time.sleep(10)
else:
logger.debug("Performing category statistics update normally")
count = get_total_count(session)
return (category_list, count)
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 fetching all category overviews: {e}")
raise DatabaseError("An error occurred when fetching all category overviews") from e
__all__ = ["fetch_all_book_category_statistics_overviews", "fetch_all_book_category_statistics_overviews_with_count"]

View File

@ -18,8 +18,8 @@ def fetch_all_book_overviews() -> List[BooksOverview]:
logger.critical("Connection with database interrupted")
raise DatabaseConnectionError("Connection with database interrupted") from e
except SQLAlchemyError as e:
logger.error(f"An error occured when fetching all books: {e}")
raise DatabaseError("An error occured when fetching all books") from e
logger.error(f"An error occurred when fetching all books: {e}")
raise DatabaseError("An error occurred when fetching all books") from e
__all__ = ["fetch_all_book_overviews"]

View File

@ -22,27 +22,28 @@ class DatabaseManager():
self.logger = logging.getLogger(__name__)
self.logger.info("Reading database config")
database_config = DatabaseConfig()
user_config = UserConfig()
self.engine = create_engine('mysql+mysqlconnector://%s:%s@%s:%s/%s' % (
database_config.user,
database_config.password,
database_config.host,
database_config.port,
database_config.name),
pool_pre_ping=True)
if self.test_connection():
self.Session = sessionmaker(bind=self.engine)
pool_pre_ping=True,
isolation_level=user_config.transaction_level.value)
self.test_connection()
self.Session = sessionmaker(bind=self.engine)
def cleanup(self) -> None:
self.logger.debug("Closing connection")
self.engine.dispose()
def test_connection(self) -> bool:
def test_connection(self):
self.logger.debug("Testing database connection")
try:
with self.engine.connect() as connection:
connection.execute(text("select 1"))
self.logger.debug("Database connection successful")
return True
except DatabaseError as e:
self.logger.critical(f"Database connection failed: {e}")
raise DatabaseConnectionError("Database connection failed") from e

View File

@ -67,7 +67,44 @@ def create_members(members: List[Dict[str, str]]):
raise DatabaseError("An error occurred when creating a new member") from e
def update_member(member: Dict[str, str]):
pass
try:
with DatabaseManager.get_session() as session:
logger.debug(f"Editing member {member['first_name']} {member['last_name']}")
existing_member = session.query(Member).get(member["id"])
if not existing_member:
logger.warning(f"Member with ID {member['id']} not found")
raise DatabaseError("Member not found in database")
existing_member.first_name = member["first_name"]
existing_member.last_name = member["last_name"]
existing_member.email = member["email"]
existing_member.phone = member["phone"]
session.commit()
except IntegrityError as e:
session.rollback()
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("An error occurred when updating member")
raise DatabaseError("An error occurred when updating member") from e
except DatabaseError as e:
session.rollback()
logger.critical("Connection with database interrupted")
raise DatabaseConnectionError("Connection with database interrupted") from e
except SQLAlchemyError as e:
session.rollback()
logger.error(f"An error occurred when saving member: {e}")
raise DatabaseError("An error occurred when creating a new member") from e
def delete_member(member_id: int) -> None:
try:

View File

@ -1,71 +0,0 @@
import os
import logging
import time
from typing import List, Dict
from xml.etree import ElementTree as ET
from xmlschema import XMLSchema
from sqlalchemy.exc import IntegrityError
from database.manager import DatabaseManager
from database.utils import get_or_create_categories, get_or_create_author
from utils.errors.import_error.xsd_scheme_not_found import XsdSchemeNotFoundError
from utils.errors.import_error.invalid_contents_error import InvalidContentsError
from utils.errors.import_error.import_error import ImportError
from utils.config import UserConfig
from models import Book, Author, BookCategory
class BookImporter:
def __init__(self):
# Initialize the logger and schema
self.logger = logging.getLogger(__name__)
try:
self.logger.debug("Opening XSD scheme in ./")
scheme_path = os.path.join(os.path.dirname(__file__), "book_import_scheme.xsd")
self.schema = XMLSchema(scheme_path)
except Exception as e:
self.logger.error("Failed to load XSD scheme")
raise XsdSchemeNotFoundError(f"Failed to load XSD schema: {e}")
def parse_xml(self, file_path: str) -> List[Dict[str, object]]:
"""Parses the XML file and validates it against the XSD schema."""
try:
tree = ET.parse(file_path)
root = tree.getroot()
if not self.schema.is_valid(file_path):
raise InvalidContentsError("XML file is not valid according to XSD schema.")
books = []
for book_element in root.findall("book"):
title = book_element.find("title").text
year_published = book_element.find("year_published").text
description = book_element.find("description").text
isbn = book_element.find("isbn").text
# Parse author
author_element = book_element.find("author")
author = {
"first_name": author_element.find("first_name").text,
"last_name": author_element.find("last_name").text
}
# Parse categories
category_elements = book_element.find("categories").findall("category")
categories = [category_element.text for category_element in category_elements]
# Create a book dictionary with explicit types
book = {
"title": title,
"description": description,
"year_published": year_published,
"isbn": isbn,
"author": author,
"categories": categories
}
books.append(book)
return books
except ET.ParseError as e:
raise ImportError(f"Failed to parse XML file: {e}")

View File

@ -6,8 +6,6 @@ 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_model.__all__,
@ -18,6 +16,4 @@ __all__ = [
*book_category_statistics_overview_model.__all__,
*book_overview_model.__all__,
*member_model.__all__,
*librarian_model.__all__,
*loan_model.__all__
]

View File

@ -8,13 +8,9 @@ class BookCategory(Base):
__table_args__ = (UniqueConstraint('name'),)
id = Column(Integer, primary_key=True, autoincrement=True)
parent_category_id = Column(Integer, ForeignKey('book_category.id'), nullable=True)
name = Column(String(100), nullable=False)
mature_content = Column(Integer, nullable=False, default=0)
last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now())
parent_category = relationship('BookCategory', remote_side=[id])
books = relationship(
'Book',
secondary='book_category_link',

View File

@ -8,11 +8,12 @@ class BookCategoryStatisticsOverview(Base):
__tablename__ = 'book_category_statistics_overview'
__table_args__ = {'extend_existing': True}
name = Column(String)
book_count = Column(INTEGER(unsigned=True), default=0)
id = Column(Integer, primary_key=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})>")
return (f"<BookCategoryStatisticsOverview(book_category_id={self.id}, book_count={self.book_count})>")
__all__ = ["BookCategoryStatisticsOverview"]

View File

@ -1,6 +1,6 @@
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, DECIMAL,Boolean
from sqlalchemy.orm import relationship
from .base_model import Base
@ -22,6 +22,8 @@ class Book(Base):
description = Column(Text, nullable=False)
year_published = Column(String(4), nullable=False)
isbn = Column(String(13), nullable=False, unique=True)
price = Column(DECIMAL(5, 2), nullable=False, default=0)
is_damaged = Column(Boolean, nullable=False, default=False)
status = Column(Enum(BookStatusEnum), nullable=False, default=BookStatusEnum.available)
created_at = Column(TIMESTAMP, nullable=False, server_default=func.now())
last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now())

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, String, TIMESTAMP, Integer, Text, Enum
from sqlalchemy import Column, String, TIMESTAMP, Integer, Text, Enum, DECIMAL, Boolean
from .base_model import Base
from .book_model import BookStatusEnum
@ -15,12 +15,10 @@ class BooksOverview(Base):
categories = Column(Text, nullable=True)
year_published = Column(Integer, nullable=True)
isbn = Column(String, nullable=True)
price = Column(DECIMAL(5, 2), nullable=False, default=0)
is_damaged = Column(Boolean, nullable=False, default=False)
status = Column(Enum(BookStatusEnum), nullable=False)
created_at = Column(TIMESTAMP, nullable=False)
borrower_name = Column(String, nullable=True)
member_id = Column(Integer, nullable=True)
librarian_name = Column(String, nullable=True)
librarian_id = Column(Integer, nullable=True)
# This prevents accidental updates/deletes as it's a view
def __repr__(self):

View File

@ -1,33 +0,0 @@
import enum
from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, func
from sqlalchemy.orm import relationship
from .base_model import Base
class LibrarianStatusEnum(enum.Enum):
active = 'active'
inactive = 'inactive'
class LibrarianRoleEnum(enum.Enum):
staff = 'staff'
admin = 'admin'
class Librarian(Base):
__tablename__ = 'librarian'
__table_args__ = (UniqueConstraint('id'),)
id = Column(Integer, primary_key=True, autoincrement=True)
first_name = Column(String(50), nullable=False)
last_name = Column(String(50), nullable=False)
email = Column(String(100), nullable=False, unique=True)
phone = Column(String(20), nullable=False)
hire_date = Column(TIMESTAMP, nullable=False, server_default=func.now())
status = Column(Enum(LibrarianStatusEnum), nullable=False, default=LibrarianStatusEnum.active)
role = Column(Enum(LibrarianRoleEnum), nullable=False)
last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now())
__all__ = ["Librarian", "LibrarianRoleEnum", "LibrarianStatusEnum"]

View File

@ -1,39 +0,0 @@
import enum
from sqlalchemy import Column, Integer, String, TIMESTAMP, Text, ForeignKey, Enum, UniqueConstraint, Float, func
from sqlalchemy.orm import relationship
from .base_model import Base
from .book_model import Book
from .member_model import Member
from .librarian_model import Librarian
class LoanStatusEnum(enum.Enum):
borrowed = 'borrowed'
returned = 'returned'
overdue = 'overdue'
reserved = 'reserved'
class Loan(Base):
__tablename__ = 'loan'
__table_args__ = (UniqueConstraint('id'),)
id = Column(Integer, primary_key=True, autoincrement=True)
book_id = Column(Integer, ForeignKey('book.id'), nullable=False)
member_id = Column(Integer, ForeignKey('member.id'), nullable=False)
librarian_id = Column(Integer, ForeignKey('librarian.id'), nullable=False)
loan_date = Column(TIMESTAMP, nullable=False, server_default=func.now())
due_date = Column(TIMESTAMP, nullable=False)
return_date = Column(TIMESTAMP, nullable=True)
status = Column(Enum(LoanStatusEnum), nullable=False, default=LoanStatusEnum.borrowed)
overdue_fee = Column(Float, nullable=True)
last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now())
book = relationship('Book', backref='loans')
member = relationship('Member', backref='loans')
librarian = relationship('Librarian', backref='loans')
__all__ = ["Loan", "LoanStatusEnum"]

View File

@ -5,3 +5,4 @@ PySide6_Essentials==6.8.1
python-dotenv==1.0.1
SQLAlchemy==2.0.36
xmlschema==3.4.3
mysql-connector-python==9.1.0

View File

@ -0,0 +1,61 @@
from typing import List
import logging
import xml.etree.ElementTree as ET
from xml.dom import minidom
from xmlschema import XMLSchema
from utils.errors import (
NoExportEntityError,
ExportError,
ExportFileError,
InvalidContentsError,
XsdSchemeNotFoundError,
ImportError,
)
from models import BookCategoryStatisticsOverview
from assets import asset_manager
from utils.errors import DatabaseConnectionError, DatabaseError
from database import fetch_all_book_category_statistics_overviews_with_count
logger = logging.getLogger(__name__)
def export_to_xml(file_path: str) -> None:
try:
category_list, count = fetch_all_book_category_statistics_overviews_with_count()
if not category_list:
raise NoExportEntityError("No categories found to export")
xml = category_statistics_to_xml(category_list, count)
with open(file_path, "w", encoding="utf-8") as file:
file.write(xml)
except OSError as e:
raise ExportFileError("Failed to save to a file") from e
except DatabaseConnectionError as e:
raise ExportError("An error with database occurred. Try again later") from e
except DatabaseError as e:
raise ExportError("Unknown error occurred") from e
def category_statistics_to_xml(category_statistics: List[BookCategoryStatisticsOverview], total_count: int):
root = ET.Element("statistics")
for statistic in category_statistics:
category_element = ET.SubElement(root, "category")
name_element = ET.SubElement(category_element, "name")
name_element.text = statistic.name
count_element = ET.SubElement(category_element, "count")
count_element.text = str(statistic.book_count)
total_count_element = ET.SubElement(root, "total_category_count")
total_count_element.text = str(total_count)
tree_str = ET.tostring(root, encoding="unicode")
pretty_xml = minidom.parseString(tree_str).toprettyxml(indent=(" " * 4))
return pretty_xml

View File

@ -58,6 +58,8 @@ def parse_from_xml(file_path: str) -> List[Dict[str, object]]:
year_published = book_element.find("year_published").text
description = book_element.find("description").text
isbn = book_element.find("isbn").text
price = float(book_element.find("price").text)
is_damaged = bool(book_element.find("is_damaged").text)
# Parse author
author_element = book_element.find("author")
@ -77,6 +79,8 @@ def parse_from_xml(file_path: str) -> List[Dict[str, object]]:
"year_published" : year_published,
"isbn" : isbn,
"author" : author,
"price": price,
"is_damaged" : is_damaged,
"categories" : categories,
}
books.append(book)
@ -119,6 +123,12 @@ def books_to_xml(books: List[Book]) -> str:
isbn_element = ET.SubElement(book_element, "isbn")
isbn_element.text = book.isbn
price_element = ET.SubElement(book_element, "price")
price_element.text = str(book.price)
damaged_element = ET.SubElement(book_element, "is_damaged")
damaged_element.text = str(book.is_damaged).lower()
# Add <categories>
categories_element = ET.SubElement(book_element, "categories")
for category in book.categories:
@ -133,4 +143,4 @@ def books_to_xml(books: List[Book]) -> str:
return pretty_xml
__all__ = ["export_to_xml", "parse_books_from_xml"]
__all__ = ["export_to_xml", "parse_from_xml"]

View File

@ -1,9 +1,9 @@
from typing import Dict
from typing import Dict, Callable
import logging
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QTextEdit, QPushButton, QComboBox, QFormLayout, QMessageBox
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QTextEdit, QPushButton, QComboBox, QFormLayout, QMessageBox, QCheckBox
)
from PySide6.QtGui import QRegularExpressionValidator
from PySide6.QtCore import QRegularExpression
@ -15,12 +15,14 @@ from utils.errors.database import DatabaseError, DatabaseConnectionError, Duplic
class BookEditor(QDialog):
def __init__(self, book: Book = None, parent=None):
def __init__(self, book: Book = None, parent=None, refresh_callback: Callable[[Dict[str, object]], None] = None):
super().__init__(parent)
self.logger = logging.getLogger(__name__)
self.create_layout()
self.refresh_callback = refresh_callback
if book:
self.book_id = book.id
self.logger.debug(f"Editing book {book.title}")
@ -54,7 +56,6 @@ class BookEditor(QDialog):
# Year published field
self.year_input = QLineEdit()
# self.year_input.setValidator
form_layout.addRow("Year Published:", self.year_input)
# ISBN field
@ -64,9 +65,24 @@ class BookEditor(QDialog):
self.isbn_input.setValidator(self.isbn_validator)
form_layout.addRow("ISBN:", self.isbn_input)
# Categories field
self.categories_input = QLineEdit()
form_layout.addRow("Categories: ", self.categories_input)
# Damage field
self.damage_input = QCheckBox()
form_layout.addRow("Damage:", self.damage_input)
# Price field
self.price_input = QLineEdit()
self.price_input.setValidator(QRegularExpressionValidator(QRegularExpression(r"^\d+(\.\d{1,2})?$")))
form_layout.addRow("Price:", self.price_input)
# Status field
self.status_input = QComboBox()
self.status_input.addItems([status.value for status in BookStatusEnum])
form_layout.addRow("Status:", self.status_input)
layout.addLayout(form_layout)
# Buttons
@ -94,21 +110,28 @@ class BookEditor(QDialog):
all_categories = ", ".join(category.name for category in book.categories)
self.categories_input.setText(all_categories)
self.damage_input.setChecked(book.is_damaged)
self.price_input.setText(str(book.price))
self.status_input.setCurrentText(book.status.value)
def save_book(self):
try:
book_object = self.parse_inputs()
if self.create_new:
create_book(book_object)
create_book(book_object, skip_existing=False)
else:
book_object["id"] = self.book_id
update_book(book_object)
QMessageBox.information(None,
"Success",
"Book updated successfully",
QMessageBox.StandardButton.Ok)
if self.refresh_callback:
self.refresh_callback(book_object)
self.accept()
except ValueError as e:
QMessageBox.critical(None,
@ -168,6 +191,21 @@ class BookEditor(QDialog):
if not categories:
raise ValueError("At least one category must be specified.")
# Damage validation
damage = self.damage_input.isChecked()
# Price validation
price = self.price_input.text().strip()
try:
price = float(price)
if price < 0:
raise ValueError("Price must be a non-negative number.")
except ValueError:
raise ValueError("Price must be a valid decimal number.")
# Status validation
status = self.status_input.currentText()
# Map parsed values to dictionary format for saving
return {
"title": title,
@ -178,7 +216,10 @@ class BookEditor(QDialog):
"description": description,
"year_published": year_published,
"isbn": isbn,
"categories": categories
"categories": categories,
"is_damaged": damage,
"price": price,
"status": status
}
__all__ = ["BookEditor"]

View File

@ -1,6 +1,6 @@
import logging
import re
from typing import Dict
from typing import Dict, Callable
from PySide6.QtGui import QGuiApplication, QAction
from PySide6.QtQml import QQmlApplicationEngine
@ -15,16 +15,18 @@ from utils.errors.database import DatabaseError, DatabaseConnectionError, Duplic
class MemberEditor(QDialog):
def __init__(self, member: Member = None):
def __init__(self, member: Member = None, refresh_callback: Callable[[Dict[str, object]], None] = None):
super().__init__()
self.logger = logging.getLogger(__name__)
self.create_layout()
self.refresh_callback = refresh_callback
if member:
self.member_id = member.id
self.logger.debug(f"Editing member {member.first_name} {member.last_name}")
self.fill_with_existing_data()
self.fill_with_existing_data(member)
self.create_new = False
else:
self.logger.debug("Editing a new member")
@ -90,13 +92,16 @@ class MemberEditor(QDialog):
QMessageBox.StandardButton.NoButton)
else:
member_object["id"] = self.member_id
update_member(book_object)
update_member(member_object)
QMessageBox.information(None,
"Success",
"Member updated successfully",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
if self.refresh_callback:
self.refresh_callback(member_object)
self.accept()
except ValueError as e:
QMessageBox.critical(None,
@ -144,8 +149,7 @@ class MemberEditor(QDialog):
"first_name": first_name,
"last_name": last_name,
"email": email,
"phone_number": phone_number
"phone": phone_number
}
__all__ = ["MemberEditor"]

View File

@ -1,2 +1,3 @@
from .book_overview_list import BookOverviewList
from .member_list import MemberList
from .member_list import MemberList
from .category_statistics_overview_list import BookCategoryStatisticsOverview

View File

@ -1,15 +1,10 @@
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 PySide6.QtGui import QAction, Qt
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QWidget, QMenu, QSizePolicy, QLayout, QMessageBox, QDialog
from models import BooksOverview, Book, BookStatusEnum
from ui.editor import BookEditor
from database.manager import DatabaseManager
from sqlalchemy import delete
from database import delete_book
from utils.errors import DatabaseConnectionError, DatabaseError
STATUS_TO_COLOR_MAP = {
BookStatusEnum.available: "#3c702e",
@ -24,10 +19,8 @@ class BookCard(QWidget):
self.book_overview = book_overview
self.setAttribute(Qt.WidgetAttribute.WA_Hover,
True) # Enable hover events
# Enable styling for background
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.setAttribute(Qt.WidgetAttribute.WA_Hover, True) # Enable hover events
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) # Enable styling for background
# Set initial stylesheet with hover behavior
self.setStyleSheet("""
@ -39,106 +32,104 @@ class BookCard(QWidget):
# Layout setup
layout = QHBoxLayout(self)
layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
self.setSizePolicy(QSizePolicy.Policy.Preferred,
QSizePolicy.Policy.Fixed)
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
# Initialize UI components
self.title_label = QLabel()
self.author_label = QLabel()
self.isbn_label = QLabel()
self.status_label = QLabel()
self.price_label = QLabel()
self.is_damaged_label = QLabel()
# Left-side content
left_side = QVBoxLayout()
layout.addLayout(left_side)
title_label = QLabel(book_overview.title)
title_label.setStyleSheet("font-size: 20px; font-weight: bold;")
author_label = QLabel("By: " + book_overview.author_name)
isbn_label = QLabel("ISBN: " + (book_overview.isbn or "Not Available"))
left_side.addWidget(title_label)
left_side.addWidget(author_label)
left_side.addWidget(isbn_label)
left_side.addWidget(self.title_label)
left_side.addWidget(self.author_label)
left_side.addWidget(self.isbn_label)
# Right-side content
right_side = QVBoxLayout()
layout.addLayout(right_side)
status_label = QLabel(str(book_overview.status.value.capitalize()))
status_label.setStyleSheet(f"color: {
STATUS_TO_COLOR_MAP[book_overview.status]}; font-size: 20px; font-weight: bold;")
status_label.setAlignment(Qt.AlignmentFlag.AlignRight)
right_side.addWidget(status_label)
if book_overview.librarian_name and book_overview.borrower_name:
borrower_label = QLabel("Borrowed: " + book_overview.borrower_name)
borrower_label.setAlignment(Qt.AlignmentFlag.AlignRight)
librarian_label = QLabel("By: " + book_overview.librarian_name)
librarian_label.setAlignment(Qt.AlignmentFlag.AlignRight)
right_side.addWidget(borrower_label)
right_side.addWidget(librarian_label)
right_side.addWidget(self.status_label)
right_side.addWidget(self.price_label)
right_side.addWidget(self.is_damaged_label)
self.setLayout(layout)
self.update_display()
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.contextMenuEvent(event)
else:
super().mousePressEvent(event)
def update_display(self):
"""Refreshes the display of the book card based on its current data."""
self.title_label.setText(self.book_overview.title)
self.title_label.setStyleSheet("font-size: 20px; font-weight: bold;")
self.author_label.setText("By: " + self.book_overview.author_name)
self.isbn_label.setText("ISBN: " + (self.book_overview.isbn or "Not Available"))
self.status_label.setText(str(self.book_overview.status.value.capitalize()))
self.status_label.setStyleSheet(f"color: {STATUS_TO_COLOR_MAP[self.book_overview.status]}; font-size: 20px; font-weight: bold;")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignRight)
self.price_label.setText("Price: " + str(self.book_overview.price))
self.price_label.setAlignment(Qt.AlignmentFlag.AlignRight)
self.is_damaged_label.setText("Damaged: " + str(self.book_overview.is_damaged))
self.is_damaged_label.setAlignment(Qt.AlignmentFlag.AlignRight)
def contextMenuEvent(self, event):
context_menu = QMenu(self)
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")
context_menu.addSeparator()
delete_book_action = context_menu.addAction("Delete Book")
delete_book_action.triggered.connect(self.delete_book)
if self.book_overview.status != BookStatusEnum.borrowed:
action_mark_returned.setVisible(False)
if self.book_overview.status != BookStatusEnum.reserved:
action_remove_reservation.setVisible(False)
action = context_menu.exec_(self.mapToGlobal(event.pos()))
if action == action_edit_book:
with DatabaseManager.get_session() as session:
book_id = self.book_overview.id
self.open_editor()
book = session.query(Book).filter(
Book.id == book_id).one_or_none()
def open_editor(self):
"""Opens the BookEditor and updates the card if changes are made."""
with DatabaseManager.get_session() as session:
book_id = self.book_overview.id
book = session.query(Book).filter(Book.id == book_id).one_or_none()
if book:
BookEditor(book).exec()
else:
QMessageBox.critical(self,
"Error",
"The book you requested could not be found. Try again later",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
elif action == action_edit_author:
print("Edit Author selected")
elif action == action_mark_returned:
print("Mark as Returned selected")
elif action == action_remove_reservation:
print("Remove reservation selected")
if book:
editor = BookEditor(book)
if editor.exec() == QDialog.DialogCode.Accepted:
updated_data = editor.parse_inputs()
self.refresh(updated_data)
else:
QMessageBox.critical(self,
"Error",
"The book you requested could not be found. Try again later",
QMessageBox.StandardButton.Ok)
def refresh(self, updated_data):
"""Updates the card's data and refreshes the display."""
self.book_overview.title = updated_data["title"]
self.book_overview.author_name = f"{updated_data['author']['first_name']} {updated_data['author']['last_name']}"
self.book_overview.isbn = updated_data["isbn"]
self.book_overview.status = BookStatusEnum(updated_data["status"])
self.book_overview.price = updated_data["price"]
self.book_overview.is_damaged = updated_data["is_damaged"]
self.update_display()
def delete_book(self):
if not self.make_sure():
return
with DatabaseManager.get_session() as session:
try:
stmt = delete(Book).where(Book.id == self.book_overview.id)
session.execute(stmt)
session.commit()
self.setVisible(False)
except Exception as e:
session.rollback
print(e)
try:
delete_book(self.book_overview.id)
self.setVisible(False)
except DatabaseConnectionError as e:
QMessageBox.critical(None, "Failed", "Connection with database failed", QMessageBox.StandardButton.Ok)
except DatabaseError as e:
QMessageBox.critical(None, "Failed", f"An error occurred when deleting book: {e}", QMessageBox.StandardButton.Ok)
def make_sure(self) -> bool:
are_you_sure_box = QMessageBox()
@ -154,5 +145,4 @@ class BookCard(QWidget):
# Handle the response
return response == QMessageBox.Yes
__all__ = ["BookCard"]

View File

@ -61,10 +61,6 @@ class BookOverviewList(QWidget):
register_member_button.clicked.connect(self.register_member)
button_layout.addWidget(register_member_button)
add_borrow_record_button = QPushButton("Add Borrow Record")
add_borrow_record_button.clicked.connect(self.add_borrow_record)
button_layout.addWidget(add_borrow_record_button)
main_layout.addLayout(button_layout)
def filter_books(self, text):

View File

@ -0,0 +1,7 @@
from .category_overview_list import *
from .category_overview_card import *
__all__ = [
*category_overview_list.__all__,
*category_overview_card.__all__
]

View File

@ -47,8 +47,8 @@ class BookCategoryStatisticsOverviewCard(QWidget):
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 = QLabel(str(book_category_statistics_overview.book_count))
status_label.setStyleSheet("font-size: 20px; font-weight: bold;")
status_label.setAlignment(Qt.AlignmentFlag.AlignRight)
right_side.addWidget(status_label)

View File

@ -5,14 +5,11 @@ from PySide6.QtWidgets import (
)
from PySide6.QtCore import Qt
from .book_card import BookCard
from models import BooksOverview
from .category_overview_card import BookCategoryStatisticsOverviewCard
from models import BookCategoryStatisticsOverview
from database.manager import DatabaseManager
from database.book_overview import fetch_all_book_overviews
from ui.editor import MemberEditor
from database import fetch_all_book_category_statistics_overviews
class BookCategoryStatisticsOverview(QWidget):
def __init__(self, parent = None):
@ -47,25 +44,21 @@ class BookCategoryStatisticsOverview(QWidget):
# Align the cards to the top
self.scroll_layout.setAlignment(Qt.AlignTop)
self.books = []
self.book_cards = []
self.category_overviews = []
self.category_overview_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):
for card, category in zip(self.category_overview_cards, self.category_overviews):
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
name_contains_text = text.lower() in category.name.lower()
card.setVisible(title_contains_text or author_name_contains_text or isbn_contains_text)
card.setVisible(name_contains_text)
def clear_layout(self, layout):
@ -82,15 +75,15 @@ class BookCategoryStatisticsOverview(QWidget):
def redraw_cards(self):
self.clear_layout(self.scroll_layout)
self.book_cards = []
self.category_overview_cards = []
self.books = fetch_all_book_overviews()
self.category_overviews = fetch_all_book_category_statistics_overviews()
for book in self.books:
card = BookCard(book)
for category in self.category_overviews:
card = BookCategoryStatisticsOverviewCard(category)
self.scroll_layout.addWidget(card)
self.book_cards.append(card)
self.category_overview_cards.append(card)
__all__ = ["BookCategoryStatisticsOverview"]

View File

@ -2,13 +2,10 @@ from PySide6.QtWidgets import (
QHBoxLayout, QVBoxLayout, QLabel, QWidget, QMenu, QSizePolicy, QLayout, QMessageBox
)
from PySide6.QtGui import Qt
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 ui.editor import MemberEditor
from utils.errors import DatabaseConnectionError, DatabaseError
STATUS_TO_COLOR_MAP = {
@ -41,32 +38,43 @@ class MemberCard(QWidget):
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(10)
# Initialize UI components
self.name_label = QLabel()
self.email_label = QLabel()
self.phone_label = QLabel()
self.status_label = QLabel()
self.register_date_label = QLabel()
# Left-side content
left_side = QVBoxLayout()
layout.addLayout(left_side)
name_label = QLabel(f"{member.first_name} {member.last_name}")
name_label.setStyleSheet("font-size: 20px; font-weight: bold;")
email_label = QLabel(f"Email: {member.email}")
phone_label = QLabel(f"Phone: {member.phone or 'Not Available'}")
left_side.addWidget(name_label)
left_side.addWidget(email_label)
left_side.addWidget(phone_label)
left_side.addWidget(self.name_label)
left_side.addWidget(self.email_label)
left_side.addWidget(self.phone_label)
# Right-side content
right_side = QVBoxLayout()
layout.addLayout(right_side)
status_label = QLabel(str(member.status.value.capitalize()))
status_label.setStyleSheet(f"color: {STATUS_TO_COLOR_MAP[member.status]}; font-size: 20px; font-weight: bold;")
status_label.setAlignment(Qt.AlignmentFlag.AlignRight)
register_date_label = QLabel(f"Registered: {member.register_date}")
register_date_label.setAlignment(Qt.AlignmentFlag.AlignRight)
right_side.addWidget(status_label)
right_side.addWidget(register_date_label)
right_side.addWidget(self.status_label)
right_side.addWidget(self.register_date_label)
self.setLayout(layout)
self.update_display()
def update_display(self):
"""Refreshes the display of the member card based on its current data."""
self.name_label.setText(f"{self.member.first_name} {self.member.last_name}")
self.name_label.setStyleSheet("font-size: 20px; font-weight: bold;")
self.email_label.setText(f"Email: {self.member.email}")
self.phone_label.setText(f"Phone: {self.member.phone or 'Not Available'}")
self.status_label.setText(str(self.member.status.value.capitalize()))
self.status_label.setStyleSheet(f"color: {STATUS_TO_COLOR_MAP[self.member.status]}; font-size: 20px; font-weight: bold;")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignRight)
self.register_date_label.setText(f"Registered: {self.member.register_date}")
self.register_date_label.setAlignment(Qt.AlignmentFlag.AlignRight)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
@ -92,7 +100,8 @@ class MemberCard(QWidget):
action = context_menu.exec_(self.mapToGlobal(event.pos()))
if action == action_edit_member:
print("Edit Member selected") # Implement editor logic here
editor = MemberEditor(self.member, refresh_callback=self.refresh)
editor.exec()
elif action == action_deactivate_member:
self.update_member_status(MemberStatusEnum.inactive)
elif action == action_activate_member:
@ -106,10 +115,9 @@ class MemberCard(QWidget):
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)
QMessageBox.critical(None, "Failed", "Connection with database failed", QMessageBox.StandardButton.Ok)
except DatabaseError as e:
QMessageBox.critical(None, "Failed", f"An error occured when deleting member: {e}", QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.NoButton)
QMessageBox.critical(None, "Failed", f"An error occurred when deleting member: {e}", QMessageBox.StandardButton.Ok)
def update_member_status(self, new_status):
with DatabaseManager.get_session() as session:
@ -120,17 +128,20 @@ class MemberCard(QWidget):
session.commit()
QMessageBox.information(self, "Status Updated", f"Member status updated to {new_status.value.capitalize()}.")
self.member.status = new_status
self.update_status_label()
self.update_display()
else:
QMessageBox.critical(self, "Error", "The member you requested could not be found.", QMessageBox.StandardButton.Ok)
except Exception as e:
session.rollback()
print(e)
def update_status_label(self):
self.findChild(QLabel, self.member.status.value).setStyleSheet(
f"color: {STATUS_TO_COLOR_MAP[self.member.status]}; font-size: 20px; font-weight: bold;"
)
def refresh(self, updated_data):
"""Updates the card's data and refreshes the display."""
self.member.first_name = updated_data["first_name"]
self.member.last_name = updated_data["last_name"]
self.member.email = updated_data["email"]
self.member.phone = updated_data["phone"]
self.update_display()
def make_sure(self) -> bool:
are_you_sure_box = QMessageBox()
@ -143,5 +154,4 @@ class MemberCard(QWidget):
response = are_you_sure_box.exec()
return response == QMessageBox.Yes
__all__ = ["MemberCard"]
__all__ = ["MemberCard"]

View File

@ -7,7 +7,7 @@ from ui.import_preview import PreviewDialog
from ui.editor import BookEditor, MemberEditor
from utils.errors import ExportError, ExportFileError, InvalidContentsError
from services import book_service, book_overview_service
from services import book_service, book_overview_service, book_category_statistics_service
class MenuBar(QMenuBar):
@ -50,10 +50,6 @@ class MenuBar(QMenuBar):
import_books_action.triggered.connect(self.import_books)
import_submenu.addAction(import_books_action)
import_members_action = QAction("Import members", self)
import_members_action.triggered.connect(self.import_members)
import_submenu.addAction(import_members_action)
# Export submenu
export_submenu = QMenu("Export", self)
file_menu.addMenu(export_submenu)
@ -62,13 +58,9 @@ class MenuBar(QMenuBar):
export_books_action.triggered.connect(self.export_books)
export_submenu.addAction(export_books_action)
export_overview_action = QAction("Export overview", self)
export_overview_action.triggered.connect(self.export_overviews)
export_submenu.addAction(export_overview_action)
export_members_action = QAction("Export members", self)
export_members_action.triggered.connect(self.export_members)
export_submenu.addAction(export_members_action)
export_category_statistics = QAction("Export category statistics", self)
export_category_statistics.triggered.connect(self.export_category_statistics)
export_submenu.addAction(export_category_statistics)
file_menu.addSeparator()
@ -109,18 +101,12 @@ class MenuBar(QMenuBar):
def import_books(self):
self.import_data("Book", None, book_service)
def import_members(self):
# self.import_data("Member", memb)
pass
def export_books(self):
self.export_data("Book", book_service)
def export_overviews(self):
self.export_data("Book overview", book_overview_service)
def export_members(self):
pass
def export_category_statistics(self):
self.export_data("Category statistics", book_category_statistics_service)
def about(self):
QMessageBox.information(

View File

@ -24,14 +24,8 @@ class SettingsDialog(QDialog):
self.data_mode_label = QtWidgets.QLabel(UserConfig.get_friendly_name("transaction_level") + ":")
data_mode_layout.addWidget(self.data_mode_label)
self.data_mode_dropdown = QtWidgets.QComboBox()
for tl in TransactionLevel:
self.data_mode_dropdown.addItem(tl.name.capitalize(), tl)
self.data_mode_dropdown.setCurrentIndex(
list(TransactionLevel).index(self.user_config.transaction_level)
)
data_mode_layout.addWidget(self.data_mode_dropdown)
self.data_mode_selected = QtWidgets.QLabel(self.user_config.transaction_level.name.capitalize())
data_mode_layout.addWidget(self.data_mode_selected)
# Slowdown simulation
@ -48,17 +42,8 @@ class SettingsDialog(QDialog):
layout.addLayout(data_mode_layout)
layout.addLayout(self.slowdown_layout)
# Set the currently selected mode to the mode in UserConfig
config = UserConfig()
# Transaction level
current_level = config.transaction_level
index = self.data_mode_dropdown.findData(current_level)
if index != -1:
self.data_mode_dropdown.setCurrentIndex(index)
# Slowdown simulation
simulate_slowdown = config.simulate_slowdown
simulate_slowdown = self.user_config.simulate_slowdown
self.slowdown_checkbox.setChecked(simulate_slowdown)
# Buttons
@ -75,15 +60,8 @@ class SettingsDialog(QDialog):
layout.addLayout(button_layout)
def save_settings(self):
data_mode = self.data_mode_dropdown.currentData()
simulate_slowdown = self.slowdown_checkbox.isChecked()
try:
self.logger.debug("Saving user configuration")
config = UserConfig()
config.transaction_level = data_mode
config.simulate_slowdown = simulate_slowdown
self.accept()
except TypeError as e:
self.logger.error("Invalid user configuration found")
QMessageBox.critical(None, "Invalid config detected", "Double check your configuration", QMessageBox.StandardButton.Ok, QMessageBox.StandardBUttons.NoButton)
self.logger.debug("Saving user configuration")
config = UserConfig()
config.simulate_slowdown = simulate_slowdown
self.accept()

View File

@ -1,7 +1,7 @@
from PySide6.QtGui import QGuiApplication, QIcon
from PySide6.QtWidgets import QMainWindow, QApplication, QTabWidget
from ui.main_tabs import BookOverviewList, MemberList
from ui.main_tabs import BookOverviewList, MemberList, BookCategoryStatisticsOverview
from ui.menu_bar import MenuBar
@ -24,8 +24,10 @@ class LibraryWindow(QMainWindow):
self.dashboard = BookOverviewList(self)
self.member_list = MemberList()
self.category_statistics_overview_list = BookCategoryStatisticsOverview()
central_widget.addTab(self.dashboard, "Dashboard")
central_widget.addTab(self.member_list, "Members")
central_widget.addTab(self.category_statistics_overview_list, "Category stats")
def center_window(self):
# Get the screen geometry
@ -46,6 +48,7 @@ class LibraryWindow(QMainWindow):
def refresh_book_cards(self):
self.dashboard.redraw_cards()
self.category_statistics_overview_list.redraw_cards()
def refresh_member_cards(self):
self.member_list.redraw_cards()

View File

@ -60,11 +60,12 @@ class DatabaseConfig():
class TransactionLevel(enum.Enum):
insecure = "READ UNCOMMITTED"
insecure = "READ COMMITTED"
secure = "SERIALIZABLE"
class UserConfig:
_instance = None
_metadata = {
@ -78,26 +79,23 @@ class UserConfig:
return cls._instance
def __init__(self):
if not hasattr(self, "_transaction_level"):
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__)
if not hasattr(self, "_transaction_level"):
env_level = os.getenv("TRANSACTION_LEVEL", "INSECURE").upper()
if env_level == "SECURE":
self.logger.debug("Running in SECURE mode")
self._transaction_level = TransactionLevel.secure
else:
self.logger.debug("Running in INSECURE mode")
self._transaction_level = TransactionLevel.insecure
if not hasattr(self, "_simulate_slowdown"):
self._simulate_slowdown = False
@property
def transaction_level(self) -> TransactionLevel:
return self._transaction_level
@transaction_level.setter
def transaction_level(self, value: Any):
if not isinstance(value, TransactionLevel):
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
def simulate_slowdown(self) -> bool:
return self._simulate_slowdown

View File

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

View File

@ -1,12 +1,24 @@
import sys
import os
import logging
def setup_logger():
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
verbosity = os.getenv("VERBOSITY", "DEBUG").upper()
level_map = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"CRITICAL": logging.CRITICAL,
}
log_level = level_map.get(verbosity, logging.DEBUG) # Default to DEBUG if invalid
logger.setLevel(log_level)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
handler.setLevel(log_level)
formatter = logging.Formatter("[%(levelname)s] - %(name)s:%(lineno)d - %(message)s")
handler.setFormatter(formatter)