[main] (WIP) Work on member imports and all 3 exports

This commit is contained in:
Thastertyn 2025-01-16 23:11:46 +01:00
parent 85382ee616
commit 9c827a04f5
10 changed files with 275 additions and 97 deletions

View File

@ -1,9 +1,11 @@
from .manager import *
from .book import *
from .member import *
from .book_overview import *
__all__ = [
*manager.__all__,
*book.__all__,
*book_overview.__all__,
*member.__all__,
]

View File

@ -3,7 +3,22 @@ from sqlalchemy import update
from models import BookCategoryStatistics, Book
def update_category_statistics(session: Session, book_id: int):
statistics = session.query(BookCategoryStatistics).get(book_id)
def update_category_statistics(session: Session, book_id: int):
book = session.query(Book).filter_by(id=book_id).first()
if not book:
raise ValueError(f"Book with ID {book_id} does not exist.")
for category in book.categories:
statistics = session.query(BookCategoryStatistics).filter_by(category_id=category.id).one_or_none()
if statistics:
statistics.book_count += 1
session.add(statistics)
else:
new_statistics = BookCategoryStatistics(category_id=category.id, book_count=1)
session.add(new_statistics)
__all__ = ["update_category_statistics"]

View File

@ -1,6 +1,7 @@
import logging
from typing import List, Dict
from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError
from sqlalchemy.exc import IntegrityError, SQLAlchemyError, DatabaseError as SqlAlchemyDatabaseError
from utils.errors.database import DatabaseError, DuplicateEntryError, DatabaseConnectionError
from models import Member
@ -8,26 +9,46 @@ from database.manager import DatabaseManager
logger = logging.getLogger(__name__)
def fetch_all_members() -> List[Member]:
"""
Fetches all members from the database.
def create_new_member(new_member: Member):
:return: A list of all members in the database.
:raises DatabaseConnectionError: If the connection to the database is interrupted.
:raises DatabaseError: If any other error occurs while fetching members.
"""
with DatabaseManager.get_session() as session:
try:
return session.query(Member).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 occurred when fetching all members: {e}")
raise DatabaseError("An error occurred when fetching all members") from e
def create_member(new_member: Dict[str, str]):
create_members([new_member])
def create_members(members: List[Dict[str, str]]):
try:
with DatabaseManager.get_session() as session:
session.add(new_member)
session.add_all(members)
session.commit()
except IntegrityError as e:
logger.warning("Data already exists")
session.rollback()
logger.warning("Data already exists")
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
raise DatabaseConnectionError("Connection with database interrupted") from e
except SQLAlchemyError as e:
logger.error(f"An error occured when saving member: {e}")
session.rollback()
raise DatabaseError(
"An error occured when creating a new member") from e
logger.error(f"An error occurred when saving member: {e}")
raise DatabaseError("An error occurred when creating a new member") from e
def update_member(member: Dict[str, str]):
pass
__all__ = ["create_new_member"]
__all__ = ["create_member", "create_members", "fetch_all_members"]

View File

