From 142b2d449dba67fbc70982b2ce89de96687b877e Mon Sep 17 00:00:00 2001 From: Thastertyn Date: Sat, 11 Jan 2025 13:10:47 +0100 Subject: [PATCH] [main] Added book export to xml --- src/export/__init__.py | 0 src/export/book_exporter.py | 63 ++++++++++++++++++++++ src/models/book.py | 22 ++++---- src/models/book_category.py | 16 ++++-- src/models/book_category_link.py | 4 +- src/ui/book_editor/book_editor.py | 14 +++++ src/ui/window.py | 47 ++++++++++++++-- src/utils/errors/export_error.py | 5 ++ src/utils/errors/no_export_entity_error.py | 6 +++ 9 files changed, 154 insertions(+), 23 deletions(-) create mode 100644 src/export/__init__.py create mode 100644 src/export/book_exporter.py create mode 100644 src/utils/errors/export_error.py create mode 100644 src/utils/errors/no_export_entity_error.py diff --git a/src/export/__init__.py b/src/export/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/export/book_exporter.py b/src/export/book_exporter.py new file mode 100644 index 0000000..126279d --- /dev/null +++ b/src/export/book_exporter.py @@ -0,0 +1,63 @@ +from typing import Optional +import xml.etree.ElementTree as ET +from xml.dom import minidom + +from utils.database import DatabaseManager +from utils.errors.export_error import ExportError +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) + + + def _get_full_xml(self) -> Optional[str]: + root = ET.Element("books") + + with DatabaseManager().get_session() as session: + self.books = session.query(Book).all() + + if not self.books: + return None + + for book in self.books: + # Create a element + book_element = ET.SubElement(root, "book") + + # Add + title_element = ET.SubElement(book_element, "title") + title_element.text = book.title + + # Add <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.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.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 \ No newline at end of file diff --git a/src/models/book.py b/src/models/book.py index 6e86071..a8e2448 100644 --- a/src/models/book.py +++ b/src/models/book.py @@ -13,18 +13,18 @@ class BookStatusEnum(enum.Enum): class Book(Base): - __tablename__ = 'book' + __tablename__ = 'book' __table_args__ = (UniqueConstraint('isbn'),) - id = Column(Integer, primary_key=True, autoincrement=True) - author_id = Column(Integer, ForeignKey('author.id'), nullable=False) - title = Column(String(100), nullable=False) - description = Column(Text, nullable=False) + id = Column(Integer, primary_key=True, autoincrement=True) + author_id = Column(Integer, ForeignKey('author.id'), nullable=False) + title = Column(String(100), nullable=False) + description = Column(Text, nullable=False) year_published = Column(String(4), nullable=False) - isbn = Column(String(13), nullable=False, unique=True) - status = Column(Enum(BookStatusEnum), nullable=False, default=BookStatusEnum.available) - created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) - last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now()) + isbn = Column(String(13), nullable=False, unique=True) + status = Column(Enum(BookStatusEnum), nullable=False, default=BookStatusEnum.available) + created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) + last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now()) - # Reference 'Author' as a string to avoid direct import - author = relationship('Author', back_populates='books') + author = relationship('Author', back_populates='books') + categories = relationship('BookCategory',secondary='book_category_link',back_populates='books') diff --git a/src/models/book_category.py b/src/models/book_category.py index eb901a6..873453d 100644 --- a/src/models/book_category.py +++ b/src/models/book_category.py @@ -4,13 +4,19 @@ from sqlalchemy.orm import relationship from .base import Base class BookCategory(Base): - __tablename__ = 'book_category' + __tablename__ = 'book_category' __table_args__ = (UniqueConstraint('name'),) - id = Column(Integer, primary_key=True, autoincrement=True) + id = Column(Integer, primary_key=True, autoincrement=True) parent_category_id = Column(Integer, ForeignKey('book_category.id'), nullable=True) - name = Column(String(100), nullable=False) - mature_content = Column(Integer, nullable=False, default=0) - last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now()) + name = Column(String(100), nullable=False) + mature_content = Column(Integer, nullable=False, default=0) + last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now()) parent_category = relationship('BookCategory', remote_side=[id]) + + books = relationship( + 'Book', + secondary='book_category_link', # Junction table + back_populates='categories' # For bidirectional relationship + ) diff --git a/src/models/book_category_link.py b/src/models/book_category_link.py index 0bc52ad..4a6ee4e 100644 --- a/src/models/book_category_link.py +++ b/src/models/book_category_link.py @@ -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', backref='categories') - book_category = relationship('BookCategory', backref='books') \ No newline at end of file + book = relationship('Book') + book_category = relationship('BookCategory') \ No newline at end of file diff --git a/src/ui/book_editor/book_editor.py b/src/ui/book_editor/book_editor.py index 69f0974..908c555 100644 --- a/src/ui/book_editor/book_editor.py +++ b/src/ui/book_editor/book_editor.py @@ -1,6 +1,8 @@ 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 class BookEditor(QDialog): @@ -21,18 +23,30 @@ class BookEditor(QDialog): 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) + # Status dropdown self.status_input = QComboBox() self.status_input.addItems([status.value for status in BookStatusEnum]) diff --git a/src/ui/window.py b/src/ui/window.py index 28a94f0..12dfbdb 100644 --- a/src/ui/window.py +++ b/src/ui/window.py @@ -1,6 +1,7 @@ from PySide6.QtGui import QGuiApplication, QAction, QIcon from PySide6.QtQml import QQmlApplicationEngine from PySide6 import QtWidgets, QtCore +from PySide6.QtWidgets import QMessageBox from ui.dashboard.dashboard import LibraryDashboard from ui.book_editor.book_editor import BookEditor @@ -8,6 +9,10 @@ from ui.member_editor.member_editor import MemberEditor from ui.settings import SettingsDialog +from export.book_exporter import BookExporter + +from utils.errors.export_error import ExportError + class LibraryWindow(QtWidgets.QMainWindow): def __init__(self): super().__init__() @@ -84,10 +89,25 @@ class LibraryWindow(QtWidgets.QMainWindow): import_members_action.triggered.connect(self.import_data) import_submenu.addAction(import_members_action) - # Export action - export_action = QAction("Export overview", self) - export_action.triggered.connect(self.export_data) - file_menu.addAction(export_action) + # Export submenu + export_submenu = QtWidgets.QMenu(self) + export_submenu.setTitle("Export") + file_menu.addMenu(export_submenu) + + # Export overview + export_overview_action = QAction("Export overview", self) + export_overview_action.triggered.connect(self.export_data) + export_submenu.addAction(export_overview_action) + + # Export books + export_books_action = QAction("Export books", self) + export_books_action.triggered.connect(self.export_books) + export_submenu.addAction(export_books_action) + + # Export members + export_members_action = QAction("Export members", self) + export_members_action.triggered.connect(self.export_data) + export_submenu.addAction(export_members_action) file_menu.addSeparator() @@ -117,6 +137,21 @@ class LibraryWindow(QtWidgets.QMainWindow): if dialog.exec() == QtWidgets.QDialog.Accepted: print("Settings were saved.") + # region Menu Actions + + def export_books(self): + try: + file_path = QtWidgets.QFileDialog.getSaveFileName(self, "Save book export", "", ".xml;;") + + if file_path[0]: + book_exporter = BookExporter() + book_exporter.save_xml(file_path[0] + file_path[1]) + + 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) + except ExportError as e: + QMessageBox.critical(self, "Error exporting books", f"An error occured when exporting books: {e}", QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.NoButton) + def new_book(self): pass @@ -130,4 +165,6 @@ class LibraryWindow(QtWidgets.QMainWindow): pass def about(self): - QtWidgets.QMessageBox.information(self, "About", "Library app demonstrating the phantom read problem") \ No newline at end of file + QtWidgets.QMessageBox.information(self, "About", "Library app demonstrating the phantom read problem") + + # endregion \ No newline at end of file diff --git a/src/utils/errors/export_error.py b/src/utils/errors/export_error.py new file mode 100644 index 0000000..b3e7421 --- /dev/null +++ b/src/utils/errors/export_error.py @@ -0,0 +1,5 @@ +class ExportError(Exception): + def __init__(self, message: str): + super().__init__(message) + + self.message = message diff --git a/src/utils/errors/no_export_entity_error.py b/src/utils/errors/no_export_entity_error.py new file mode 100644 index 0000000..f5dbc99 --- /dev/null +++ b/src/utils/errors/no_export_entity_error.py @@ -0,0 +1,6 @@ +from .export_error import ExportError +class NoExportEntityError(ExportError): + def __init__(self, message: str): + super().__init__(message) + + self.message = message \ No newline at end of file