[main] Added imports as well a whole bunch of tweaks elsewhere
This commit is contained in:
		
							parent
							
								
									142b2d449d
								
							
						
					
					
						commit
						48163325d8
					
				@ -9,5 +9,5 @@ python-dotenv==1.0.1
 | 
			
		||||
shiboken2==5.13.2
 | 
			
		||||
shiboken6==6.8.1
 | 
			
		||||
SQLAlchemy==2.0.36
 | 
			
		||||
sqlalchemy-stubs==0.4
 | 
			
		||||
typing_extensions==4.12.2
 | 
			
		||||
xmlschema==3.4.3
 | 
			
		||||
							
								
								
									
										19
									
								
								src/app.py
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/app.py
									
									
									
									
									
								
							@ -7,7 +7,8 @@ from PySide6.QtWidgets import QMessageBox, QApplication
 | 
			
		||||
 | 
			
		||||
from utils.config import UserConfig
 | 
			
		||||
from utils.database import DatabaseManager
 | 
			
		||||
from utils.errors.database_config_error import DatabaseConfigError
 | 
			
		||||
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
 | 
			
		||||
@ -25,15 +26,18 @@ class LibraryApp():
 | 
			
		||||
        try:
 | 
			
		||||
            self.database_manger = DatabaseManager()
 | 
			
		||||
        except DatabaseConfigError as e:
 | 
			
		||||
            detail_text = f"Invalid config: {e.config_name}"
 | 
			
		||||
            self.show_error(e.message, detail_text=detail_text)
 | 
			
		||||
            sys.exit(1)
 | 
			
		||||
            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.show_error("Configuration not found")
 | 
			
		||||
            sys.exit(1)
 | 
			
		||||
 | 
			
		||||
            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()
 | 
			
		||||
@ -53,6 +57,7 @@ class LibraryApp():
 | 
			
		||||
 | 
			
		||||
    def cleanup(self) -> None:
 | 
			
		||||
        self.logger.info("Cleaning up")
 | 
			
		||||
        self.qt_app.quit()
 | 
			
		||||
        self.database_manger.cleanup()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,56 +8,69 @@ from utils.errors.no_export_entity_error import NoExportEntityError
 | 
			
		||||
 | 
			
		||||
