[main] Added book export to xml

This commit is contained in:
Thastertyn 2025-01-11 13:10:47 +01:00
parent 50227b9a2b
commit 142b2d449d
9 changed files with 154 additions and 23 deletions

0
src/export/__init__.py Normal file
View File

View File

@ -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 <book> element
book_element = ET.SubElement(root, "book")
# Add <title>
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

View File

@ -13,18 +13,18 @@ class BookStatusEnum(enum.Enum):
class Book(Base): class Book(Base):
__tablename__ = 'book' __tablename__ = 'book'
__table_args__ = (UniqueConstraint('isbn'),) __table_args__ = (UniqueConstraint('isbn'),)
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
author_id = Column(Integer, ForeignKey('author.id'), nullable=False) author_id = Column(Integer, ForeignKey('author.id'), nullable=False)
title = Column(String(100), nullable=False) title = Column(String(100), nullable=False)
description = Column(Text, nullable=False) description = Column(Text, nullable=False)
year_published = Column(String(4), nullable=False) year_published = Column(String(4), nullable=False)
isbn = Column(String(13), nullable=False, unique=True) isbn = Column(String(13), nullable=False, unique=True)
status = Column(Enum(BookStatusEnum), nullable=False, default=BookStatusEnum.available) status = Column(Enum(BookStatusEnum), nullable=False, default=BookStatusEnum.available)
created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) created_at = Column(TIMESTAMP, nullable=False, server_default=func.now())
last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now()) last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now())
# 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')

View File

@ -4,13 +4,19 @@ from sqlalchemy.orm import relationship
from .base import Base from .base import Base
class BookCategory(Base): class BookCategory(Base):
__tablename__ = 'book_category' __tablename__ = 'book_category'
__table_args__ = (UniqueConstraint('name'),) __table_args__ = (UniqueConstraint('name'),)
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
parent_category_id = Column(Integer, ForeignKey('book_category.id'), nullable=True) parent_category_id = Column(Integer, ForeignKey('book_category.id'), nullable=True)
name = Column(String(100), nullable=False) name = Column(String(100), nullable=False)
mature_content = Column(Integer, nullable=False, default=0) mature_content = Column(Integer, nullable=False, default=0)
last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now()) last_updated = Column(TIMESTAMP, nullable=False, server_default=func.now())
parent_category = relationship('BookCategory', remote_side=[id]) parent_category = relationship('BookCategory', remote_side=[id])
books = relationship(
'Book',
secondary='book_category_link', # Junction table
back_populates='categories' # For bidirectional relationship
)

View File

@ -11,5 +11,5 @@ class BookCategoryLink(Base):
book_id = Column(Integer, ForeignKey('book.id'), primary_key=True) book_id = Column(Integer, ForeignKey('book.id'), primary_key=True)
book_category_id = Column(Integer, ForeignKey('book_category.id'), primary_key=True) book_category_id = Column(Integer, ForeignKey('book_category.id'), primary_key=True)
book = relationship('Book', backref='categories') book = relationship('Book')
book_category = relationship('BookCategory', backref='books') book_category = relationship('BookCategory')

View File

@ -1,6 +1,8 @@
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QTextEdit, QPushButton, QComboBox, QFormLayout 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 models.book import Book, BookStatusEnum
class BookEditor(QDialog): class BookEditor(QDialog):
@ -21,18 +23,30 @@ class BookEditor(QDialog):
self.title_input = QLineEdit(self.book.title) self.title_input = QLineEdit(self.book.title)
form_layout.addRow("Title:", self.title_input) 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 # Description field
self.description_input = QTextEdit(self.book.description) self.description_input = QTextEdit(self.book.description)
form_layout.addRow("Description:", self.description_input) form_layout.addRow("Description:", self.description_input)
# Year published field # Year published field
self.year_input = QLineEdit(self.book.year_published) self.year_input = QLineEdit(self.book.year_published)
# self.year_input.setValidator
form_layout.addRow("Year Published:", self.year_input) form_layout.addRow("Year Published:", self.year_input)
# ISBN field # ISBN field
self.isbn_input = QLineEdit(self.book.isbn) 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) 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 # Status dropdown
self.status_input = QComboBox() self.status_input = QComboBox()
self.status_input.addItems([status.value for status in BookStatusEnum]) self.status_input.addItems([status.value for status in BookStatusEnum])

View File

@ -1,6 +1,7 @@
from PySide6.QtGui import QGuiApplication, QAction, QIcon from PySide6.QtGui import QGuiApplication, QAction, QIcon
from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtQml import QQmlApplicationEngine
from PySide6 import QtWidgets, QtCore from PySide6 import QtWidgets, QtCore
from PySide6.QtWidgets import QMessageBox
from ui.dashboard.dashboard import LibraryDashboard from ui.dashboard.dashboard import LibraryDashboard
from ui.book_editor.book_editor import BookEditor 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 ui.settings import SettingsDialog
from export.book_exporter import BookExporter
from utils.errors.export_error import ExportError
class LibraryWindow(QtWidgets.QMainWindow): class LibraryWindow(QtWidgets.QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -84,10 +89,25 @@ class LibraryWindow(QtWidgets.QMainWindow):
import_members_action.triggered.connect(self.import_data) import_members_action.triggered.connect(self.import_data)
import_submenu.addAction(import_members_action) import_submenu.addAction(import_members_action)
# Export action # Export submenu
export_action = QAction("Export overview", self) export_submenu = QtWidgets.QMenu(self)
export_action.triggered.connect(self.export_data) export_submenu.setTitle("Export")
file_menu.addAction(export_action) 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() file_menu.addSeparator()
@ -117,6 +137,21 @@ class LibraryWindow(QtWidgets.QMainWindow):
if dialog.exec() == QtWidgets.QDialog.Accepted: if dialog.exec() == QtWidgets.QDialog.Accepted:
print("Settings were saved.") 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): def new_book(self):
pass pass
@ -130,4 +165,6 @@ class LibraryWindow(QtWidgets.QMainWindow):
pass pass
def about(self): 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

View File

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

View File

@ -0,0 +1,6 @@
from .export_error import ExportError
class NoExportEntityError(ExportError):
def __init__(self, message: str):
super().__init__(message)
self.message = message