[main] Major rework of structure

This commit is contained in:
Thastertyn 2025-01-15 14:08:18 +01:00
parent 3fa44ccaf3
commit 516639ef9d
38 changed files with 546 additions and 297 deletions

View File

@ -1,65 +1,6 @@
import sys
import os
import logging
from typing import Optional
from PySide6.QtWidgets import QMessageBox, QApplication
from utils.config import UserConfig
from utils.database import DatabaseManager
from utils.errors.database.database_config_error import DatabaseConfigError
from utils.errors.database.database_connection_error import DatabaseConnectionError
from utils.setup_logger import setup_logger
from ui.window import LibraryWindow
class LibraryApp():
def __init__(self, user_config: Optional[UserConfig] = None):
setup_logger()
self.logger = logging.getLogger(__name__)
self.logger.info("Starting")
self.qt_app = QApplication([])
self.user_config = user_config
try:
self.database_manger = DatabaseManager()
except DatabaseConfigError as e:
self.exit_with_error(f"Invalid config: {e.config_name}", e.message)
except DatabaseConnectionError as e:
self.exit_with_error(f"Could not connect to database: {e}")
except FileNotFoundError:
self.exit_with_error("Configuration not found")
self.window = LibraryWindow()
def exit_with_error(self, error: str, additional_text: str = ""):
self.show_error(error, additional_text)
self.qt_app.quit()
sys.exit(1)
def run(self) -> int:
self.window.show()
status = self.qt_app.exec()
self.cleanup()
self.logger.info("Exiting")
return status
def show_error(self, text: str, detail_text: str = ""):
error_dialog = QMessageBox()
error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setWindowTitle("Error")
error_dialog.setText(text)
if detail_text:
error_dialog.setInformativeText(detail_text)
error_dialog.setStandardButtons(QMessageBox.StandardButton.Ok)
error_dialog.exec()
def cleanup(self) -> None:
self.logger.info("Cleaning up")
self.qt_app.quit()
self.database_manger.cleanup()
from app.library_app import LibraryApp
if __name__ == "__main__":
library_app = LibraryApp()

5
src/app/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from .library_app import *
__all__ = [
*library_app.__all__
]

62
src/app/library_app.py Normal file
View File

@ -0,0 +1,62 @@
import sys
import os
import logging
from typing import Optional
from PySide6.QtWidgets import QMessageBox, QApplication
from utils.config import UserConfig
from database.manager import DatabaseManager
from utils.errors.database import DatabaseConfigError, DatabaseConnectionError
from utils.setup_logger import setup_logger
from ui.window import LibraryWindow
class LibraryApp():
def __init__(self, user_config: Optional[UserConfig] = None):
setup_logger()
self.logger = logging.getLogger(__name__)
self.logger.info("Starting")
self.qt_app = QApplication([])
self.user_config = user_config
try:
self.database_manger = DatabaseManager()
except DatabaseConfigError as e:
self.exit_with_error(f"Invalid config: {e.config_name}", e.message)
except DatabaseConnectionError as e:
self.exit_with_error(f"Could not connect to database.")
except FileNotFoundError:
self.exit_with_error("Configuration not found.")
self.window = LibraryWindow()
def exit_with_error(self, error: str, additional_text: str = ""):
self.show_error(error, additional_text)
self.qt_app.quit()
sys.exit(1)
def run(self) -> int:
self.window.show()
status = self.qt_app.exec()
self.cleanup()
self.logger.info("Exiting")
return status
def show_error(self, text: str, detail_text: str = ""):
error_dialog = QMessageBox()
error_dialog.setIcon(QMessageBox.Icon.Critical)
error_dialog.setWindowTitle("Error")
error_dialog.setText(text)
if detail_text:
error_dialog.setInformativeText(detail_text)
error_dialog.setStandardButtons(QMessageBox.StandardButton.Ok)
error_dialog.exec()
def cleanup(self) -> None:
self.logger.info("Cleaning up")
self.qt_app.quit()
self.database_manger.cleanup()
__all__ = ["LibraryApp"]