@ -0,0 +1,78 @@
import os
import logging
from typing import Optional, List, Dict
import xml.etree.ElementTree as ET
from xml.dom import minidom
from xmlschema import XMLSchema
from utils.errors import (
NoExportEntityError,
ExportError,
ExportFileError,
InvalidContentsError,
XsdSchemeNotFoundError,
ImportError,
)
# Initialize logger and XML Schema
logger = logging.getLogger(__name__)
from models import BooksOverview
from database import fetch_all_book_overviews
def export_to_xml(file_path: str) -> None:
logger.debug("Attempting to export book overview")
all_books = fetch_all_book_overviews()
if not all_books:
logger.warning("No books found to export")
raise NoExportEntityError("No books found to export")
xml = overviews_to_xml(all_books)
try:
with open(file_path, "w", encoding="utf-8") as file:
file.write(xml)
logger.info("Successfully saved book overview export")
except OSError as e:
raise ExportFileError("Failed to save to a file") from e
def overviews_to_xml(overview_list: List[BooksOverview]) -> str:
root = ET.Element("book_overview")
for book_overview in overview_list:
# Create a <book_entry> element
book_element = ET.SubElement(root, "book_entry")
# Add <title>
title_element = ET.SubElement(book_element, "title")
title_element.text = book_overview.title
# Add <author>
author_element = ET.SubElement(book_element, "author")
author_element.text = book_overview.author_name
# Add <year_published>
year_published_element = ET.SubElement(book_element, "year_published")
year_published_element.text = book_overview.year_published
# Add <isbn>
isbn_element = ET.SubElement(book_element, "isbn")
isbn_element.text = book_overview.isbn
# Add <borrower_name>
borrower_name = ET.SubElement(book_element, "borrower_name")
borrower_name.text = book_overview.borrower_name
# Add <librarian_name>
librarian_name = ET.SubElement(book_element, "librarian_name")
librarian_name.text = book_overview.librarian_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

@ -46,7 +46,7 @@ def export_to_xml(file_path: str) -> None:
def save_books(books: List[Dict[str, object]]):
create_books(books)
def parse_books_from_xml(file_path: str) -> List[Dict[str, object]]:
def parse_from_xml(file_path: str) -> List[Dict[str, object]]:
if not SCHEMA.is_valid(file_path):
raise InvalidContentsError("XML file is not valid according to XSD schema.")

View File

View File

@ -23,7 +23,7 @@ class BookEditor(QDialog):
if book:
self.book_id = book.id
self.logger.debug(f"Editing existing book {book.title}")
self.logger.debug(f"Editing book {book.title}")
self.create_new = False
self.fill_with_existing_data(book)
else:
@ -107,34 +107,29 @@ class BookEditor(QDialog):
QMessageBox.information(None,
"Success",
"Book updated successfully",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
QMessageBox.StandardButton.Ok)
self.accept()
except ValueError as e:
QMessageBox.critical(None,
"Invalid Input",
f"Input validation failed: {e}",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
QMessageBox.StandardButton.Ok)
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)
QMessageBox.StandardButton.Ok)
except DatabaseConnectionError as e:
QMessageBox.critical(None,
"Failed to save",
"Could not connect to the database",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
QMessageBox.StandardButton.Ok)
except DatabaseError as e:
QMessageBox.critical(self.parent,
"An error occurred",
f"Could not save the book because of the following error: {e}",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
QMessageBox.StandardButton.Ok)
def parse_inputs(self) -> Dict[str, object]:
# Title validation

View File