from models.book import Book
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BookExporter():
 | 
			
		||||
    def save_xml(self, file_path: str):
 | 
			
		||||
 | 
			
		||||
        xml = self._get_full_xml()
 | 
			
		||||
 | 
			
		||||
        if xml is None:
 | 
			
		||||
            raise NoExportEntityError("No books found to export")
 | 
			
		||||
 | 
			
		||||
        with open(file_path, "w", encoding="utf-8") as file:
 | 
			
		||||
                file.write(xml)
 | 
			
		||||
            file.write(xml)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _get_full_xml(self) -> Optional[str]:
 | 
			
		||||
    def _get_full_xml(self) -> str:
 | 
			
		||||
        root = ET.Element("books")
 | 
			
		||||
 | 
			
		||||
        with DatabaseManager().get_session() as session:
 | 
			
		||||
        with DatabaseManager.get_session() as session:
 | 
			
		||||
            self.books = session.query(Book).all()
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
            if not self.books:
 | 
			
		||||
                return None
 | 
			
		||||
                raise NoExportEntityError("No books found to export")
 | 
			
		||||
 | 
			
		||||
            for book in self.books:
 | 
			
		||||
                # Create a <book> element
 | 
			
		||||
                book_element = ET.SubElement(root, "book")
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                # Add <title>
 | 
			
		||||
                title_element = ET.SubElement(book_element, "title")
 | 
			
		||||
                title_element.text = book.title
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                # Add <author>
 | 
			
		||||
                author_element = ET.SubElement(book_element, "author")
 | 
			
		||||
 | 
			
		||||
                # Add <first_name>
 | 
			
		||||
                author_first_name_element = ET.SubElement(
 | 
			
		||||
                    author_element, "first_name")
 | 
			
		||||
                author_first_name_element.text = book.author.first_name
 | 
			
		||||
 | 
			
		||||
                author_last_name_element = ET.SubElement(
 | 
			
		||||
                    author_element, "last_name")
 | 
			
		||||
                author_last_name_element.text = book.author.last_name
 | 
			
		||||
 | 
			
		||||
                # Add <description>
 | 
			
		||||
                description_element = ET.SubElement(book_element, "description")
 | 
			
		||||
                description_element = ET.SubElement(
 | 
			
		||||
                    book_element, "description")
 | 
			
		||||
                description_element.text = book.description
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                # Add <year_published>
 | 
			
		||||
                year_published_element = ET.SubElement(book_element, "year_published")
 | 
			
		||||
                year_published_element = ET.SubElement(
 | 
			
		||||
                    book_element, "year_published")
 | 
			
		||||
                year_published_element.text = book.year_published
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                # Add <isbn>
 | 
			
		||||
                isbn_element = ET.SubElement(book_element, "isbn")
 | 
			
		||||
                isbn_element.text = book.isbn
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                # Add <categories>
 | 
			
		||||
                categories_element = ET.SubElement(book_element, "categories")
 | 
			
		||||
                for category in book.categories:
 | 
			
		||||
                    category_element = ET.SubElement(categories_element, "category")
 | 
			
		||||
                    category_element = ET.SubElement(
 | 
			
		||||
                        categories_element, "category")
 | 
			
		||||
                    category_element.text = category.name
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        # Convert the tree to a string
 | 
			
		||||
        tree_str = ET.tostring(root, encoding="unicode")
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        # Pretty print the XML
 | 
			
		||||
        pretty_xml = minidom.parseString(tree_str).toprettyxml(indent=(" " * 4))
 | 
			
		||||
        return pretty_xml
 | 
			
		||||
        pretty_xml = minidom.parseString(
 | 
			
		||||
            tree_str).toprettyxml(indent=(" " * 4))
 | 
			
		||||
        return pretty_xml
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								src/importer/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/importer/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								src/importer/book/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/importer/book/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -15,6 +15,26 @@
 | 
			
		||||
                                    </xs:restriction>
 | 
			
		||||
                                </xs:simpleType>
 | 
			
		||||
                            </xs:element>
 | 
			
		||||
                            <xs:element name="author"> <!-- Author -->
 | 
			
		||||
                                <xs:complexType>
 | 
			
		||||
                                    <xs:sequence>
 | 
			
		||||
                                        <xs:element name="first_name">
 | 
			
		||||
                                            <xs:simpleType>
 | 
			
		||||
                                                <xs:restriction base="xs:string">
 | 
			
		||||
                                                    <xs:maxLength value="50"/>
 | 
			
		||||
                                                </xs:restriction>
 | 
			
		||||
                                            </xs:simpleType>
 | 
			
		||||
                                        </xs:element>
 | 
			
		||||
                                        <xs:element name="last_name">
 | 
			
		||||
                                            <xs:simpleType>
 | 
			
		||||
                                                <xs:restriction base="xs:string">
 | 
			
		||||
                                                    <xs:maxLength value="50"/>
 | 
			
		||||
                                                </xs:restriction>
 | 
			
		||||
                                            </xs:simpleType>
 | 
			
		||||
                                        </xs:element>
 | 
			
		||||
                                    </xs:sequence>
 | 
			
		||||
                                </xs:complexType>
 | 
			
		||||
                            </xs:element>
 | 
			
		||||
                            <xs:element name="description" type="xs:string" /> <!-- Description -->
 | 
			
		||||
                            <xs:element name="year_published"> <!-- Year published -->
 | 
			
		||||
                                <xs:simpleType>
 | 
			
		||||
@ -33,7 +53,8 @@
 | 
			
		||||
                            <xs:element name="categories"> <!-- Categories list -->
 | 
			
		||||
                                <xs:complexType>
 | 
			
		||||
                                    <xs:sequence>
 | 
			
		||||
                                        <xs:element name="category" type="xs:string" maxOccurs="unbounded" />
 | 
			
		||||
                                        <xs:element name="category" type="xs:string"
 | 
			
		||||
                                            maxOccurs="unbounded" />
 | 
			
		||||
                                    </xs:sequence>
 | 
			
		||||
                                </xs:complexType>
 | 
			
		||||
                            </xs:element>
 | 
			
		||||
							
								
								
									
										119
									
								
								src/importer/book/book_importer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/importer/book/book_importer.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
			
		||||
