[main] Added book export to xml
This commit is contained in:
parent
50227b9a2b
commit
142b2d449d
0
src/export/__init__.py
Normal file
0
src/export/__init__.py
Normal file
63
src/export/book_exporter.py
Normal file
63
src/export/book_exporter.py
Normal 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
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
@ -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')
|
@ -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])
|
||||||
|
@ -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
|
5
src/utils/errors/export_error.py
Normal file
5
src/utils/errors/export_error.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class ExportError(Exception):
|
||||||
|
def __init__(self, message: str):
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
self.message = message
|
6
src/utils/errors/no_export_entity_error.py
Normal file
6
src/utils/errors/no_export_entity_error.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .export_error import ExportError
|
||||||
|
class NoExportEntityError(ExportError):
|
||||||
|
def __init__(self, message: str):
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
self.message = message
|
Loading…
x
Reference in New Issue
Block a user