@ -1,4 +1,6 @@
import logging
import re
from typing import Dict
from PySide6.QtGui import QGuiApplication, QAction
from PySide6.QtQml import QQmlApplicationEngine
@ -7,7 +9,7 @@ from PySide6.QtWidgets import QVBoxLayout, QFormLayout, QLineEdit, QHBoxLayout,
from models import Member
from database.member import create_new_member
from database.member import create_new_member, update_member
from utils.errors.database import DatabaseError, DatabaseConnectionError, DuplicateEntryError
@ -20,11 +22,12 @@ class MemberEditor(QDialog):
self.create_layout()
if member:
self.member = member
self.member_id = member.id
self.logger.debug(f"Editing member {member.first_name} {member.last_name}")
self.fill_with_existing_data()
self.create_new = False
else:
self.member = Member()
self.logger.debug("Editing a new member")
self.create_new = True
def create_layout(self):
@ -68,27 +71,79 @@ class MemberEditor(QDialog):
self.layout.addLayout(self.button_layout)
def fill_with_existing_data(self):
self.first_name_input.setText(self.member.first_name)
self.last_name_input.setText(self.member.last_name)
self.email_input.setText(self.member.email)
self.phone_number_input.setText(self.member.phone)
def fill_with_existing_data(self, member: Member):
self.first_name_input.setText(member.first_name)
self.last_name_input.setText(member.last_name)
self.email_input.setText(member.email)
self.phone_number_input.setText(member.phone)
def save_member(self):
self.member.first_name = self.first_name_input.text()
self.member.last_name = self.last_name_input.text()
self.member.email = self.email_input.text()
self.member.phone = self.phone_number_input.text()
try:
member_object = self.parse_inputs()
if self.create_new:
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)
create_member(member_object)
QMessageBox.information(None,
"Success",
"Member created successfully",
QMessageBox.StandardButton.Ok)
else:
member_object["id"] = self.member_id
update_member(book_object)
QMessageBox.information(None,
"Success",
"Member updated successfully",
QMessageBox.StandardButton.Ok)
self.accept()
except ValueError as e:
QMessageBox.critical(None,
"Invalid Input",
f"Input validation failed: {e}",
QMessageBox.StandardButton.Ok)
except DuplicateEntryError as e:
QMessageBox.critical(None,
"ISBN is already in use",
"The ISBN provided is already in use",
QMessageBox.StandardButton.Ok)
except DatabaseConnectionError as e:
QMessageBox.critical(None,
"Failed to save",
"Could not connect to the database",
QMessageBox.StandardButton.Ok)
except DatabaseError as e:
QMessageBox.critical(self.parent,
"An error occurred",
f"Could not save the book because of the following error: {e}",
QMessageBox.StandardButton.Ok)
self.accept()
def parse_inputs(self) -> Dict:
first_name = self.first_name_input.text().strip()
if not first_name or len(first_name) > 50:
raise ValueError("First name must be non-empty and at most 50 characters long.")
last_name = self.last_name_input.text().strip()
if not last_name or len(last_name) > 50:
raise ValueError("Last name must be non-empty and at most 50 characters long.")
email = self.email_input.text().strip()
email_regex = r"^[\w\-\.]+@([\w\-]+\.)+[\w\-]{2,}$"
if not re.match(email_regex, email):
raise ValueError("E-mail address is not in a valid format.")
phone_number = self.phone_number_input.text().strip()
phone_number_regex = r"(\+\d{1,3})?\d{9}"
if not re.match(phone_number_regex, phone_number):
raise ValueError("Phone number is not in valid format.")
return {
"first_name": first_name,
"last_name": last_name,
"email": email,
"phone_number": phone_number
}
__all__ = ["MemberEditor"]

View File