View File

@ -0,0 +1,9 @@
from .manager import *
from .book import *
from .member import *
__all__ = [
*manager.__all__,
*book.__all__,
*member.__all__,
]

66
src/database/book.py Normal file
View File

@ -0,0 +1,66 @@
from typing import Dict, List
import logging
from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError as SqlAlchemyDatabaseError
from utils.errors.database import DatabaseError, DuplicateEntryError, DatabaseConnectionError
from models import Book
from database.manager import DatabaseManager
logger = logging.getLogger(__name__)
def fetch_all():
with DatabaseManager.get_session() as session:
try:
return session.query(Book).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 occured when fetching all books: {e}")
raise DatabaseError(
"An error occured when fetching all books") from e
def create_new_book(book: Book):
pass
def update_book(book: Book):
session = DatabaseManager.get_session()
try:
with session:
logger.debug(f"Updating book {book.title}")
existing_book = session.query(Book).get(book.id)
if not existing_book:
logger.warning(f"Book with id {book.id} not found")
raise DatabaseError("Book not found in the database")
existing_book.title = book.title
existing_book.description = book.description
existing_book.year_published = book.year_published
existing_book.isbn = book.isbn
session.commit()
logger.info("Book successfully updated")
except IntegrityError as e:
logger.warning("Data already exists")
session.rollback()
raise DuplicateEntryError(
"Data already exists in the database") from e
except SqlAlchemyDatabaseError as e:
session.rollback()
logger.critical("Connection with database interrupted")
raise DatabaseConnectionError(
"Connection with database interrupted") from e
except SQLAlchemyError as e:
logger.error(f"An error occured when saving book: {e}")
session.rollback()
raise DatabaseError(
"An error occured when updating the book") from e
__all__ = ["create_new_book", "update_book"]

55
src/database/manager.py Normal file
View File

@ -0,0 +1,55 @@
import logging
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy import create_engine, text
from sqlalchemy.exc import DatabaseError
from utils.config import DatabaseConfig
from utils.errors.database import DatabaseConnectionError
class DatabaseManager():
_instance: 'DatabaseManager' = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self) -> None:
self.logger = logging.getLogger(__name__)
self.logger.info("Reading database config")
database_config = DatabaseConfig()
self.engine = create_engine('mysql+mysqlconnector://%s:%s@%s:%s/%s' % (
database_config.user,
database_config.password,
database_config.host,
database_config.port,
database_config.name),
pool_pre_ping=True,
echo=True)
if self.test_connection():
self.Session = sessionmaker(bind=self.engine)
def cleanup(self) -> None:
self.logger.debug("Closing connection")
self.engine.dispose()
def test_connection(self) -> bool:
self.logger.debug("Testing database connection")
try:
with self.engine.connect() as connection:
connection.execute(text("select 1"))
self.logger.debug("Database connection successful")
return True
except DatabaseError as e:
self.logger.critical(f"Database connection failed: {e}")
raise DatabaseConnectionError("Database connection failed") from e
@classmethod
def get_session(cls) -> Session:
return DatabaseManager._instance.Session()
__all__ = ["DatabaseManager"]

View File

@ -1,11 +1,13 @@
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
import logging
from models.member import Member
from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError
from utils.database import DatabaseManager
from utils.errors.database import DatabaseError, DuplicateEntryError, DatabaseConnectionError
from models import Member
from database.manager import DatabaseManager
logger = logging.getLogger(__name__)
from utils.errors.database.database_error import DatabaseError
from utils.errors.database.duplicate_entry_error import DuplicateEntryError
def create_new_member(new_member: Member):
try:
@ -13,11 +15,19 @@ def create_new_member(new_member: Member):
session.add(new_member)
session.commit()
except IntegrityError as e:
print(e)
logger.warning("Data already exists")
session.rollback()
raise DuplicateEntryError from e
raise DuplicateEntryError("Data already exists in the database") 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:
print(e)
logger.error(f"An error occured when saving member: {e}")
session.rollback()
raise DatabaseError from e
raise DatabaseError(
"An error occured when creating a new member") from e
__all__ = ["create_new_member"]

