[main] Major changes for submit
This commit is contained in:
		
							parent
							
								
									e452822ffc
								
							
						
					
					
						commit
						104d42201d
					
				@ -2,4 +2,7 @@ DATABASE_HOST=
 | 
				
			|||||||
DATABASE_PORT=
 | 
					DATABASE_PORT=
 | 
				
			||||||
DATABASE_NAME=
 | 
					DATABASE_NAME=
 | 
				
			||||||
DATABASE_USER=
 | 
					DATABASE_USER=
 | 
				
			||||||
DATABASE_PASSWORD=
 | 
					DATABASE_PASSWORD=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TRANSACTION_LEVEL=
 | 
				
			||||||
 | 
					VERBOSITY=
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								db/Model.mwb
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db/Model.mwb
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							@ -6,9 +6,8 @@
 | 
				
			|||||||
                <xs:element name="book" maxOccurs="unbounded">
 | 
					                <xs:element name="book" maxOccurs="unbounded">
 | 
				
			||||||
                    <xs:complexType>
 | 
					                    <xs:complexType>
 | 
				
			||||||
                        <xs:sequence>
 | 
					                        <xs:sequence>
 | 
				
			||||||
                            <xs:element name="title"> <!-- Book title-->
 | 
					                            <xs:element name="title"> <!-- Book title -->
 | 
				
			||||||
                                <xs:simpleType>
 | 
					                                <xs:simpleType>
 | 
				
			||||||
                                    <!-- Allow for a string 2-100 characters long -->
 | 
					 | 
				
			||||||
                                    <xs:restriction base="xs:string">
 | 
					                                    <xs:restriction base="xs:string">
 | 
				
			||||||
                                        <xs:minLength value="2" />
 | 
					                                        <xs:minLength value="2" />
 | 
				
			||||||
                                        <xs:maxLength value="100" />
 | 
					                                        <xs:maxLength value="100" />
 | 
				
			||||||
@ -21,14 +20,14 @@
 | 
				
			|||||||
                                        <xs:element name="first_name">
 | 
					                                        <xs:element name="first_name">
 | 
				
			||||||
                                            <xs:simpleType>
 | 
					                                            <xs:simpleType>
 | 
				
			||||||
                                                <xs:restriction base="xs:string">
 | 
					                                                <xs:restriction base="xs:string">
 | 
				
			||||||
                                                    <xs:maxLength value="50"/>
 | 
					                                                    <xs:maxLength value="50" />
 | 
				
			||||||
                                                </xs:restriction>
 | 
					                                                </xs:restriction>
 | 
				
			||||||
                                            </xs:simpleType>
 | 
					                                            </xs:simpleType>
 | 
				
			||||||
                                        </xs:element>
 | 
					                                        </xs:element>
 | 
				
			||||||
                                        <xs:element name="last_name">
 | 
					                                        <xs:element name="last_name">
 | 
				
			||||||
                                            <xs:simpleType>
 | 
					                                            <xs:simpleType>
 | 
				
			||||||
                                                <xs:restriction base="xs:string">
 | 
					                                                <xs:restriction base="xs:string">
 | 
				
			||||||
                                                    <xs:maxLength value="50"/>
 | 
					                                                    <xs:maxLength value="50" />
 | 
				
			||||||
                                                </xs:restriction>
 | 
					                                                </xs:restriction>
 | 
				
			||||||
                                            </xs:simpleType>
 | 
					                                            </xs:simpleType>
 | 
				
			||||||
                                        </xs:element>
 | 
					                                        </xs:element>
 | 
				
			||||||
@ -50,6 +49,16 @@
 | 
				
			|||||||
                                    </xs:restriction>
 | 
					                                    </xs:restriction>
 | 
				
			||||||
                                </xs:simpleType>
 | 
					                                </xs:simpleType>
 | 
				
			||||||
                            </xs:element>
 | 
					                            </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:element name="categories"> <!-- Categories list -->
 | 
				
			||||||
                                <xs:complexType>
 | 
					                                <xs:complexType>
 | 
				
			||||||
                                    <xs:sequence>
 | 
					                                    <xs:sequence>
 | 
				
			||||||
@ -64,5 +73,4 @@
 | 
				
			|||||||
            </xs:sequence>
 | 
					            </xs:sequence>
 | 
				
			||||||
        </xs:complexType>
 | 
					        </xs:complexType>
 | 
				
			||||||
    </xs:element>
 | 
					    </xs:element>
 | 
				
			||||||
 | 
					</xs:schema>
 | 
				
			||||||
</xs:schema>
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,17 @@
 | 
				
			|||||||
from .manager import *
 | 
					from .manager import *
 | 
				
			||||||
from .book import *
 | 
					from .book import *
 | 
				
			||||||
 | 
					from .book_category import *
 | 
				
			||||||
from .book_category_statistics import *
 | 
					from .book_category_statistics import *
 | 
				
			||||||
 | 
					from .book_category_statistics_overview 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.__all__,
 | 
				
			||||||
    *book_category_statistics.__all__,
 | 
					    *book_category_statistics.__all__,
 | 
				
			||||||
 | 
					    *book_category_statistics_overview.__all__,
 | 
				
			||||||
    *book_overview.__all__,
 | 
					    *book_overview.__all__,
 | 
				
			||||||
    *member.__all__,
 | 
					    *member.__all__,
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
@ -1,37 +1,24 @@
 | 
				
			|||||||
from typing import Dict, List, Optional
 | 
					from typing import Dict, List
 | 
				
			||||||
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 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 .book_category_statistics import update_category_statistics
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from utils.config import UserConfig
 | 
					import logging
 | 
				
			||||||
 | 
					 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