@ -7,7 +7,8 @@ from ui.import_preview import PreviewDialog
from ui.editor import BookEditor, MemberEditor
from utils.errors import ExportError, ExportFileError
from services import book_service
from services import book_service, book_overview_service
class MenuBar(QMenuBar):
def __init__(self, parent):
@ -50,23 +51,23 @@ class MenuBar(QMenuBar):
import_submenu.addAction(import_books_action)
import_members_action = QAction("Import members", self)
import_members_action.triggered.connect(self.import_data)
import_members_action.triggered.connect(self.import_members)
import_submenu.addAction(import_members_action)
# Export submenu
export_submenu = QMenu("Export", self)
file_menu.addMenu(export_submenu)
export_overview_action = QAction("Export overview", self)
export_overview_action.triggered.connect(self.export_data)
export_submenu.addAction(export_overview_action)
export_books_action = QAction("Export books", self)
export_books_action.triggered.connect(self.export_books)
export_submenu.addAction(export_books_action)
export_overview_action = QAction("Export overview", self)
export_overview_action.triggered.connect(self.export_overviews)
export_submenu.addAction(export_overview_action)
export_members_action = QAction("Export members", self)
export_members_action.triggered.connect(self.export_data)
export_members_action.triggered.connect(self.export_members)
export_submenu.addAction(export_members_action)
file_menu.addSeparator()
@ -97,36 +98,34 @@ class MenuBar(QMenuBar):
def edit_preferences(self):
SettingsDialog(parent=self).exec()
def export_books(self):
try:
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]
def new_book(self):
BookEditor().exec()
self.parent.refresh_book_cards()
if file_path.endswith(selected_filetype):
selected_filetype = ""
book_service.export_to_xml(file_path + selected_filetype)
except ExportFileError as e:
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 occurred when exporting books: {e}",
QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.NoButton)
def new_member(self):
MemberEditor().exec()
self.parent.refresh_member_cards()
def import_books(self):
self.import_data("Book", book_service)
def import_members(self):
self.import_data("Member", memb)
def export_books(self):
self.export_data("Book", book_service)
def export_overviews(self):
self.export_data("Book overview", book_overview_service)
def export_members(self):
pass
def about(self):
QMessageBox.information(
self, "About", "Library app demonstrating the phantom read problem")
def import_data(self, import_name: str, preview_dialog, service):
try:
home_dir = QStandardPaths.writableLocation(QStandardPaths.HomeLocation)
file_path, _ = QFileDialog.getOpenFileName(self, "Choose import file", home_dir, ";;".join(self.file_types.keys()))
@ -134,18 +133,18 @@ class MenuBar(QMenuBar):
if not file_path:
return # User canceled
books = book_service.parse_books_from_xml(file_path)
if not books:
QMessageBox.information(self, "No New Books", "No new books to import.", QMessageBox.Ok)
parsed_data = service.parse_from_xml(file_path)
if not parsed_data:
QMessageBox.warning(self, f"No New {import_name}s", f"No new {import_name}s to import.", QMessageBox.Ok)
return
# Show preview dialog
dialog = PreviewDialog(books, self)
dialog = PreviewDialog(parsed_data, self)
if dialog.exec() == QDialog.Accepted:
# User confirmed, proceed with importing
book_service.create_books(books)
book_service.create_books(parsed_data)
QMessageBox.information(self, "Success", "Books imported successfully!", QMessageBox.Ok)
self.parent.redraw_book_cards()
self.parent.refresh_book_cards()
else:
QMessageBox.information(self, "Canceled", "Import was canceled.", QMessageBox.Ok)
except ImportError as e:
@ -154,18 +153,28 @@ class MenuBar(QMenuBar):
f"An error occurred when importing books from the file provided: {e}",
QMessageBox.StandardButton.Ok)
def new_book(self):
BookEditor().exec()
def export_data(self, export_name: str, service):
try:
home_dir = QStandardPaths.writableLocation(QStandardPaths.HomeLocation)
file_path, selected_filter = QFileDialog.getSaveFileName(self,
f"Save {export_name} export",
home_dir,
";;".join(self.file_types.keys()))
if file_path:
selected_filetype = self.file_types[selected_filter]
def new_member(self):
MemberEditor().exec()
if file_path.endswith(selected_filetype):
selected_filetype = ""
def import_data(self):
pass
service.export_to_xml(file_path + selected_filetype)
def export_data(self):
pass
def about(self):
QMessageBox.information(
self, "About", "Library app demonstrating the phantom read problem")
except ExportFileError as e:
QMessageBox.critical(self,
"Error saving file",
f"Error occurred when saving the exported data: {e}",
QMessageBox.StandardButton.Ok)
except ExportError as e:
QMessageBox.critical(self,
f"Error exporting {export_name}s",
f"An error occurred when exporting {export_name}s: {e}",
QMessageBox.StandardButton.Ok)

View File

@ -44,5 +44,8 @@ class LibraryWindow(QMainWindow):
# Move the window to the calculated geometry
self.move(window_geometry.topLeft())
def redraw_book_cards(self):
self.dashboard.redraw_cards()
def refresh_book_cards(self):
self.dashboard.redraw_cards()
def refresh_member_cards(self):
self.member_list.redraw_cards()