View File

@ -2,11 +2,11 @@ from typing import Optional
import xml.etree.ElementTree as ET
from xml.dom import minidom
from utils.database import DatabaseManager
from database.manager import DatabaseManager
from utils.errors.export_error import ExportError
from utils.errors.no_export_entity_error import NoExportEntityError
from models.book import Book
from models import Book
class BookExporter():

View File

@ -3,13 +3,11 @@ import os
import logging
from xml.etree import ElementTree as ET
from xmlschema import XMLSchema
from utils.database import DatabaseManager
from database.manager import DatabaseManager
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 models.book import Book
from models.author import Author
from models.book_category import BookCategory
from models import Book, Author, BookCategory
from sqlalchemy.exc import IntegrityError
class BookImporter:

View File

@ -1,11 +1,19 @@
from .base import Base
from .author import Author
from .book import Book
from .book_category import BookCategory
from .book_overview import BooksOverview
from .book_category_link import BookCategoryLink
from .member import Member
from .librarian import Librarian
from .loan import Loan
from .author import *
from .book import *
from .book_category import *
from .book_category_link import *
from .book_overview import *
from .member import *
from .librarian import *
from .loan import *
__all__ = ["Author", "Book", "BookCategory", "BookCategoryLink", "Member", "Librarian", "Loan", "BooksOverview"]
__all__ = [
*author.__all__,
*book.__all__,
*book_category.__all__,
*book_category_link.__all__,
*book_overview.__all__,
*member.__all__,
*librarian.__all__,
*loan.__all__
]

View File

@ -15,3 +15,6 @@ class Author(Base):
# Reference 'Book' as a string to avoid direct import
books = relationship('Book', back_populates='author')
__all__ = ["Author"]

View File

@ -1,3 +1,5 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
__all__ = ["Base"]

View File

@ -28,3 +28,6 @@ class Book(Base):
author = relationship('Author', back_populates='books')
categories = relationship('BookCategory',secondary='book_category_link',back_populates='books')
__all__ = ["Book", "BookStatusEnum"]

View File

@ -20,3 +20,6 @@ class BookCategory(Base):
secondary='book_category_link', # Junction table
back_populates='categories' # For bidirectional relationship
)
__all__ = ["BookCategory"]

View File

@ -5,11 +5,17 @@ from .book import Book
from .book_category import BookCategory
from .base import Base
class BookCategoryLink(Base):
__tablename__ = 'book_category_link'
book_id = Column(Integer, ForeignKey('book.id', ondelete="cascade"), primary_key=True)
book_category_id = Column(Integer, ForeignKey('book_category.id', ondelete="cascade"), primary_key=True)
book_id = Column(Integer, ForeignKey(
'book.id', ondelete="cascade"), primary_key=True)
book_category_id = Column(Integer, ForeignKey(
'book_category.id', ondelete="cascade"), primary_key=True)
book = relationship('Book', overlaps='categories,books')
book_category = relationship('BookCategory', overlaps='categories,books')
book = relationship('Book', overlaps='categories,books')
book_category = relationship('BookCategory', overlaps='categories,books')
__all__ = ["BookCategoryLink"]

View File

@ -28,4 +28,7 @@ class BooksOverview(Base):
return (f"<BooksOverview(id={self.id}, title={self.title}, author_name={self.author_name}, "
f"categories={self.categories}, year_published={self.year_published}, isbn={self.isbn}, "
f"status={self.status}, created_at={self.created_at}, borrower_name={self.borrower_name}, "
f"librarian_name={self.librarian_name})>")
f"librarian_name={self.librarian_name})>")
__all__ = ["BooksOverview"]