from typing import List
 | 
			
		||||
import os
 | 
			
		||||
import logging
 | 
			
		||||
from xml.etree import ElementTree as ET
 | 
			
		||||
from xmlschema import XMLSchema
 | 
			
		||||
from utils.database 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 sqlalchemy.exc import IntegrityError
 | 
			
		||||
 | 
			
		||||
class BookImporter:
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        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[Book]:
 | 
			
		||||
        """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")
 | 
			
		||||
                first_name = author_element.find("first_name").text
 | 
			
		||||
                last_name = author_element.find("last_name").text
 | 
			
		||||
                author = Author(first_name=first_name, last_name=last_name)
 | 
			
		||||
 | 
			
		||||
                # Parse categories
 | 
			
		||||
                category_elements = book_element.find("categories").findall("category")
 | 
			
		||||
                categories = [BookCategory(name=category_element.text) for category_element in category_elements]
 | 
			
		||||
 | 
			
		||||
                # Create a Book object
 | 
			
		||||
                book = 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}")
 | 
			
		||||
 | 
			
		||||
    def filter_new_books(self, books: List[Book]) -> List[Book]:
 | 
			
		||||
        """Filters out books that already exist in the database."""
 | 
			
		||||
        new_books = []
 | 
			
		||||
        with DatabaseManager.get_session() as session:
 | 
			
		||||
            for book in books:
 | 
			
		||||
                existing_book = session.query(Book).filter(
 | 
			
		||||
                    Book.isbn == book.isbn,
 | 
			
		||||
                ).first()
 | 
			
		||||
                if existing_book is None:
 | 
			
		||||
                    new_books.append(book)
 | 
			
		||||
        return new_books
 | 
			
		||||
 | 
			
		||||
    def save_books(self, books: List[Book]):
 | 
			
		||||
        """Saves a list of books to the database."""
 | 
			
		||||
        try:
 | 
			
		||||
            with DatabaseManager.get_session() as session:
 | 
			
		||||
                for book in books:
 | 
			
		||||
                    # Check if the author exists, otherwise add
 | 
			
		||||
                    existing_author = session.query(Author).filter_by(
 | 
			
		||||
                        first_name=book.author.first_name,
 | 
			
		||||
                        last_name=book.author.last_name
 | 
			
		||||
                    ).first()
 | 
			
		||||
                    if existing_author:
 | 
			
		||||
                        book.author = existing_author
 | 
			
		||||
                    else:
 | 
			
		||||
                        session.add(book.author)
 | 
			
		||||
                        session.commit()
 | 
			
		||||
 | 
			
		||||
                    # Handle categories
 | 
			
		||||
                    new_categories = []
 | 
			
		||||
                    for category in book.categories:
 | 
			
		||||
                        existing_category = session.query(BookCategory).filter_by(name=category.name).first()
 | 
			
		||||
                        if existing_category:
 | 
			
		||||
                            new_categories.append(existing_category)
 | 
			
		||||
                        else:
 | 
			
		||||
                            self.logger.debug(f"Adding new category: {category.name}")
 | 
			
		||||
                            session.add(category)
 | 
			
		||||
                            session.commit()
 | 
			
		||||
                            new_categories.append(category)
 | 
			
		||||
                    # Replace book categories with the resolved categories
 | 
			
		||||
                    book.categories = new_categories
 | 
			
		||||
 | 
			
		||||
                    # Check if the book already exists
 | 
			
		||||
                    existing_book = session.query(Book).filter_by(isbn=book.isbn).first()
 | 
			
		||||
                    if not existing_book:
 | 
			
		||||
                        session.add(book)
 | 
			
		||||
                    else:
 | 
			
		||||
                        self.logger.warning(f"Book with ISBN {book.isbn} already exists. Skipping.")
 | 
			
		||||
 | 
			
		||||
                # Commit all changes
 | 
			
		||||
                session.commit()
 | 
			
		||||
        except IntegrityError as e:
 | 
			
		||||
            raise ImportError(f"An error occurred when importing books: {e}") from e
 | 
			
		||||
@ -11,5 +11,5 @@ class BookCategoryLink(Base):
 | 
			
		||||
    book_id          = Column(Integer, ForeignKey('book.id'), primary_key=True)
 | 
			
		||||
    book_category_id = Column(Integer, ForeignKey('book_category.id'), primary_key=True)
 | 
			
		||||
 | 
			
		||||
    book          = relationship('Book')
 | 
			
		||||
    book_category = relationship('BookCategory')
 | 
			
		||||
    book          = relationship('Book', overlaps='categories,books')
 | 
			
		||||
    book_category = relationship('BookCategory', overlaps='categories,books')
 | 
			
		||||
							
								
								
									
										7
									
								
								src/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/requirements.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
PySide6==6.8.1
 | 
			
		||||
PySide6==6.8.1.1
 | 
			
		||||
PySide6_Addons==6.8.1
 | 
			
		||||
PySide6_Essentials==6.8.1
 | 
			
		||||
python-dotenv==1.0.1
 | 
			
		||||
SQLAlchemy==2.0.36
 | 
			
		||||
xmlschema==3.4.3
 | 
			
		||||
@ -89,13 +89,16 @@ class BookCard(QWidget):
 | 
			
		||||
 | 
			
		||||
        action_edit_book = context_menu.addAction("Edit Book")
 | 
			
		||||
        action_edit_author = context_menu.addAction("Edit Author")
 | 
			
		||||
        action_mark_returned = context_menu.addAction("Mark as Returned")
 | 
			
		||||
        action_remove_reservation = context_menu.addAction("Remove reservation")
 | 
			
		||||
        context_menu.addSeparator()
 | 
			
		||||
        delete_book_action = context_menu.addAction("Delete Book")
 | 
			
		||||
 | 
			
		||||
        if self.book_overview.status == BookStatusEnum.borrowed:
 | 
			
		||||
            action_mark_returned = context_menu.addAction("Mark as Returned")
 | 
			
		||||
        if self.book_overview.status != BookStatusEnum.borrowed:
 | 
			
		||||
            action_mark_returned.setVisible(False)
 | 
			
		||||
 | 
			
		||||
        if self.book_overview.status == BookStatusEnum.reserved:
 | 
			
		||||
            action_remove_reservation = context_menu.addAction(
 | 
			
		||||
                "Remove reservation")
 | 
			
		||||
        if self.book_overview.status != BookStatusEnum.reserved:
 | 
			
		||||
            action_remove_reservation.setVisible(False)
 | 
			
		||||
 | 
			
		||||
        action = context_menu.exec_(self.mapToGlobal(event.pos()))
 | 
			
		||||
 | 
			
		||||
@ -120,3 +123,5 @@ class BookCard(QWidget):
 | 
			
		||||
            print("Mark as Returned selected")
 | 
			
		||||
        elif action == action_remove_reservation:
 | 
			
		||||
            print("Remove reservation selected")
 | 
			
		||||
        elif action == delete_book_action:
 | 
			
		||||
            print("Delete book")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										34
									
								
								src/ui/import_preview.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/ui/import_preview.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QHeaderView
 | 
			
		||||
 | 
			
		||||
class PreviewDialog(QDialog):
 | 
			
		||||
    def __init__(self, books, parent=None):
 | 
			
		||||
        super().__init__(parent)
 | 
			
		||||
 | 
			
		||||
        self.setWindowTitle("Preview Books to Import")
 | 
			
		||||
        self.setLayout(QVBoxLayout())
 | 
			
		||||
        self.setMinimumWidth(500)
 | 
			
		||||
 | 
			
		||||
        # Table to display books
 | 
			
		||||
        table = QTableWidget(self)
 | 
			
		||||
        table.setRowCount(len(books))
 | 
			
		||||
        table.setColumnCount(4)
 | 
			
		||||
        table.setHorizontalHeaderLabels(["Title", "Author", "Year", "ISBN"])
 | 
			
		||||
 | 
			
		||||
        for row, book in enumerate(books):
 | 
			
		||||
            table.setItem(row, 0, QTableWidgetItem(book.title))
 | 
			
		||||
            table.setItem(row, 1, QTableWidgetItem(f"{book.author.first_name} {book.author.last_name}"))
 | 
			
		||||
            table.setItem(row, 2, QTableWidgetItem(book.year_published))
 | 
			
		||||
            table.setItem(row, 3, QTableWidgetItem(book.isbn))
 | 
			
		||||
 | 
			
		||||
        header = table.horizontalHeader()
 | 
			
		||||
        header.setSectionResizeMode(QHeaderView.Stretch)
 | 
			
		||||
 | 
			
		||||
        self.layout().addWidget(table)
 | 
			
		||||
 | 
			
		||||
        # Add buttons
 | 
			
		||||
        self.confirm_button = QPushButton("Confirm", self)
 | 
			
		||||
        self.cancel_button = QPushButton("Cancel", self)
 | 
			
		||||
        self.confirm_button.clicked.connect(self.accept)
 | 
			
		||||
        self.cancel_button.clicked.connect(self.reject)
 | 
			
		||||
        self.layout().addWidget(self.confirm_button)
 | 
			
		||||
        self.layout().addWidget(self.cancel_button)
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
from PySide6.QtGui import QGuiApplication, QAction, QIcon
 | 
			
		||||
from PySide6.QtQml import QQmlApplicationEngine
 | 
			
		||||
from PySide6 import QtWidgets, QtCore
 | 
			
		||||
from PySide6.QtWidgets import QMessageBox
 | 
			
		||||
from PySide6.QtWidgets import QMessageBox, QFileDialog
 | 
			
		||||
from PySide6.QtCore import QStandardPaths
 | 
			
		||||
 | 
			
		||||
from ui.dashboard.dashboard import LibraryDashboard
 | 
			
		||||
from ui.book_editor.book_editor import BookEditor
 | 
			
		||||
@ -9,9 +10,14 @@ from ui.member_editor.member_editor import MemberEditor
 | 
			
		||||
 | 
			
		||||
from ui.settings import SettingsDialog
 | 
			
		||||
 | 
			
		||||
from ui.import_preview import PreviewDialog
 | 
			
		||||
 | 
			
		||||
from export.book_exporter import BookExporter
 | 
			
		||||
from importer.book.book_importer import BookImporter
 | 
			
		||||
 | 
			
		||||
from utils.errors.export_error import ExportError
 | 
			
		||||
from utils.errors.import_error.import_error import ImportError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LibraryWindow(QtWidgets.QMainWindow):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
@ -19,6 +25,7 @@ class LibraryWindow(QtWidgets.QMainWindow):
 | 
			
		||||
 | 
			
		||||
        # Set up main window properties
 | 
			
		||||
        self.setWindowTitle("Library App")
 | 
			
		||||
        self.setWindowIcon(QIcon.fromTheme("x-content-ebook-reader"))
 | 
			
		||||
        self.setGeometry(0, 0, 800, 600)
 | 
			
		||||
 | 
			
		||||
        self.center_window()
 | 
			
		||||
@ -31,9 +38,11 @@ class LibraryWindow(QtWidgets.QMainWindow):
 | 
			
		||||
        self.setCentralWidget(central_widget)
 | 
			
		||||
 | 
			
		||||
        central_widget.addTab(LibraryDashboard(), "Dashboard")
 | 
			
		||||
        # central_widget.addTab(BookEditor(), "Books")
 | 
			
		||||
        central_widget.addTab(MemberEditor(), "Members")
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        self.file_types = {
 | 
			
		||||
            "XML files (*.xml)": ".xml",
 | 
			
		||||
            "Any file type (*)": ""}
 | 
			
		||||
 | 
			
		||||
    def center_window(self):
 | 
			
		||||
        # Get the screen geometry
 | 
			
		||||
@ -52,8 +61,6 @@ class LibraryWindow(QtWidgets.QMainWindow):
 | 
			
		||||
        # Move the window to the calculated geometry
 | 
			
		||||
        self.move(window_geometry.topLeft())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def create_menu_bar(self):
 | 
			
		||||
        # Create the menu bar
 | 
			
		||||
        menu_bar = self.menuBar()
 | 
			
		||||
@ -82,7 +89,7 @@ class LibraryWindow(QtWidgets.QMainWindow):
 | 
			
		||||
        file_menu.addMenu(import_submenu)
 | 
			
		||||
 | 
			
		||||
        import_books_action = QAction("Import books", self)
 | 
			
		||||
        import_books_action.triggered.connect(self.import_data)
 | 
			
		||||
        import_books_action.triggered.connect(self.import_books)
 | 
			
		||||
        import_submenu.addAction(import_books_action)
 | 
			
		||||
 | 
			
		||||
        import_members_action = QAction("Import members", self)
 | 
			
		||||
@ -141,16 +148,62 @@ class LibraryWindow(QtWidgets.QMainWindow):
 | 
			
		||||
 | 
			
		||||
    def export_books(self):
 | 
			
		||||
        try:
 | 
			
		||||
            file_path = QtWidgets.QFileDialog.getSaveFileName(self, "Save book export", "", ".xml;;")
 | 
			
		||||
            home_dir = QStandardPaths.writableLocation(
 | 
			
		||||
                QStandardPaths.HomeLocation)
 | 
			
		||||
            file_path, selected_filter = QFileDialog.getSaveFileName(self,
 | 
			
		||||
                                                                     "Save book export",
 | 
			
		||||
                                                                     home_dir,
 | 
			
		||||
                                                                     ";;".join(self.file_types.keys()))
 | 
			
		||||
            if file_path:
 | 
			
		||||
                selected_filetype = self.file_types[selected_filter]
 | 
			
		||||
 | 
			
		||||
                if file_path.endswith(selected_filetype):
 | 
			
		||||
                    selected_filetype = ""
 | 
			
		||||
 | 
			
		||||
            if file_path[0]:
 | 
			
		||||
                book_exporter = BookExporter()
 | 
			
		||||
                book_exporter.save_xml(file_path[0] + file_path[1])
 | 
			
		||||
                book_exporter.save_xml(file_path + selected_filetype)
 | 
			
		||||
 | 
			
		||||
        except OSError as e:
 | 
			
		||||
            QMessageBox.critical(self, "Error saving file", f"An error occured when saving the exported data: {e}", QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.NoButton)
 | 
			
		||||
            QMessageBox.critical(self, "Error saving file", f"Error occurred when saving the exported data: {
 | 
			
		||||
                                 e}", QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.NoButton)
 | 
			
		||||
        except ExportError as e:
 | 
			
		||||
            QMessageBox.critical(self, "Error exporting books", f"An error occured when exporting books: {e}", QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.NoButton)
 | 
			
		||||
            QMessageBox.critical(self, "Error exporting books", f"An error occurred when exporting books: {
 | 
			
		||||
                                 e}", QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.NoButton)
 | 
			
		||||
 | 
			
		||||
    def import_books(self):
 | 
			
		||||
        try:
 | 
			
		||||
            home_dir = QStandardPaths.writableLocation(
 | 
			
		||||
                QStandardPaths.HomeLocation)
 | 
			
		||||
            file_path, _ = QFileDialog.getOpenFileName(
 | 
			
		||||
                self, "Choose import file", home_dir, ";;".join(
 | 
			
		||||
                    self.file_types.keys())
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if not file_path:
 | 
			
		||||
                return  # User canceled
 | 
			
		||||
 | 
			
		||||
            importer = BookImporter()
 | 
			
		||||
            books = importer.parse_xml(file_path)
 | 
			
		||||
            new_books = importer.filter_new_books(books)
 | 
			
		||||
 | 
			
		||||
            if not new_books:
 | 
			
		||||
                QMessageBox.information(
 | 
			
		||||
                    self, "No New Books", "No new books to import.", QMessageBox.Ok)
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            # Show preview dialog
 | 
			
		||||
            dialog = PreviewDialog(new_books, self)
 | 
			
		||||
            if dialog.exec() == QtWidgets.QDialog.Accepted:
 | 
			
		||||
                # User confirmed, proceed with importing
 | 
			
		||||
                importer.save_books(new_books)
 | 
			
		||||
                QMessageBox.information(
 | 
			
		||||
                    self, "Success", "Books imported successfully!", QMessageBox.Ok)
 | 
			
		||||
            else:
 | 
			
		||||
                QMessageBox.information(
 | 
			
		||||
                    self, "Canceled", "Import was canceled.", QMessageBox.Ok)
 | 
			
		||||
        except ImportError as e:
 | 
			
		||||
            QMessageBox.critical(self, "Error importing books", f"An error occurred when importing books from the file provided: {
 | 
			
		||||
                                    e}", QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.NoButton)
 | 
			
		||||
 | 
			
		||||
    def new_book(self):
 | 
			
		||||
        pass
 | 
			
		||||
@ -165,6 +218,7 @@ class LibraryWindow(QtWidgets.QMainWindow):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def about(self):
 | 
			
		||||
        QtWidgets.QMessageBox.information(self, "About", "Library app demonstrating the phantom read problem")
 | 
			
		||||
        QtWidgets.QMessageBox.information(
 | 
			
		||||
            self, "About", "Library app demonstrating the phantom read problem")
 | 
			
		||||
 | 
			
		||||
    # endregion
 | 
			
		||||
    # endregion
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import enum
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from utils.errors.database_config_error import DatabaseConfigError
 | 
			
		||||
from utils.errors.database.database_config_error import DatabaseConfigError
 | 
			
		||||
from dotenv import load_dotenv
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,12 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from sqlalchemy.orm import sessionmaker, Session
 | 
			
		||||
from sqlalchemy import create_engine
 | 
			
		||||
from sqlalchemy import create_engine, text
 | 
			
		||||
 | 
			
		||||
from sqlalchemy.exc import DatabaseError
 | 
			
		||||
 | 
			
		||||
from utils.config import DatabaseConfig
 | 
			
		||||
from utils.errors.database_config_error import DatabaseConfigError
 | 
			
		||||
from utils.errors.database.database_connection_error import DatabaseConnectionError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DatabaseManager():
 | 
			
		||||
@ -19,15 +21,32 @@ class DatabaseManager():
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        self.logger = logging.getLogger(__name__)
 | 
			
		||||
        self.logger.info("Reading database config")
 | 
			
		||||
        self.database_config = DatabaseConfig()
 | 
			
		||||
        self.engine = create_engine(f'mysql+mysqlconnector://{self.database_config.user}:{
 | 
			
		||||
                                    self.database_config.password}@{self.database_config.host}/{self.database_config.name}')
 | 
			
		||||
        self.session_local = sessionmaker(bind=self.engine)
 | 
			
		||||
        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_local = 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_local()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								src/utils/errors/database/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/utils/errors/database/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										5
									
								
								src/utils/errors/database/database_connection_error.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/utils/errors/database/database_connection_error.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
from .database_error import DatabaseError 
 | 
			
		||||
class DatabaseConnectionError(DatabaseError):
 | 
			
		||||
    def __init__(self, message: str):
 | 
			
		||||
        super().__init__(message)
 | 
			
		||||
        self.message = message
 | 
			
		||||
							
								
								
									
										4
									
								
								src/utils/errors/database/database_error.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/utils/errors/database/database_error.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
class DatabaseError(Exception):
 | 
			
		||||
    def __init__(self, message: str):
 | 
			
		||||
        super().__init__(message)
 | 
			
		||||
        self.message = message
 | 
			
		||||
							
								
								
									
										0
									
								
								src/utils/errors/import_error/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/utils/errors/import_error/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										5
									
								
								src/utils/errors/import_error/import_error.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/utils/errors/import_error/import_error.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
class ImportError(Exception):
 | 
			
		||||
    def __init__(self, message: str):
 | 
			
		||||
        super().__init__(message)
 | 
			
		||||
 | 
			
		||||
        self.message = message
 | 
			
		||||
							
								
								
									
										8
									
								
								src/utils/errors/import_error/invalid_contents_error.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/utils/errors/import_error/invalid_contents_error.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
from .import_error import ImportError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidContentsError(ImportError):
 | 
			
		||||
    def __init__(self, message: str):
 | 
			
		||||
        super().__init__(message)
 | 
			
		||||
 | 
			
		||||
        self.message = message
 | 
			
		||||
							
								
								
									
										8
									
								
								src/utils/errors/import_error/xsd_scheme_not_found.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/utils/errors/import_error/xsd_scheme_not_found.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
from .import_error import ImportError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class XsdSchemeNotFoundError(ImportError):
 | 
			
		||||
    def __init__(self, message: str):
 | 
			
		||||
        super().__init__(message)
 | 
			
		||||
 | 
			
		||||
        self.message = message
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user