def fetch_all_books() -> List[Book]:
 | 
					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:
 | 
					    with DatabaseManager.get_session() as session:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return session.query(Book) \
 | 
					            return session.query(Book) \
 | 
				
			||||||
                          .options(
 | 
					                          .options(
 | 
				
			||||||
                            joinedload(Book.author),
 | 
					                              joinedload(Book.author),
 | 
				
			||||||
                            joinedload(Book.categories)
 | 
					                              joinedload(Book.categories)
 | 
				
			||||||
                          ) \
 | 
					                          ) \
 | 
				
			||||||
                          .all()
 | 
					                          .all()
 | 
				
			||||||
        except SqlAlchemyDatabaseError as e:
 | 
					        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}")
 | 
					            logger.error(f"An error occurred when fetching all books: {e}")
 | 
				
			||||||
            raise DatabaseError("An error occurred when fetching all books") from 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:
 | 
					def create_books(books: List[Dict[str, object]], skip_existing: bool = True) -> 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.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        with DatabaseManager.get_session() as session:
 | 
					        with DatabaseManager.get_session() as session:
 | 
				
			||||||
            for book in books:
 | 
					            for book in books:
 | 
				
			||||||
                logger.debug(f"Attempting to create a new book: {book['title']}")
 | 
					                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()
 | 
					                existing_book = session.query(Book).filter_by(isbn=book["isbn"]).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if existing_book:
 | 
					                if existing_book:
 | 
				
			||||||
                    logger.warning(f"Book with ISBN {book['isbn']} already exists. Skipping.")
 | 
					                    if skip_existing:
 | 
				
			||||||
                    continue
 | 
					                        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"])
 | 
					                author = get_or_create_author(session, book["author"])
 | 
				
			||||||
                categories = get_or_create_categories(session, book["categories"])
 | 
					                categories = get_or_create_categories(session, book["categories"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # Create the new book
 | 
					 | 
				
			||||||
                new_book = Book(
 | 
					                new_book = Book(
 | 
				
			||||||
                    title=book["title"],
 | 
					                    title=book["title"],
 | 
				
			||||||
                    description=book["description"],
 | 
					                    description=book["description"],
 | 
				
			||||||
                    year_published=book["year_published"],
 | 
					                    year_published=book["year_published"],
 | 
				
			||||||
                    isbn=book["isbn"],
 | 
					                    isbn=book["isbn"],
 | 
				
			||||||
 | 
					                    price=book["price"],
 | 
				
			||||||
 | 
					                    is_damaged=book["is_damaged"],
 | 
				
			||||||
                    author=author,
 | 
					                    author=author,
 | 
				
			||||||
                    categories=categories
 | 
					                    categories=categories
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                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)
 | 
					            update_category_statistics(session)
 | 
				
			||||||
 | 
					 | 
				
			||||||
            session.commit()
 | 
					 | 
				
			||||||
            # logger.info(f"Book {book['title']} successfully created.")
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            logger.debug("Committing all changes")
 | 
					 | 
				
			||||||
            session.commit()
 | 
					            session.commit()
 | 
				
			||||||
    except IntegrityError as e:
 | 
					    except IntegrityError as e:
 | 
				
			||||||
        logger.warning("Data already exists")
 | 
					        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}")
 | 
					        logger.error(f"An error occurred when creating the book: {e}")
 | 
				
			||||||
        raise DatabaseError("An error occurred when creating the book") from e
 | 
					        raise DatabaseError("An error occurred when creating the book") from e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
def update_book(book: Dict[str, object]) -> None:
 | 
					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:
 | 
					    try:
 | 
				
			||||||
        with DatabaseManager.get_session() as session:
 | 
					        with DatabaseManager.get_session() as session:
 | 
				
			||||||
            logger.debug(f"Updating book {book['title']}")
 | 
					            logger.debug(f"Updating book {book['title']}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Find the existing book
 | 
					            existing_book = session.query(Book).filter_by(isbn=book["isbn"]).first()
 | 
				
			||||||
            existing_book = session.query(Book).get(book["id"])
 | 
					
 | 
				
			||||||
            if not existing_book:
 | 
					            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")
 | 
					                raise DatabaseError("Book not found in the database")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Get or create the author
 | 
					 | 
				
			||||||
            author = get_or_create_author(session, book["author"])
 | 
					            author = get_or_create_author(session, book["author"])
 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Get or create the categories
 | 
					 | 
				
			||||||
            categories = get_or_create_categories(session, book["categories"])
 | 
					            categories = get_or_create_categories(session, book["categories"])
 | 
				
			||||||
 | 
					            session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Update the book details
 | 
					 | 
				
			||||||
            existing_book.title = book["title"]
 | 
					            existing_book.title = book["title"]
 | 
				
			||||||
            existing_book.description = book["description"]
 | 
					            existing_book.description = book["description"]
 | 
				
			||||||
            existing_book.year_published = book["year_published"]
 | 
					            existing_book.year_published = book["year_published"]
 | 
				
			||||||
            existing_book.isbn = book["isbn"]
 | 
					            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.author = author
 | 
				
			||||||
            existing_book.categories = categories
 | 
					            existing_book.categories = categories
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            update_category_statistics(session, ignore_config=True)
 | 
				
			||||||
            session.commit()
 | 
					            session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            logger.info(f"{book['title']} successfully updated.")
 | 
					            logger.info(f"{book['title']} successfully updated.")
 | 
				
			||||||
    except IntegrityError as e:
 | 
					    except IntegrityError as e:
 | 
				
			||||||
        logger.warning("Data already exists")
 | 
					        logger.warning("Data already exists")
 | 
				
			||||||
@ -163,9 +116,12 @@ def update_book(book: Dict[str, object]) -> None:
 | 
				
			|||||||
def delete_book(book_id: int) -> None:
 | 
					def delete_book(book_id: int) -> None:
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        with DatabaseManager.get_session() as session:
 | 
					        with DatabaseManager.get_session() as session:
 | 
				
			||||||
 | 
					            logger.debug(f"Deleting book id {book_id}")
 | 
				
			||||||
            stmt = delete(Book).where(Book.id == book_id)
 | 
					            stmt = delete(Book).where(Book.id == book_id)
 | 
				
			||||||
            session.execute(stmt)
 | 
					            session.execute(stmt)
 | 
				
			||||||
 | 
					            update_category_statistics(session, ignore_config=True)
 | 
				
			||||||
            session.commit()
 | 
					            session.commit()
 | 
				
			||||||
 | 
					            logger.info(f"Successfully deleted book with id {book_id}")
 | 
				
			||||||
    except SqlAlchemyDatabaseError as e:
 | 
					    except SqlAlchemyDatabaseError as e:
 | 
				
			||||||
        logger.critical("Connection with database interrupted")
 | 
					        logger.critical("Connection with database interrupted")
 | 
				
			||||||
        raise DatabaseConnectionError("Connection with database interrupted") from e
 | 
					        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}")
 | 
					        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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = ["create_book", "create_books", "update_book", "fetch_all_books"]
 | 
					__all__ = ["create_book", "create_books", "update_book", "fetch_all_books", "delete_book"]
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,9 @@ import logging
 | 
				
			|||||||
from models import BookCategory
 | 
					from models import BookCategory
 | 
				
			||||||
from database.manager import DatabaseManager
 | 
					from database.manager import DatabaseManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from sqlalchemy.orm import Session
 | 
				
			||||||
 | 
					from sqlalchemy import func
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					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
 | 
					            processed_categories[category_name] = new_category
 | 
				
			||||||
            filtered_categories.append(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"]
 | 
				
			||||||
@ -1,43 +1,56 @@
 | 
				
			|||||||
 | 
					from sqlalchemy import func, insert
 | 
				
			||||||
from sqlalchemy.orm import Session
 | 
					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, ignore_config: bool = False) -> None:
 | 
				
			||||||
def update_category_statistics(session: Session) -> None:
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Updates category statistics by calculating the count of books in each category.
 | 
					    Updates category statistics by calculating the count of books in each category.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param session: SQLAlchemy session object.
 | 
					    :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 = (
 | 
					    category_counts = (
 | 
				
			||||||
        session.query(
 | 
					        session.query(
 | 
				
			||||||
            BookCategoryLink.book_category_id, 
 | 
					            BookCategory.id.label('book_category_id'),
 | 
				
			||||||
            func.count(BookCategoryLink.book_id).label('book_count')
 | 
					            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()
 | 
					        .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:
 | 
					    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 = (
 | 
					        existing_statistics = (
 | 
				
			||||||
            session.query(BookCategoryStatistics)
 | 
					            session.query(BookCategoryStatistics)
 | 
				
			||||||
            .filter_by(book_category_id=category_id)
 | 
					            .filter(BookCategoryStatistics.book_category_id == category_id)
 | 
				
			||||||
            .one_or_none()
 | 
					            .one_or_none()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if existing_statistics:
 | 
					        if existing_statistics:
 | 
				
			||||||
            # Update the existing count
 | 
					            # If statistics exist, update the count
 | 
				
			||||||
            existing_statistics.book_count = book_count
 | 
					            existing_statistics.book_count = book_count
 | 
				
			||||||
 | 
					            logger.debug(f"Updated category {category_id} with count {book_count}")
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            # Create new statistics for the category
 | 
					            # If statistics don't exist, create a new one
 | 
				
			||||||
            new_statistics = BookCategoryStatistics(
 | 
					            new_statistics = BookCategoryStatistics(
 | 
				
			||||||
                book_category_id=category_id,
 | 
					                book_category_id=category_id,
 | 
				
			||||||
                book_count=book_count
 | 
					                book_count=book_count
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            session.add(new_statistics)
 | 
					            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"]
 | 
					__all__ = ["update_category_statistics"]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										55
									
								
								src/database/book_category_statistics_overview.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/database/book_category_statistics_overview.py
									
									
									
									
									
										Normal 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"]
 | 
				
			||||||
@ -18,8 +18,8 @@ def fetch_all_book_overviews() -> List[BooksOverview]:
 | 
				
			|||||||
            logger.critical("Connection with database interrupted")
 | 
					            logger.critical("Connection with database interrupted")
 | 
				
			||||||
            raise DatabaseConnectionError("Connection with database interrupted") from e
 | 
					            raise DatabaseConnectionError("Connection with database interrupted") from e
 | 
				
			||||||
        except SQLAlchemyError as e:
 | 
					        except SQLAlchemyError as e:
 | 
				
			||||||
            logger.error(f"An error occured when fetching all books: {e}")
 | 
					            logger.error(f"An error occurred when fetching all books: {e}")
 | 
				
			||||||
            raise DatabaseError("An error occured when fetching all books") from e
 | 
					            raise DatabaseError("An error occurred when fetching all books") from e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = ["fetch_all_book_overviews"]
 | 
					__all__ = ["fetch_all_book_overviews"]
 | 
				
			||||||
 | 
				
			|||||||
@ -22,27 +22,28 @@ class DatabaseManager():
 | 
				
			|||||||
        self.logger = logging.getLogger(__name__)
 | 
					        self.logger = logging.getLogger(__name__)
 | 
				
			||||||
        self.logger.info("Reading database config")
 | 
					        self.logger.info("Reading database config")
 | 
				
			||||||
        database_config = DatabaseConfig()
 | 
					        database_config = DatabaseConfig()
 | 
				
			||||||
 | 
					        user_config = UserConfig()
 | 
				
			||||||
        self.engine = create_engine('mysql+mysqlconnector://%s:%s@%s:%s/%s' % (
 | 
					        self.engine = create_engine('mysql+mysqlconnector://%s:%s@%s:%s/%s' % (
 | 
				
			||||||
            database_config.user,
 | 
					            database_config.user,
 | 
				
			||||||
            database_config.password,
 | 
					            database_config.password,
 | 
				
			||||||
            database_config.host,
 | 
					            database_config.host,
 | 
				
			||||||
            database_config.port,
 | 
					            database_config.port,
 | 
				
			||||||
            database_config.name),
 | 
					            database_config.name),
 | 
				
			||||||
            pool_pre_ping=True)
 | 
					            pool_pre_ping=True,
 | 
				
			||||||
        if self.test_connection():
 | 
					            isolation_level=user_config.transaction_level.value)
 | 
				
			||||||
            self.Session = sessionmaker(bind=self.engine)
 | 
					        self.test_connection()
 | 
				
			||||||
 | 
					        self.Session = sessionmaker(bind=self.engine)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def cleanup(self) -> None:
 | 
					    def cleanup(self) -> None:
 | 
				
			||||||
        self.logger.debug("Closing connection")
 | 
					        self.logger.debug("Closing connection")
 | 
				
			||||||
        self.engine.dispose()
 | 
					        self.engine.dispose()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_connection(self) -> bool:
 | 
					    def test_connection(self):
 | 
				
			||||||
        self.logger.debug("Testing database connection")
 | 
					        self.logger.debug("Testing database connection")
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            with self.engine.connect() as connection:
 | 
					            with self.engine.connect() as connection:
 | 
				
			||||||
                connection.execute(text("select 1"))
 | 
					                connection.execute(text("select 1"))
 | 
				
			||||||
            self.logger.debug("Database connection successful")
 | 
					            self.logger.debug("Database connection successful")
 | 
				
			||||||
            return True
 | 
					 | 
				
			||||||
        except DatabaseError as e:
 | 
					        except DatabaseError as e:
 | 
				
			||||||
            self.logger.critical(f"Database connection failed: {e}")
 | 
					            self.logger.critical(f"Database connection failed: {e}")
 | 
				
			||||||
            raise DatabaseConnectionError("Database connection failed") from e
 | 
					            raise DatabaseConnectionError("Database connection failed") from e
 | 
				
			||||||
 | 
				
			|||||||
@ -67,7 +67,44 @@ def create_members(members: List[Dict[str, str]]):
 | 
				
			|||||||
        raise DatabaseError("An error occurred when creating a new member") from e
 | 
					        raise DatabaseError("An error occurred when creating a new member") from e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def update_member(member: Dict[str, str]):
 | 
					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:
 | 
					def delete_member(member_id: int) -> None:
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
 | 
				
			|||||||
@ -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}")
 | 
					 | 
				
			||||||
@ -6,8 +6,6 @@ from .book_category_statistics_model import *
 | 
				
			|||||||
from .book_category_statistics_overview_model import *
 | 
					from .book_category_statistics_overview_model import *
 | 
				
			||||||
from .book_overview_model import *
 | 
					from .book_overview_model import *
 | 
				
			||||||
from .member_model import *
 | 
					from .member_model import *
 | 
				
			||||||
from .librarian_model import *
 | 
					 | 
				
			||||||
from .loan_model import *
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = [
 | 
					__all__ = [
 | 
				
			||||||
    *author_model.__all__,
 | 
					    *author_model.__all__,
 | 
				
			||||||
@ -18,6 +16,4 @@ __all__ = [
 | 
				
			|||||||
    *book_category_statistics_overview_model.__all__,
 | 
					    *book_category_statistics_overview_model.__all__,
 | 
				
			||||||
    *book_overview_model.__all__,
 | 
					    *book_overview_model.__all__,
 | 
				
			||||||
    *member_model.__all__,
 | 
					    *member_model.__all__,
 | 
				
			||||||
    *librarian_model.__all__,
 | 
					 | 
				
			||||||
    *loan_model.__all__
 | 
					 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
				
			|||||||
@ -8,13 +8,9 @@ class BookCategory(Base):
 | 
				
			|||||||
    __table_args__ = (UniqueConstraint('name'),)
 | 
					    __table_args__ = (UniqueConstraint('name'),)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    id = Column(Integer, primary_key=True, autoincrement=True)
 | 
					    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)
 | 
					    name = Column(String(100), nullable=False)
 | 
				
			||||||
    mature_content = Column(Integer, nullable=False, default=0)
 | 
					 | 
				
			||||||
    last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now())
 | 
					    last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    parent_category = relationship('BookCategory', remote_side=[id])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    books = relationship(
 | 
					    books = relationship(
 | 
				
			||||||
        'Book',
 | 
					        'Book',
 | 
				
			||||||
        secondary='book_category_link',
 | 
					        secondary='book_category_link',
 | 
				
			||||||
 | 
				
			|||||||
@ -8,11 +8,12 @@ class BookCategoryStatisticsOverview(Base):
 | 
				
			|||||||
    __tablename__ = 'book_category_statistics_overview'
 | 
					    __tablename__ = 'book_category_statistics_overview'
 | 
				
			||||||
    __table_args__ = {'extend_existing': True}
 | 
					    __table_args__ = {'extend_existing': True}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name = Column(String)
 | 
					    id = Column(Integer, primary_key=True)
 | 
				
			||||||
    book_count = Column(INTEGER(unsigned=True), default=0)
 | 
					    name             = Column(String)
 | 
				
			||||||
 | 
					    book_count       = Column(INTEGER(unsigned=True), default=0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __repr__(self):
 | 
					    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"]
 | 
					__all__ = ["BookCategoryStatisticsOverview"]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import enum
 | 
					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 sqlalchemy.orm import relationship
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .base_model import Base
 | 
					from .base_model import Base
 | 
				
			||||||
@ -22,6 +22,8 @@ class Book(Base):
 | 
				
			|||||||
    description    = Column(Text, nullable=False)
 | 
					    description    = Column(Text, nullable=False)
 | 
				
			||||||
    year_published = Column(String(4), nullable=False)
 | 
					    year_published = Column(String(4), nullable=False)
 | 
				
			||||||
    isbn           = Column(String(13), nullable=False, unique=True)
 | 
					    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)
 | 
					    status         = Column(Enum(BookStatusEnum), nullable=False, default=BookStatusEnum.available)
 | 
				
			||||||
    created_at     = Column(TIMESTAMP, nullable=False, server_default=func.now())
 | 
					    created_at     = Column(TIMESTAMP, nullable=False, server_default=func.now())
 | 
				
			||||||
    last_updated   = Column(TIMESTAMP, nullable=False, server_default=func.now())
 | 
					    last_updated   = Column(TIMESTAMP, nullable=False, server_default=func.now())
 | 
				
			||||||
 | 
				
			|||||||
@ -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 .base_model import Base
 | 
				
			||||||
from .book_model import BookStatusEnum
 | 
					from .book_model import BookStatusEnum
 | 
				
			||||||
@ -15,12 +15,10 @@ class BooksOverview(Base):
 | 
				
			|||||||
    categories     = Column(Text, nullable=True)
 | 
					    categories     = Column(Text, nullable=True)
 | 
				
			||||||
    year_published = Column(Integer, nullable=True)
 | 
					    year_published = Column(Integer, nullable=True)
 | 
				
			||||||
    isbn           = Column(String, 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)
 | 
					    status         = Column(Enum(BookStatusEnum), nullable=False)
 | 
				
			||||||
    created_at     = Column(TIMESTAMP, 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
 | 
					    # This prevents accidental updates/deletes as it's a view
 | 
				
			||||||
    def __repr__(self):
 | 
					    def __repr__(self):
 | 
				
			||||||
 | 
				
			|||||||
@ -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"]
 | 
					 | 
				
			||||||
@ -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"]
 | 
					 | 
				
			||||||
@ -5,3 +5,4 @@ PySide6_Essentials==6.8.1
 | 
				
			|||||||
python-dotenv==1.0.1
 | 
					python-dotenv==1.0.1
 | 
				
			||||||
SQLAlchemy==2.0.36
 | 
					SQLAlchemy==2.0.36
 | 
				
			||||||
xmlschema==3.4.3
 | 
					xmlschema==3.4.3
 | 
				
			||||||
 | 
					mysql-connector-python==9.1.0
 | 
				
			||||||
							
								
								
									
										61
									
								
								src/services/book_category_statistics_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/services/book_category_statistics_service.py
									
									
									
									
									
										Normal 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
 | 
				
			||||||
@ -58,6 +58,8 @@ def parse_from_xml(file_path: str) -> List[Dict[str, object]]:
 | 
				
			|||||||
            year_published = book_element.find("year_published").text
 | 
					            year_published = book_element.find("year_published").text
 | 
				
			||||||
            description = book_element.find("description").text
 | 
					            description = book_element.find("description").text
 | 
				
			||||||
            isbn = book_element.find("isbn").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
 | 
					            # Parse author
 | 
				
			||||||
            author_element = book_element.find("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,
 | 
					                "year_published" : year_published,
 | 
				
			||||||
                "isbn" : isbn,
 | 
					                "isbn" : isbn,
 | 
				
			||||||
                "author" : author,
 | 
					                "author" : author,
 | 
				
			||||||
 | 
					                "price": price,
 | 
				
			||||||
 | 
					                "is_damaged" : is_damaged,
 | 
				
			||||||
                "categories" : categories,
 | 
					                "categories" : categories,
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            books.append(book)
 | 
					            books.append(book)
 | 
				
			||||||
@ -119,6 +123,12 @@ def books_to_xml(books: List[Book]) -> str:
 | 
				
			|||||||
        isbn_element = ET.SubElement(book_element, "isbn")
 | 
					        isbn_element = ET.SubElement(book_element, "isbn")
 | 
				
			||||||
        isbn_element.text = book.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>
 | 
					        # Add <categories>
 | 
				
			||||||
        categories_element = ET.SubElement(book_element, "categories")
 | 
					        categories_element = ET.SubElement(book_element, "categories")
 | 
				
			||||||
        for category in book.categories:
 | 
					        for category in book.categories:
 | 
				
			||||||
@ -133,4 +143,4 @@ def books_to_xml(books: List[Book]) -> str:
 | 
				
			|||||||
    return pretty_xml
 | 
					    return pretty_xml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = ["export_to_xml", "parse_books_from_xml"]
 | 
					__all__ = ["export_to_xml", "parse_from_xml"]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
from typing import Dict
 | 
					from typing import Dict, Callable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PySide6.QtWidgets import (
 | 
					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.QtGui import QRegularExpressionValidator
 | 
				
			||||||
from PySide6.QtCore import QRegularExpression
 | 
					from PySide6.QtCore import QRegularExpression
 | 
				
			||||||
@ -15,12 +15,14 @@ from utils.errors.database import DatabaseError, DatabaseConnectionError, Duplic
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BookEditor(QDialog):
 | 
					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)
 | 
					        super().__init__(parent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.logger = logging.getLogger(__name__)
 | 
					        self.logger = logging.getLogger(__name__)
 | 
				
			||||||
        self.create_layout()
 | 
					        self.create_layout()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.refresh_callback = refresh_callback
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if book:
 | 
					        if book:
 | 
				
			||||||
            self.book_id = book.id
 | 
					            self.book_id = book.id
 | 
				
			||||||
            self.logger.debug(f"Editing book {book.title}")
 | 
					            self.logger.debug(f"Editing book {book.title}")
 | 
				
			||||||
@ -54,7 +56,6 @@ class BookEditor(QDialog):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # Year published field
 | 
					        # Year published field
 | 
				
			||||||
        self.year_input = QLineEdit()
 | 
					        self.year_input = QLineEdit()
 | 
				
			||||||
        # self.year_input.setValidator
 | 
					 | 
				
			||||||
        form_layout.addRow("Year Published:", self.year_input)
 | 
					        form_layout.addRow("Year Published:", self.year_input)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # ISBN field
 | 
					        # ISBN field
 | 
				
			||||||
@ -64,9 +65,24 @@ class BookEditor(QDialog):
 | 
				
			|||||||
        self.isbn_input.setValidator(self.isbn_validator)
 | 
					        self.isbn_input.setValidator(self.isbn_validator)
 | 
				
			||||||
        form_layout.addRow("ISBN:", self.isbn_input)
 | 
					        form_layout.addRow("ISBN:", self.isbn_input)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Categories field
 | 
				
			||||||
        self.categories_input = QLineEdit()
 | 
					        self.categories_input = QLineEdit()
 | 
				
			||||||
        form_layout.addRow("Categories: ", self.categories_input)
 | 
					        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)
 | 
					        layout.addLayout(form_layout)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Buttons
 | 
					        # Buttons
 | 
				
			||||||
@ -94,21 +110,28 @@ class BookEditor(QDialog):
 | 
				
			|||||||
        all_categories = ", ".join(category.name for category in book.categories)
 | 
					        all_categories = ", ".join(category.name for category in book.categories)
 | 
				
			||||||
        self.categories_input.setText(all_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):
 | 
					    def save_book(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            book_object = self.parse_inputs()
 | 
					            book_object = self.parse_inputs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if self.create_new:
 | 
					            if self.create_new:
 | 
				
			||||||
                create_book(book_object)
 | 
					                create_book(book_object, skip_existing=False)
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                book_object["id"] = self.book_id
 | 
					                book_object["id"] = self.book_id
 | 
				
			||||||
                update_book(book_object)
 | 
					                update_book(book_object)
 | 
				
			||||||
            
 | 
					
 | 
				
			||||||
            QMessageBox.information(None,
 | 
					            QMessageBox.information(None,
 | 
				
			||||||
                                    "Success",
 | 
					                                    "Success",
 | 
				
			||||||
                                    "Book updated successfully",
 | 
					                                    "Book updated successfully",
 | 
				
			||||||
                                    QMessageBox.StandardButton.Ok)
 | 
					                                    QMessageBox.StandardButton.Ok)
 | 
				
			||||||
                
 | 
					
 | 
				
			||||||
 | 
					            if self.refresh_callback:
 | 
				
			||||||
 | 
					                self.refresh_callback(book_object)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.accept()
 | 
					            self.accept()
 | 
				
			||||||
        except ValueError as e:
 | 
					        except ValueError as e:
 | 
				
			||||||
            QMessageBox.critical(None,
 | 
					            QMessageBox.critical(None,
 | 
				
			||||||
@ -168,6 +191,21 @@ class BookEditor(QDialog):
 | 
				
			|||||||
        if not categories:
 | 
					        if not categories:
 | 
				
			||||||
            raise ValueError("At least one category must be specified.")
 | 
					            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
 | 
					        # Map parsed values to dictionary format for saving
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            "title": title,
 | 
					            "title": title,
 | 
				
			||||||
@ -178,7 +216,10 @@ class BookEditor(QDialog):
 | 
				
			|||||||
            "description": description,
 | 
					            "description": description,
 | 
				
			||||||
            "year_published": year_published,
 | 
					            "year_published": year_published,
 | 
				
			||||||
            "isbn": isbn,
 | 
					            "isbn": isbn,
 | 
				
			||||||
            "categories": categories
 | 
					            "categories": categories,
 | 
				
			||||||
 | 
					            "is_damaged": damage,
 | 
				
			||||||
 | 
					            "price": price,
 | 
				
			||||||
 | 
					            "status": status
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = ["BookEditor"]
 | 
					__all__ = ["BookEditor"]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import logging
 | 
					import logging
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
from typing import Dict
 | 
					from typing import Dict, Callable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PySide6.QtGui import QGuiApplication, QAction
 | 
					from PySide6.QtGui import QGuiApplication, QAction
 | 
				
			||||||
from PySide6.QtQml import QQmlApplicationEngine
 | 
					from PySide6.QtQml import QQmlApplicationEngine
 | 
				
			||||||
@ -15,16 +15,18 @@ from utils.errors.database import DatabaseError, DatabaseConnectionError, Duplic
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MemberEditor(QDialog):
 | 
					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__()
 | 
					        super().__init__()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.logger = logging.getLogger(__name__)
 | 
					        self.logger = logging.getLogger(__name__)
 | 
				
			||||||
        self.create_layout()
 | 
					        self.create_layout()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.refresh_callback = refresh_callback
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if member:
 | 
					        if member:
 | 
				
			||||||
            self.member_id = member.id
 | 
					            self.member_id = member.id
 | 
				
			||||||
            self.logger.debug(f"Editing member {member.first_name} {member.last_name}")
 | 
					            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
 | 
					            self.create_new = False
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            self.logger.debug("Editing a new member")
 | 
					            self.logger.debug("Editing a new member")
 | 
				
			||||||
@ -90,13 +92,16 @@ class MemberEditor(QDialog):
 | 
				
			|||||||
                                        QMessageBox.StandardButton.NoButton)
 | 
					                                        QMessageBox.StandardButton.NoButton)
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                member_object["id"] = self.member_id
 | 
					                member_object["id"] = self.member_id
 | 
				
			||||||
                update_member(book_object)
 | 
					                update_member(member_object)
 | 
				
			||||||
                QMessageBox.information(None,
 | 
					                QMessageBox.information(None,
 | 
				
			||||||
                                        "Success",
 | 
					                                        "Success",
 | 
				
			||||||
                                        "Member updated successfully",
 | 
					                                        "Member updated successfully",
 | 
				
			||||||
                                        QMessageBox.StandardButton.Ok,
 | 
					                                        QMessageBox.StandardButton.Ok,
 | 
				
			||||||
                                        QMessageBox.StandardButton.NoButton)
 | 
					                                        QMessageBox.StandardButton.NoButton)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if self.refresh_callback:
 | 
				
			||||||
 | 
					                self.refresh_callback(member_object)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.accept()
 | 
					            self.accept()
 | 
				
			||||||
        except ValueError as e:
 | 
					        except ValueError as e:
 | 
				
			||||||
            QMessageBox.critical(None,
 | 
					            QMessageBox.critical(None,
 | 
				
			||||||
@ -144,8 +149,7 @@ class MemberEditor(QDialog):
 | 
				
			|||||||
            "first_name": first_name,
 | 
					            "first_name": first_name,
 | 
				
			||||||
            "last_name": last_name,
 | 
					            "last_name": last_name,
 | 
				
			||||||
            "email": email,
 | 
					            "email": email,
 | 
				
			||||||
            "phone_number": phone_number
 | 
					            "phone": phone_number
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
__all__ = ["MemberEditor"]
 | 
					__all__ = ["MemberEditor"]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,3 @@
 | 
				
			|||||||
from .book_overview_list import BookOverviewList
 | 
					from .book_overview_list import BookOverviewList
 | 
				
			||||||
from .member_list import MemberList
 | 
					from .member_list import MemberList
 | 
				
			||||||
 | 
					from .category_statistics_overview_list import BookCategoryStatisticsOverview
 | 
				
			||||||
@ -1,15 +1,10 @@
 | 
				
			|||||||
from PySide6.QtGui import QGuiApplication, QAction, Qt
 | 
					from PySide6.QtGui import QAction, Qt
 | 
				
			||||||
from PySide6.QtQml import QQmlApplicationEngine
 | 
					from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QWidget, QMenu, QSizePolicy, QLayout, QMessageBox, QDialog
 | 
				
			||||||
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 BooksOverview, Book, BookStatusEnum
 | 
					from models import BooksOverview, Book, BookStatusEnum
 | 
				
			||||||
 | 
					from ui.editor import BookEditor
 | 
				
			||||||
from database.manager import DatabaseManager
 | 
					from database.manager import DatabaseManager
 | 
				
			||||||
 | 
					from database import delete_book
 | 
				
			||||||
from sqlalchemy import delete
 | 
					from utils.errors import DatabaseConnectionError, DatabaseError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
STATUS_TO_COLOR_MAP = {
 | 
					STATUS_TO_COLOR_MAP = {
 | 
				
			||||||
    BookStatusEnum.available: "#3c702e",
 | 
					    BookStatusEnum.available: "#3c702e",
 | 
				
			||||||
@ -24,10 +19,8 @@ class BookCard(QWidget):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.book_overview = book_overview
 | 
					        self.book_overview = book_overview
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.setAttribute(Qt.WidgetAttribute.WA_Hover,
 | 
					        self.setAttribute(Qt.WidgetAttribute.WA_Hover, True)  # Enable hover events
 | 
				
			||||||
                          True)  # Enable hover events
 | 
					        self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)  # Enable styling for background
 | 
				
			||||||
        # Enable styling for background
 | 
					 | 
				
			||||||
        self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Set initial stylesheet with hover behavior
 | 
					        # Set initial stylesheet with hover behavior
 | 
				
			||||||
        self.setStyleSheet("""
 | 
					        self.setStyleSheet("""
 | 
				
			||||||
@ -39,106 +32,104 @@ class BookCard(QWidget):
 | 
				
			|||||||
        # Layout setup
 | 
					        # Layout setup
 | 
				
			||||||
        layout = QHBoxLayout(self)
 | 
					        layout = QHBoxLayout(self)
 | 
				
			||||||
        layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
 | 
					        layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
 | 
				
			||||||
        self.setSizePolicy(QSizePolicy.Policy.Preferred,
 | 
					        self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
 | 
				
			||||||
                           QSizePolicy.Policy.Fixed)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        layout.setContentsMargins(10, 10, 10, 10)
 | 
					        layout.setContentsMargins(10, 10, 10, 10)
 | 
				
			||||||
        layout.setSpacing(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 content
 | 
				
			||||||
        left_side = QVBoxLayout()
 | 
					        left_side = QVBoxLayout()
 | 
				
			||||||
        layout.addLayout(left_side)
 | 
					        layout.addLayout(left_side)
 | 
				
			||||||
        title_label = QLabel(book_overview.title)
 | 
					        left_side.addWidget(self.title_label)
 | 
				
			||||||
        title_label.setStyleSheet("font-size: 20px; font-weight: bold;")
 | 
					        left_side.addWidget(self.author_label)
 | 
				
			||||||
        author_label = QLabel("By: " + book_overview.author_name)
 | 
					        left_side.addWidget(self.isbn_label)
 | 
				
			||||||
        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)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Right-side content
 | 
					        # Right-side content
 | 
				
			||||||
        right_side = QVBoxLayout()
 | 
					        right_side = QVBoxLayout()
 | 
				
			||||||
        layout.addLayout(right_side)
 | 
					        layout.addLayout(right_side)
 | 
				
			||||||
 | 
					        right_side.addWidget(self.status_label)
 | 
				
			||||||
        status_label = QLabel(str(book_overview.status.value.capitalize()))
 | 
					        right_side.addWidget(self.price_label)
 | 
				
			||||||
        status_label.setStyleSheet(f"color: {
 | 
					        right_side.addWidget(self.is_damaged_label)
 | 
				
			||||||
                                   STATUS_TO_COLOR_MAP[book_overview.status]}; font-size: 20px; font-weight: bold;")
 | 
					 | 
				
			||||||
        status_label.setAlignment(Qt.AlignmentFlag.AlignRight)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        right_side.addWidget(status_label)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if book_overview.librarian_name and book_overview.borrower_name:
 | 
					 | 
				
			||||||
            borrower_label = QLabel("Borrowed: " + book_overview.borrower_name)
 | 
					 | 
				
			||||||
            borrower_label.setAlignment(Qt.AlignmentFlag.AlignRight)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            librarian_label = QLabel("By: " + book_overview.librarian_name)
 | 
					 | 
				
			||||||
            librarian_label.setAlignment(Qt.AlignmentFlag.AlignRight)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            right_side.addWidget(borrower_label)
 | 
					 | 
				
			||||||
            right_side.addWidget(librarian_label)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.setLayout(layout)
 | 
					        self.setLayout(layout)
 | 
				
			||||||
 | 
					        self.update_display()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def mousePressEvent(self, event):
 | 
					    def update_display(self):
 | 
				
			||||||
        if event.button() == Qt.MouseButton.LeftButton:
 | 
					        """Refreshes the display of the book card based on its current data."""
 | 
				
			||||||
            self.contextMenuEvent(event)
 | 
					        self.title_label.setText(self.book_overview.title)
 | 
				
			||||||
        else:
 | 
					        self.title_label.setStyleSheet("font-size: 20px; font-weight: bold;")
 | 
				
			||||||
            super().mousePressEvent(event)
 | 
					
 | 
				
			||||||
 | 
					        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):
 | 
					    def contextMenuEvent(self, event):
 | 
				
			||||||
        context_menu = QMenu(self)
 | 
					        context_menu = QMenu(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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_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 = context_menu.addAction("Delete Book")
 | 
				
			||||||
        delete_book_action.triggered.connect(self.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()))
 | 
					        action = context_menu.exec_(self.mapToGlobal(event.pos()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if action == action_edit_book:
 | 
					        if action == action_edit_book:
 | 
				
			||||||
            with DatabaseManager.get_session() as session:
 | 
					            self.open_editor()
 | 
				
			||||||
                book_id = self.book_overview.id
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                book = session.query(Book).filter(
 | 
					    def open_editor(self):
 | 
				
			||||||
                    Book.id == book_id).one_or_none()
 | 
					        """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:
 | 
					            if book:
 | 
				
			||||||
                    BookEditor(book).exec()
 | 
					                editor = BookEditor(book)
 | 
				
			||||||
                else:
 | 
					                if editor.exec() == QDialog.DialogCode.Accepted:
 | 
				
			||||||
                    QMessageBox.critical(self,
 | 
					                    updated_data = editor.parse_inputs()
 | 
				
			||||||
                                         "Error",
 | 
					                    self.refresh(updated_data)
 | 
				
			||||||
                                         "The book you requested could not be found. Try again later",
 | 
					            else:
 | 
				
			||||||
                                         QMessageBox.StandardButton.Ok,
 | 
					                QMessageBox.critical(self,
 | 
				
			||||||
                                         QMessageBox.StandardButton.NoButton)
 | 
					                                     "Error",
 | 
				
			||||||
        elif action == action_edit_author:
 | 
					                                     "The book you requested could not be found. Try again later",
 | 
				
			||||||
            print("Edit Author selected")
 | 
					                                     QMessageBox.StandardButton.Ok)
 | 
				
			||||||
        elif action == action_mark_returned:
 | 
					
 | 
				
			||||||
            print("Mark as Returned selected")
 | 
					    def refresh(self, updated_data):
 | 
				
			||||||
        elif action == action_remove_reservation:
 | 
					        """Updates the card's data and refreshes the display."""
 | 
				
			||||||
            print("Remove reservation selected")
 | 
					        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):
 | 
					    def delete_book(self):
 | 
				
			||||||
        if not self.make_sure():
 | 
					        if not self.make_sure():
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        with DatabaseManager.get_session() as session:
 | 
					        try:
 | 
				
			||||||
            try:
 | 
					            delete_book(self.book_overview.id)
 | 
				
			||||||
                stmt = delete(Book).where(Book.id == self.book_overview.id)
 | 
					            self.setVisible(False)
 | 
				
			||||||
                session.execute(stmt)
 | 
					        except DatabaseConnectionError as e:
 | 
				
			||||||
                session.commit()
 | 
					            QMessageBox.critical(None, "Failed", "Connection with database failed", QMessageBox.StandardButton.Ok)
 | 
				
			||||||
                self.setVisible(False)
 | 
					        except DatabaseError as e:
 | 
				
			||||||
            except Exception as e:
 | 
					            QMessageBox.critical(None, "Failed", f"An error occurred when deleting book: {e}", QMessageBox.StandardButton.Ok)
 | 
				
			||||||
                session.rollback
 | 
					 | 
				
			||||||
                print(e)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_sure(self) -> bool:
 | 
					    def make_sure(self) -> bool:
 | 
				
			||||||
        are_you_sure_box = QMessageBox()
 | 
					        are_you_sure_box = QMessageBox()
 | 
				
			||||||
@ -154,5 +145,4 @@ class BookCard(QWidget):
 | 
				
			|||||||
        # Handle the response
 | 
					        # Handle the response
 | 
				
			||||||
        return response == QMessageBox.Yes
 | 
					        return response == QMessageBox.Yes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
__all__ = ["BookCard"]
 | 
					__all__ = ["BookCard"]
 | 
				
			||||||
 | 
				
			|||||||
@ -61,10 +61,6 @@ class BookOverviewList(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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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)
 | 
					        main_layout.addLayout(button_layout)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def filter_books(self, text):
 | 
					    def filter_books(self, text):
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					from .category_overview_list import *
 | 
				
			||||||
 | 
					from .category_overview_card import *
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__all__ = [
 | 
				
			||||||
 | 
					    *category_overview_list.__all__,
 | 
				
			||||||
 | 
					    *category_overview_card.__all__
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
@ -47,8 +47,8 @@ class BookCategoryStatisticsOverviewCard(QWidget):
 | 
				
			|||||||
        right_side = QVBoxLayout()
 | 
					        right_side = QVBoxLayout()
 | 
				
			||||||
        layout.addLayout(right_side)
 | 
					        layout.addLayout(right_side)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        status_label = QLabel(book_category_statistics_overview.book_count)
 | 
					        status_label = QLabel(str(book_category_statistics_overview.book_count))
 | 
				
			||||||
        status_label.setStyleSheet(f"font-size: 20px; font-weight: bold;")
 | 
					        status_label.setStyleSheet("font-size: 20px; font-weight: bold;")
 | 
				
			||||||
        status_label.setAlignment(Qt.AlignmentFlag.AlignRight)
 | 
					        status_label.setAlignment(Qt.AlignmentFlag.AlignRight)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        right_side.addWidget(status_label)
 | 
					        right_side.addWidget(status_label)
 | 
				
			||||||
 | 
				
			|||||||
@ -5,14 +5,11 @@ from PySide6.QtWidgets import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
from PySide6.QtCore import Qt
 | 
					from PySide6.QtCore import Qt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .book_card import BookCard
 | 
					from .category_overview_card import BookCategoryStatisticsOverviewCard
 | 
				
			||||||
from models import BooksOverview
 | 
					from models import BookCategoryStatisticsOverview
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from database.manager import DatabaseManager
 | 
					from database.manager import DatabaseManager
 | 
				
			||||||
from database.book_overview import fetch_all_book_overviews
 | 
					from database import fetch_all_book_category_statistics_overviews
 | 
				
			||||||
 | 
					 | 
				
			||||||
from ui.editor import MemberEditor
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BookCategoryStatisticsOverview(QWidget):
 | 
					class BookCategoryStatisticsOverview(QWidget):
 | 
				
			||||||
    def __init__(self, parent = None):
 | 
					    def __init__(self, parent = None):
 | 
				
			||||||
@ -47,25 +44,21 @@ class BookCategoryStatisticsOverview(QWidget):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # Align the cards to the top
 | 
					        # Align the cards to the top
 | 
				
			||||||
        self.scroll_layout.setAlignment(Qt.AlignTop)
 | 
					        self.scroll_layout.setAlignment(Qt.AlignTop)
 | 
				
			||||||
        self.books = []
 | 
					        self.category_overviews = []
 | 
				
			||||||
        self.book_cards = []
 | 
					        self.category_overview_cards = []
 | 
				
			||||||
        self.redraw_cards()
 | 
					        self.redraw_cards()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.scroll_widget.setLayout(self.scroll_layout)
 | 
					        self.scroll_widget.setLayout(self.scroll_layout)
 | 
				
			||||||
        self.scroll_area.setWidget(self.scroll_widget)
 | 
					        self.scroll_area.setWidget(self.scroll_widget)
 | 
				
			||||||
        main_layout.addWidget(self.scroll_area)
 | 
					        main_layout.addWidget(self.scroll_area)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        main_layout.addLayout(button_layout)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def filter_categories(self, text):
 | 
					    def filter_categories(self, text):
 | 
				
			||||||
        """Filter the cards based on the search input."""
 | 
					        """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()
 | 
					            name_contains_text = text.lower() in category.name.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)
 | 
					            card.setVisible(name_contains_text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clear_layout(self, layout):
 | 
					    def clear_layout(self, layout):
 | 
				
			||||||
@ -82,15 +75,15 @@ class BookCategoryStatisticsOverview(QWidget):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def redraw_cards(self):
 | 
					    def redraw_cards(self):
 | 
				
			||||||
        self.clear_layout(self.scroll_layout)
 | 
					        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:
 | 
					        for category in self.category_overviews:
 | 
				
			||||||
            card = BookCard(book)
 | 
					            card = BookCategoryStatisticsOverviewCard(category)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.scroll_layout.addWidget(card)
 | 
					            self.scroll_layout.addWidget(card)
 | 
				
			||||||
            self.book_cards.append(card)
 | 
					            self.category_overview_cards.append(card)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = ["BookCategoryStatisticsOverview"]
 | 
					__all__ = ["BookCategoryStatisticsOverview"]
 | 
				
			||||||
 | 
				
			|||||||
@ -2,13 +2,10 @@ from PySide6.QtWidgets import (
 | 
				
			|||||||
    QHBoxLayout, QVBoxLayout, QLabel, QWidget, QMenu, QSizePolicy, QLayout, QMessageBox
 | 
					    QHBoxLayout, QVBoxLayout, QLabel, QWidget, QMenu, QSizePolicy, QLayout, QMessageBox
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from PySide6.QtGui import Qt
 | 
					from PySide6.QtGui import Qt
 | 
				
			||||||
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 database import delete_member
 | 
				
			||||||
from sqlalchemy import delete
 | 
					from ui.editor import MemberEditor
 | 
				
			||||||
 | 
					 | 
				
			||||||
from utils.errors import DatabaseConnectionError, DatabaseError
 | 
					from utils.errors import DatabaseConnectionError, DatabaseError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
STATUS_TO_COLOR_MAP = {
 | 
					STATUS_TO_COLOR_MAP = {
 | 
				
			||||||
@ -41,32 +38,43 @@ class MemberCard(QWidget):
 | 
				
			|||||||
        layout.setContentsMargins(10, 10, 10, 10)
 | 
					        layout.setContentsMargins(10, 10, 10, 10)
 | 
				
			||||||
        layout.setSpacing(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 content
 | 
				
			||||||
        left_side = QVBoxLayout()
 | 
					        left_side = QVBoxLayout()
 | 
				
			||||||
        layout.addLayout(left_side)
 | 
					        layout.addLayout(left_side)
 | 
				
			||||||
        name_label = QLabel(f"{member.first_name} {member.last_name}")
 | 
					        left_side.addWidget(self.name_label)
 | 
				
			||||||
        name_label.setStyleSheet("font-size: 20px; font-weight: bold;")
 | 
					        left_side.addWidget(self.email_label)
 | 
				
			||||||
        email_label = QLabel(f"Email: {member.email}")
 | 
					        left_side.addWidget(self.phone_label)
 | 
				
			||||||
        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)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Right-side content
 | 
					        # Right-side content
 | 
				
			||||||
        right_side = QVBoxLayout()
 | 
					        right_side = QVBoxLayout()
 | 
				
			||||||
        layout.addLayout(right_side)
 | 
					        layout.addLayout(right_side)
 | 
				
			||||||
 | 
					        right_side.addWidget(self.status_label)
 | 
				
			||||||
        status_label = QLabel(str(member.status.value.capitalize()))
 | 
					        right_side.addWidget(self.register_date_label)
 | 
				
			||||||
        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)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.setLayout(layout)
 | 
					        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):
 | 
					    def mousePressEvent(self, event):
 | 
				
			||||||
        if event.button() == Qt.MouseButton.LeftButton:
 | 
					        if event.button() == Qt.MouseButton.LeftButton:
 | 
				
			||||||
@ -92,7 +100,8 @@ class MemberCard(QWidget):
 | 
				
			|||||||
        action = context_menu.exec_(self.mapToGlobal(event.pos()))
 | 
					        action = context_menu.exec_(self.mapToGlobal(event.pos()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if action == action_edit_member:
 | 
					        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:
 | 
					        elif action == action_deactivate_member:
 | 
				
			||||||
            self.update_member_status(MemberStatusEnum.inactive)
 | 
					            self.update_member_status(MemberStatusEnum.inactive)
 | 
				
			||||||
        elif action == action_activate_member:
 | 
					        elif action == action_activate_member:
 | 
				
			||||||
@ -106,10 +115,9 @@ class MemberCard(QWidget):
 | 
				
			|||||||
            delete_member(self.member.id)
 | 
					            delete_member(self.member.id)
 | 
				
			||||||
            self.setVisible(False)
 | 
					            self.setVisible(False)
 | 
				
			||||||
        except DatabaseConnectionError as e:
 | 
					        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:
 | 
					        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):
 | 
					    def update_member_status(self, new_status):
 | 
				
			||||||
        with DatabaseManager.get_session() as session:
 | 
					        with DatabaseManager.get_session() as session:
 | 
				
			||||||
@ -120,17 +128,20 @@ class MemberCard(QWidget):
 | 
				
			|||||||
                    session.commit()
 | 
					                    session.commit()
 | 
				
			||||||
                    QMessageBox.information(self, "Status Updated", f"Member status updated to {new_status.value.capitalize()}.")
 | 
					                    QMessageBox.information(self, "Status Updated", f"Member status updated to {new_status.value.capitalize()}.")
 | 
				
			||||||
                    self.member.status = new_status
 | 
					                    self.member.status = new_status
 | 
				
			||||||
                    self.update_status_label()
 | 
					                    self.update_display()
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    QMessageBox.critical(self, "Error", "The member you requested could not be found.", QMessageBox.StandardButton.Ok)
 | 
					                    QMessageBox.critical(self, "Error", "The member you requested could not be found.", QMessageBox.StandardButton.Ok)
 | 
				
			||||||
            except Exception as e:
 | 
					            except Exception as e:
 | 
				
			||||||
                session.rollback()
 | 
					                session.rollback()
 | 
				
			||||||
                print(e)
 | 
					                print(e)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_status_label(self):
 | 
					    def refresh(self, updated_data):
 | 
				
			||||||
        self.findChild(QLabel, self.member.status.value).setStyleSheet(
 | 
					        """Updates the card's data and refreshes the display."""
 | 
				
			||||||
            f"color: {STATUS_TO_COLOR_MAP[self.member.status]}; font-size: 20px; font-weight: bold;"
 | 
					        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:
 | 
					    def make_sure(self) -> bool:
 | 
				
			||||||
        are_you_sure_box = QMessageBox()
 | 
					        are_you_sure_box = QMessageBox()
 | 
				
			||||||
@ -143,5 +154,4 @@ class MemberCard(QWidget):
 | 
				
			|||||||
        response = are_you_sure_box.exec()
 | 
					        response = are_you_sure_box.exec()
 | 
				
			||||||
        return response == QMessageBox.Yes
 | 
					        return response == QMessageBox.Yes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__all__ = ["MemberCard"]
 | 
				
			||||||
__all__ = ["MemberCard"]
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from ui.import_preview import PreviewDialog
 | 
				
			|||||||
from ui.editor import BookEditor, MemberEditor
 | 
					from ui.editor import BookEditor, MemberEditor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from utils.errors import ExportError, ExportFileError, InvalidContentsError
 | 
					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):
 | 
					class MenuBar(QMenuBar):
 | 
				
			||||||
@ -50,10 +50,6 @@ class MenuBar(QMenuBar):
 | 
				
			|||||||
        import_books_action.triggered.connect(self.import_books)
 | 
					        import_books_action.triggered.connect(self.import_books)
 | 
				
			||||||
        import_submenu.addAction(import_books_action)
 | 
					        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
 | 
				
			||||||
        export_submenu = QMenu("Export", self)
 | 
					        export_submenu = QMenu("Export", self)
 | 
				
			||||||
        file_menu.addMenu(export_submenu)
 | 
					        file_menu.addMenu(export_submenu)
 | 
				
			||||||
@ -62,13 +58,9 @@ class MenuBar(QMenuBar):
 | 
				
			|||||||
        export_books_action.triggered.connect(self.export_books)
 | 
					        export_books_action.triggered.connect(self.export_books)
 | 
				
			||||||
        export_submenu.addAction(export_books_action)
 | 
					        export_submenu.addAction(export_books_action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        export_overview_action = QAction("Export overview", self)
 | 
					        export_category_statistics = QAction("Export category statistics", self)
 | 
				
			||||||
        export_overview_action.triggered.connect(self.export_overviews)
 | 
					        export_category_statistics.triggered.connect(self.export_category_statistics)
 | 
				
			||||||
        export_submenu.addAction(export_overview_action)
 | 
					        export_submenu.addAction(export_category_statistics)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        export_members_action = QAction("Export members", self)
 | 
					 | 
				
			||||||
        export_members_action.triggered.connect(self.export_members)
 | 
					 | 
				
			||||||
        export_submenu.addAction(export_members_action)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        file_menu.addSeparator()
 | 
					        file_menu.addSeparator()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -109,18 +101,12 @@ class MenuBar(QMenuBar):
 | 
				
			|||||||
    def import_books(self):
 | 
					    def import_books(self):
 | 
				
			||||||
        self.import_data("Book", None, book_service)
 | 
					        self.import_data("Book", None, book_service)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def import_members(self):
 | 
					 | 
				
			||||||
        # 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def export_overviews(self):
 | 
					 | 
				
			||||||
        self.export_data("Book overview", book_overview_service)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def export_members(self):
 | 
					    def export_category_statistics(self):
 | 
				
			||||||
        pass
 | 
					        self.export_data("Category statistics", book_category_statistics_service)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def about(self):
 | 
					    def about(self):
 | 
				
			||||||
        QMessageBox.information(
 | 
					        QMessageBox.information(
 | 
				
			||||||
 | 
				
			|||||||
@ -24,14 +24,8 @@ class SettingsDialog(QDialog):
 | 
				
			|||||||
        self.data_mode_label = QtWidgets.QLabel(UserConfig.get_friendly_name("transaction_level") + ":")
 | 
					        self.data_mode_label = QtWidgets.QLabel(UserConfig.get_friendly_name("transaction_level") + ":")
 | 
				
			||||||
        data_mode_layout.addWidget(self.data_mode_label)
 | 
					        data_mode_layout.addWidget(self.data_mode_label)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.data_mode_dropdown = QtWidgets.QComboBox()
 | 
					        self.data_mode_selected = QtWidgets.QLabel(self.user_config.transaction_level.name.capitalize())
 | 
				
			||||||
        for tl in TransactionLevel:
 | 
					        data_mode_layout.addWidget(self.data_mode_selected)
 | 
				
			||||||
            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)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Slowdown simulation
 | 
					        # Slowdown simulation
 | 
				
			||||||
@ -48,17 +42,8 @@ class SettingsDialog(QDialog):
 | 
				
			|||||||
        layout.addLayout(data_mode_layout)
 | 
					        layout.addLayout(data_mode_layout)
 | 
				
			||||||
        layout.addLayout(self.slowdown_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
 | 
					        # Slowdown simulation
 | 
				
			||||||
        simulate_slowdown = config.simulate_slowdown
 | 
					        simulate_slowdown = self.user_config.simulate_slowdown
 | 
				
			||||||
        self.slowdown_checkbox.setChecked(simulate_slowdown)
 | 
					        self.slowdown_checkbox.setChecked(simulate_slowdown)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Buttons
 | 
					        # Buttons
 | 
				
			||||||
@ -75,15 +60,8 @@ class SettingsDialog(QDialog):
 | 
				
			|||||||
        layout.addLayout(button_layout)
 | 
					        layout.addLayout(button_layout)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save_settings(self):
 | 
					    def save_settings(self):
 | 
				
			||||||
        data_mode = self.data_mode_dropdown.currentData()
 | 
					 | 
				
			||||||
        simulate_slowdown = self.slowdown_checkbox.isChecked()
 | 
					        simulate_slowdown = self.slowdown_checkbox.isChecked()
 | 
				
			||||||
        try:
 | 
					        self.logger.debug("Saving user configuration")
 | 
				
			||||||
            self.logger.debug("Saving user configuration")
 | 
					        config = UserConfig()
 | 
				
			||||||
            config = UserConfig()
 | 
					        config.simulate_slowdown = simulate_slowdown
 | 
				
			||||||
            config.transaction_level = data_mode
 | 
					        self.accept()
 | 
				
			||||||
            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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
from PySide6.QtGui import QGuiApplication, QIcon
 | 
					from PySide6.QtGui import QGuiApplication, QIcon
 | 
				
			||||||
from PySide6.QtWidgets import QMainWindow, QApplication, QTabWidget
 | 
					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
 | 
					from ui.menu_bar import MenuBar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,8 +24,10 @@ class LibraryWindow(QMainWindow):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.dashboard = BookOverviewList(self)
 | 
					        self.dashboard = BookOverviewList(self)
 | 
				
			||||||
        self.member_list = MemberList()
 | 
					        self.member_list = MemberList()
 | 
				
			||||||
 | 
					        self.category_statistics_overview_list = BookCategoryStatisticsOverview()
 | 
				
			||||||
        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")
 | 
				
			||||||
 | 
					        central_widget.addTab(self.category_statistics_overview_list, "Category stats")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def center_window(self):
 | 
					    def center_window(self):
 | 
				
			||||||
        # Get the screen geometry
 | 
					        # Get the screen geometry
 | 
				
			||||||
@ -46,6 +48,7 @@ class LibraryWindow(QMainWindow):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def refresh_book_cards(self):
 | 
					    def refresh_book_cards(self):
 | 
				
			||||||
        self.dashboard.redraw_cards()
 | 
					        self.dashboard.redraw_cards()
 | 
				
			||||||
 | 
					        self.category_statistics_overview_list.redraw_cards()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def refresh_member_cards(self):
 | 
					    def refresh_member_cards(self):
 | 
				
			||||||
        self.member_list.redraw_cards()
 | 
					        self.member_list.redraw_cards()
 | 
				
			||||||
@ -60,11 +60,12 @@ class DatabaseConfig():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TransactionLevel(enum.Enum):
 | 
					class TransactionLevel(enum.Enum):
 | 
				
			||||||
    insecure = "READ UNCOMMITTED"
 | 
					    insecure = "READ COMMITTED"
 | 
				
			||||||
    secure = "SERIALIZABLE"
 | 
					    secure = "SERIALIZABLE"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserConfig:
 | 
					class UserConfig:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _instance = None
 | 
					    _instance = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _metadata = {
 | 
					    _metadata = {
 | 
				
			||||||
@ -78,26 +79,23 @@ class UserConfig:
 | 
				
			|||||||
        return cls._instance
 | 
					        return cls._instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self):
 | 
					    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"):
 | 
					        if not hasattr(self, "logger"):
 | 
				
			||||||
            self.logger = logging.getLogger(__name__)
 | 
					            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
 | 
					    @property
 | 
				
			||||||
    def transaction_level(self) -> TransactionLevel:
 | 
					    def transaction_level(self) -> TransactionLevel:
 | 
				
			||||||
        return self._transaction_level
 | 
					        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
 | 
					    @property
 | 
				
			||||||
    def simulate_slowdown(self) -> bool:
 | 
					    def simulate_slowdown(self) -> bool:
 | 
				
			||||||
        return self._simulate_slowdown
 | 
					        return self._simulate_slowdown
 | 
				
			||||||
 | 
				
			|||||||
@ -20,7 +20,7 @@ class DatabaseConnectionError(DatabaseError):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DuplicateEntryError(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)
 | 
					        super().__init__(message)
 | 
				
			||||||
        self.duplicate_entry_name = duplicate_entry_name
 | 
					        self.duplicate_entry_name = duplicate_entry_name
 | 
				
			||||||
        self.message = message
 | 
					        self.message = message
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,24 @@
 | 
				
			|||||||
import sys
 | 
					import sys
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def setup_logger():
 | 
					def setup_logger():
 | 
				
			||||||
    logger = logging.getLogger()
 | 
					    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 = logging.StreamHandler(sys.stdout)
 | 
				
			||||||
    handler.setLevel(logging.DEBUG)
 | 
					    handler.setLevel(log_level)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    formatter = logging.Formatter("[%(levelname)s] - %(name)s:%(lineno)d - %(message)s")
 | 
					    formatter = logging.Formatter("[%(levelname)s] - %(name)s:%(lineno)d - %(message)s")
 | 
				
			||||||
    handler.setFormatter(formatter)
 | 
					    handler.setFormatter(formatter)
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user