View File

@ -28,3 +28,6 @@ class Librarian(Base):
status = Column(Enum(LibrarianStatusEnum), nullable=False, default=LibrarianStatusEnum.active)
role = Column(Enum(LibrarianRoleEnum), nullable=False)
last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now())
__all__ = ["Librarian", "LibrarianRoleEnum", "LibrarianStatusEnum"]

View File

@ -33,4 +33,7 @@ class Loan(Base):
book = relationship('Book', backref='loans')
member = relationship('Member', backref='loans')
librarian = relationship('Librarian', backref='loans')
librarian = relationship('Librarian', backref='loans')
__all__ = ["Loan", "LoanStatusEnum"]

View File

@ -22,4 +22,7 @@ class Member(Base):
phone = Column(String(20), nullable=True, unique=True)
register_date = Column(TIMESTAMP, nullable=True, server_default=func.now())
status = Column(Enum(MemberStatusEnum), nullable=True, default=MemberStatusEnum.active)
last_updated = Column(TIMESTAMP, nullable=True)
last_updated = Column(TIMESTAMP, nullable=True)
__all__ = ["Member", "MemberStatusEnum"]

View File

@ -1,88 +0,0 @@
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QTextEdit, QPushButton, QComboBox, QFormLayout
)
from PySide6.QtGui import QRegularExpressionValidator
from PySide6.QtCore import QRegularExpression
from models.book import Book, BookStatusEnum
from utils.database import DatabaseManager
from sqlalchemy import update
class BookEditor(QDialog):
def __init__(self, book: Book, parent=None):
super().__init__(parent)
self.book = book
self.setWindowTitle("Edit Book")
self.setMinimumSize(400, 300)
# Create main layout
layout = QVBoxLayout(self)
# Form layout for book fields
form_layout = QFormLayout()
# Title field
self.title_input = QLineEdit(self.book.title)
form_layout.addRow("Title:", self.title_input)
full_author_name = f"{self.book.author.first_name} {self.book.author.last_name}"
self.author_input = QLineEdit(full_author_name)
form_layout.addRow("Author: ", self.author_input)
# Description field
self.description_input = QTextEdit(self.book.description)
form_layout.addRow("Description:", self.description_input)
# Year published field
self.year_input = QLineEdit(self.book.year_published)
# self.year_input.setValidator
form_layout.addRow("Year Published:", self.year_input)
# ISBN field
self.isbn_input = QLineEdit(self.book.isbn)
self.isbn_expression = QRegularExpression("\d{10}|\d{13}")
self.isbn_validator = QRegularExpressionValidator(self.isbn_expression)
self.isbn_input.setValidator(self.isbn_validator)
form_layout.addRow("ISBN:", self.isbn_input)
all_categories = ", ".join(category.name for category in self.book.categories)
self.categories_input = QLineEdit(all_categories)
form_layout.addRow("Categories: ", self.categories_input)
layout.addLayout(form_layout)
# Buttons
button_layout = QHBoxLayout()
self.save_button = QPushButton("Save")
self.save_button.clicked.connect(self.save_book)
button_layout.addWidget(self.save_button)
self.cancel_button = QPushButton("Discard")
self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
def save_book(self):
# Update book object with input values
self.book.title = self.title_input.text()
self.book.description = self.description_input.toPlainText()
self.book.year_published = self.year_input.text()
self.book.isbn = self.isbn_input.text()
self.book.status = BookStatusEnum(self.status_input.currentText())
with DatabaseManager.get_session() as session:
existing_book = session.query(Book).get(self.book.id)
existing_book.title = self.book.title
existing_book.description = self.book.description
existing_book.year_published = self.book.year_published
existing_book.isbn = self.book.isbn
session.commit()
# Accept the dialog and close
self.accept()

View File

