[main] Major rework of structure
This commit is contained in:
parent
3fa44ccaf3
commit
516639ef9d
61
src/app.py
61
src/app.py
@ -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
5
src/app/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .library_app import *
|
||||
|
||||
__all__ = [
|
||||
*library_app.__all__
|
||||
]
|
62
src/app/library_app.py
Normal file
62
src/app/library_app.py
Normal 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"]
|
@ -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
66
src/database/book.py
Normal 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
55
src/database/manager.py
Normal 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"]
|
@ -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"]
|
||||
|
@ -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():
|
||||
|
@ -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:
|
||||
|
@ -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__
|
||||
]
|
||||
|
@ -15,3 +15,6 @@ class Author(Base):
|
||||
|
||||
# Reference 'Book' as a string to avoid direct import
|
||||
books = relationship('Book', back_populates='author')
|
||||
|
||||
|
||||
__all__ = ["Author"]
|
||||
|
@ -1,3 +1,5 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
__all__ = ["Base"]
|
||||
|
@ -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"]
|
||||
|
@ -20,3 +20,6 @@ class BookCategory(Base):
|
||||
secondary='book_category_link', # Junction table
|
||||
back_populates='categories' # For bidirectional relationship
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["BookCategory"]
|
||||
|
@ -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"]
|
||||
|
@ -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"]
|
||||
|
@ -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"]
|
||||
|
@ -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"]
|
||||
|
@ -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"]
|
||||
|
@ -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()
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
7
src/ui/editor/__init__.py
Normal file
7
src/ui/editor/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
from .book_editor import *
|
||||
from .member_editor import *
|
||||
|
||||
__all__ = [
|
||||
*book_editor.__all__,
|
||||
*member_editor.__all__
|
||||
]
|
130
src/ui/editor/book_editor.py
Normal file
130
src/ui/editor/book_editor.py
Normal 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"]
|
@ -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"]
|
@ -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 = {
|
||||
|
@ -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):
|
||||
|
@ -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()
|
@ -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):
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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()
|
25
src/utils/errors/database.py
Normal file
25
src/utils/errors/database.py
Normal 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
|
@ -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
|
@ -1,5 +0,0 @@
|
||||
from .database_error import DatabaseError
|
||||
class DatabaseConnectionError(DatabaseError):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
self.message = message
|
@ -1,4 +0,0 @@
|
||||
class DatabaseError(Exception):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
self.message = message
|
@ -1,6 +0,0 @@
|
||||
from .database_error import DatabaseError
|
||||
|
||||
class DuplicateEntryError(DatabaseError):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
self.message = message
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user