@ -3,12 +3,11 @@ from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QWidget, QMenu, QSizePolicy, QLayout, QMessageBox
from PySide6.QtCore import qDebug
from ui.book_editor.book_editor import BookEditor
from ui.editor import BookEditor
from models.book import BookStatusEnum, Book
from models.book_overview import BooksOverview
from models import BooksOverview, Book, BookStatusEnum
from utils.database import DatabaseManager
from database.manager import DatabaseManager
from sqlalchemy import delete

View File

@ -6,11 +6,11 @@ from PySide6.QtWidgets import (
from PySide6.QtCore import Qt
from .book_card import BookCard
from models.book_overview import BooksOverview
from models import BooksOverview
from utils.database import DatabaseManager
from database.manager import DatabaseManager
from ui.member_editor.member_editor import MemberEditor
from ui.editor import MemberEditor
class LibraryDashboard(QWidget):
@ -29,7 +29,7 @@ class LibraryDashboard(QWidget):
# Search bar
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Type to search...")
self.search_input.setPlaceholderText("Search in books...")
self.search_input.textChanged.connect(self.filter_books)
main_layout.addWidget(self.search_input)

View File

@ -0,0 +1,7 @@
from .book_editor import *
from .member_editor import *
__all__ = [
*book_editor.__all__,
*member_editor.__all__
]

View File

@ -0,0 +1,130 @@
import logging
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QTextEdit, QPushButton, QComboBox, QFormLayout, QMessageBox
)
from PySide6.QtGui import QRegularExpressionValidator
from PySide6.QtCore import QRegularExpression
from models import Book, BookStatusEnum
from database import update_book
from utils.errors.database import DatabaseError, DatabaseConnectionError, DuplicateEntryError
class BookEditor(QDialog):
def __init__(self, book: Book = None, parent=None):
super().__init__(parent)
self.logger = logging.getLogger(__name__)
self.create_layout()
if book:
self.logger.debug(f"Editing existing book {book.title}")
self.book = book
self.create_new = False
self.fill_with_existing_data()
else:
self.logger.debug("Editing a new book")
self.create_new = True
def create_layout(self):
self.setWindowTitle("Books")
self.setMinimumWidth(400)
# Create main layout
layout = QVBoxLayout(self)
# Form layout for book fields
form_layout = QFormLayout()
# Title field
self.title_input = QLineEdit()
form_layout.addRow("Title:", self.title_input)
# Author field
self.author_label = QLabel()
form_layout.addRow("Author: ", self.author_label)
# Description field
self.description_input = QTextEdit()
form_layout.addRow("Description:", self.description_input)
# Year published field
self.year_input = QLineEdit()
# self.year_input.setValidator
form_layout.addRow("Year Published:", self.year_input)
# ISBN field
self.isbn_input = QLineEdit()
self.isbn_expression = QRegularExpression("\d{10}|\d{13}")
self.isbn_validator = QRegularExpressionValidator(self.isbn_expression)
self.isbn_input.setValidator(self.isbn_validator)
form_layout.addRow("ISBN:", self.isbn_input)
self.categories_input = QLineEdit()
self.categories_input.setEnabled(False)
form_layout.addRow("Categories: ", self.categories_input)
layout.addLayout(form_layout)
# Buttons
button_layout = QHBoxLayout()
self.save_button = QPushButton("Save")
self.save_button.clicked.connect(self.save_book)
button_layout.addWidget(self.save_button)
self.cancel_button = QPushButton("Discard")
self.cancel_button.clicked.connect(self.reject)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
def fill_with_existing_data(self):
self.title_input.setText(self.book.title)
self.description_input.setText(self.book.description)
self.year_input.setText(self.book.year_published)
self.isbn_input.setText(self.book.isbn)
def save_book(self):
# Update book object with input values
self.book.title = self.title_input.text()
full_author_name = f"{self.book.author.first_name} {
self.book.author.last_name}"
self.author_label.setText(full_author_name)
self.book.description = self.description_input.toPlainText()
self.book.year_published = self.year_input.text()
self.book.isbn = self.isbn_input.text()
all_categories = ", ".join(
category.name for category in self.book.categories)
self.categories_input.setText(all_categories)
try:
if self.create_new:
pass
else:
update_book(self.book)
self.accept()
except DuplicateEntryError as e:
QMessageBox.critical(None,
"ISBN is already in use",
"The ISBN provided is already in use",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
except DatabaseConnectionError as e:
QMessageBox.critical(None,
"Failed to save",
"Could not connect to the database",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
except DatabaseError as e:
QMessageBox.critical(self.parent,
"An error occured",
f"Could not save the book because of the following error: {e}",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
__all__ = ["BookEditor"]

View File

@ -5,19 +5,18 @@ from PySide6.QtQml import QQmlApplicationEngine
from PySide6 import QtWidgets, QtCore
from PySide6.QtWidgets import QVBoxLayout, QFormLayout, QLineEdit, QHBoxLayout, QPushButton, QDialog, QMessageBox
from models.member import Member
from models import Member
from database.member import create_new_member
from utils.errors.database.database_error import DatabaseError
from utils.errors.database.duplicate_entry_error import DuplicateEntryError
from utils.errors.database import DatabaseError, DatabaseConnectionError, DuplicateEntryError
class MemberEditor(QDialog):
def __init__(self, member: Member = None):
super().__init__()
self.logger = logging.getLogger(__name__)
self.logger = logging.getLogger(__name__)
self.create_layout()
if member:
@ -30,7 +29,7 @@ class MemberEditor(QDialog):
def create_layout(self):
self.setWindowTitle("Members")
self.setMinimumSize(400, 300)
self.setMinimumWidth(400)
# Create main layout
self.layout = QVBoxLayout(self)
@ -86,6 +85,10 @@ class MemberEditor(QDialog):
self.logger.debug("Creating new member")
create_new_member(self.member)
except DuplicateEntryError:
QMessageBox.critical(None, "Details already in use", "Cannot create a new user", QMessageBox.StandardButton.Ok, QMessageBox.StandardButtons.NoButton)
QMessageBox.critical(None, "Details already in use", "Cannot create a new user",
QMessageBox.StandardButton.Ok, QMessageBox.StandardButtons.NoButton)
self.accept()
self.accept()
__all__ = ["MemberEditor"]

View File

@ -4,8 +4,8 @@ from PySide6.QtWidgets import (
from PySide6.QtGui import Qt
from PySide6.QtCore import qDebug
from models.member import Member, MemberStatusEnum
from utils.database import DatabaseManager
from models import Member, MemberStatusEnum
from database.manager import DatabaseManager
from sqlalchemy import delete
STATUS_TO_COLOR_MAP = {

View File

@ -5,10 +5,10 @@ from PySide6.QtWidgets import (
from PySide6.QtCore import Qt
from .member_card import MemberCard
from models.member import Member
from utils.database import DatabaseManager
from models import Member
from database.manager import DatabaseManager
from ui.member_editor.member_editor import MemberEditor
from ui.editor import MemberEditor
class MemberList(QWidget):

View File

@ -1,12 +1,16 @@
import logging
import sys
from PySide6 import QtWidgets
from PySide6.QtWidgets import QDialog, QMessageBox
from utils.config import UserConfig, TransactionLevel
class SettingsDialog(QtWidgets.QDialog):
class SettingsDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.logger = logging.getLogger(__name__)
self.user_config = UserConfig()
self.setWindowTitle("Settings")
self.setMinimumSize(400, 100)
@ -16,23 +20,43 @@ class SettingsDialog(QtWidgets.QDialog):
data_mode_layout = QtWidgets.QHBoxLayout()
self.data_mode_label = QtWidgets.QLabel("Data mode:")
# Transaction isolation level
self.data_mode_label = QtWidgets.QLabel(UserConfig.get_friendly_name("transaction_level") + ":")
data_mode_layout.addWidget(self.data_mode_label)
self.data_mode_dropdown = QtWidgets.QComboBox()
for tl in TransactionLevel:
self.data_mode_dropdown.addItem(tl.name.capitalize(), tl.value)
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
self.slowdown_label = QtWidgets.QLabel(UserConfig.get_friendly_name("simulate_slowdown") + ":")
data_mode_layout.addWidget(self.slowdown_label)
self.slowdown_checkbox = QtWidgets.QCheckBox()
self.slowdown_checkbox.setChecked(self.user_config.simulate_slowdown)
data_mode_layout.addWidget(self.slowdown_checkbox)
layout.addLayout(data_mode_layout)
# Set the currently selected mode to the mode in UserConfig
config = UserConfig()
# Transaction level
current_level = config.transaction_level
index = self.data_mode_dropdown.findData(current_level)
if index != -1:
self.data_mode_dropdown.setCurrentIndex(index)
# Slowdown simulation
simulate_slowdown = config.simulate_slowdown
self.slowdown_checkbox.setChecked(simulate_slowdown)
# Buttons
button_layout = QtWidgets.QHBoxLayout()
@ -48,12 +72,14 @@ class SettingsDialog(QtWidgets.QDialog):
def save_settings(self):
data_mode = self.data_mode_dropdown.currentData()
simulate_slowdown = self.slowdown_checkbox.isChecked()
try:
self.logger.debug("Saving user configuration")
config = UserConfig()
config.transaction_level = data_mode
config.simulate_slowdown = simulate_slowdown
self.accept()
except TypeError as e:
self.logger.error("Invalid user configuration found")
QMessageBox.critical(None, "Invalid config detected", "Double check your configuration", QMessageBox.StandardButton.Ok, QMessageBox.StandardBUttons.NoButton)
config = UserConfig()
config.transaction_level = data_mode
print("Settings Saved:")
print(f"Data Mode: {config.transaction_level}")
self.accept()

View File

@ -6,8 +6,7 @@ from PySide6.QtCore import QStandardPaths
from ui.dashboard.dashboard import LibraryDashboard
from ui.main_window_tabs.member_list.member_list import MemberList
from ui.book_editor.book_editor import BookEditor
from ui.member_editor.member_editor import MemberEditor
from ui.editor import BookEditor, MemberEditor
from ui.settings import SettingsDialog
@ -144,9 +143,7 @@ class LibraryWindow(QtWidgets.QMainWindow):
# Menu action slots
def edit_preferences(self):
dialog = SettingsDialog(parent=self)
if dialog.exec() == QtWidgets.QDialog.Accepted:
print("Settings were saved.")
SettingsDialog(parent=self).exec()
# region Menu Actions
@ -210,6 +207,7 @@ class LibraryWindow(QtWidgets.QMainWindow):
e}", QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.NoButton)
def new_book(self):
# BookEditor()
pass
def new_member(self):

View File

@ -1,9 +1,10 @@
import enum
import os
from utils.errors.database.database_config_error import DatabaseConfigError
from utils.errors.database import DatabaseConfigError
from dotenv import load_dotenv
import logging
from typing import Any
class DatabaseConfig():
@ -59,18 +60,57 @@ class DatabaseConfig():
class TransactionLevel(enum.Enum):
insecure = 'READ UNCOMMITED'
secure = 'SERIALIZABLE'
insecure = "READ UNCOMMITTED"
secure = "SERIALIZABLE"
class UserConfig():
class UserConfig:
_instance = None
_metadata = {
"transaction_level": {"friendly_name": "Transaction Level"},
"simulate_slowdown": {"friendly_name": "Simulate Slowdown"},
}
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, "transaction_level"):
self.transaction_level = TransactionLevel.insecure
self._transaction_level = TransactionLevel.insecure
self._simulate_slowdown = False
@property
def transaction_level(self) -> TransactionLevel:
return self._transaction_level
@transaction_level.setter
def transaction_level(self, value: Any):
if not isinstance(value, TransactionLevel):
raise TypeError(
f"Invalid value for 'transaction_level'. Must be a TransactionLevel enum, got {type(value).__name__}."
)
self._transaction_level = value
@property
def simulate_slowdown(self) -> bool:
return self._simulate_slowdown
@simulate_slowdown.setter
def simulate_slowdown(self, value: Any):
if not isinstance(value, bool):
raise TypeError(
f"Invalid value for 'simulate_slowdown'. Must be a boolean, got {type(value).__name__}."
)
self._simulate_slowdown = value
@classmethod
def get_friendly_name(cls, option: str) -> str:
return cls._metadata.get(option, {}).get("friendly_name", option)
def __dict__(self) -> dict:
return {
"transaction_level": self.transaction_level,
"simulate_slowdown": self.simulate_slowdown,
}

View File

@ -1,52 +0,0 @@
import logging
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy import create_engine, text
from sqlalchemy.exc import DatabaseError
from utils.config import DatabaseConfig
from utils.errors.database.database_connection_error import DatabaseConnectionError
class DatabaseManager():
_instance: 'DatabaseManager' = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self) -> None:
self.logger = logging.getLogger(__name__)
self.logger.info("Reading database config")
database_config = DatabaseConfig()
self.engine = create_engine('mysql+mysqlconnector://%s:%s@%s:%s/%s' % (
database_config.user,
database_config.password,
database_config.host,
database_config.port,
database_config.name),
pool_pre_ping=True)
if self.test_connection():
self.Session = sessionmaker(bind=self.engine)
def cleanup(self) -> None:
self.logger.debug("Closing connection")
self.engine.dispose()
def test_connection(self) -> bool:
self.logger.debug("Testing database connection")
try:
with self.engine.connect() as connection:
connection.execute(text("select 1"))
self.logger.debug("Database connection successful")
return True
except DatabaseError as e:
self.logger.error(f"Database connection failed: {e}")
raise DatabaseConnectionError("Database connection failed") from e
@classmethod
def get_session(cls) -> Session:
return DatabaseManager._instance.Session()

View File

@ -0,0 +1,25 @@
class DatabaseError(Exception):
def __init__(self, message: str):
super().__init__(message)
self.message = message
class DatabaseConfigError(Exception):
def __init__(self, message: str, config_name: str, config_value: str):
super().__init__(message)
self.message = message
self.config_name = config_name
self.config_value = config_value
class DatabaseConnectionError(DatabaseError):
def __init__(self, message: str):
super().__init__(message)
self.message = message
class DuplicateEntryError(DatabaseError):
def __init__(self, message: str):
super().__init__(message)
self.message = message

View File

@ -1,7 +0,0 @@
class DatabaseConfigError(Exception):
def __init__(self, message: str, config_name: str, config_value: str):
super().__init__(message)
self.message = message
self.config_name = config_name
self.config_value = config_value

View File

@ -1,5 +0,0 @@
from .database_error import DatabaseError
class DatabaseConnectionError(DatabaseError):
def __init__(self, message: str):
super().__init__(message)
self.message = message

View File

@ -1,4 +0,0 @@
class DatabaseError(Exception):
def __init__(self, message: str):
super().__init__(message)
self.message = message

View File

@ -1,6 +0,0 @@
from .database_error import DatabaseError
class DuplicateEntryError(DatabaseError):
def __init__(self, message: str):
super().__init__(message)
self.message = message

View File

@ -8,7 +8,7 @@ def setup_logger():
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter("[%(levelname)s] - %(name)s - %(message)s")
formatter = logging.Formatter("[%(levelname)s] - %(name)s:%(lineno)d - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)