From 8ab57d69136eaecde24addea2b71c09359c6e40a Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:02:27 +0200 Subject: [PATCH 01/83] implement backup, fix ret date bug --- src/logic/backup.py | 25 ++++++++++++++ src/ui/main_ui.py | 84 ++++++++++++++++++++++++++------------------- 2 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 src/logic/backup.py diff --git a/src/logic/backup.py b/src/logic/backup.py new file mode 100644 index 0000000..cf514e2 --- /dev/null +++ b/src/logic/backup.py @@ -0,0 +1,25 @@ +import os +import sys +import shutil +from src import config + + +class Backup: + def __init__(self): + self.source_path = config.database.path + "/" + config.database.name + self.backup_path = config.database.backupLocation + "/" + config.database.name + self.backup = False + self.checkpaths() + + def checkpaths(self): + if os.path.exists(config.database.backupLocation): + self.backup = True + + def createBackup(self): + if self.backup: + if os.path.exists(self.source_path): + if os.path.exists(self.backup_path): + os.remove(self.backup_path) + shutil.copy(self.source_path, self.backup_path) + return True + return False diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index a7a2977..844b7fd 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -5,12 +5,14 @@ from .createUser import CreateUser from .multiUserInfo import MultiUserFound from .newentry import NewEntry from src import config -from src.logic import Database, Catalogue -from src.utils.stringtodate import stringToDate +from src.logic import Database, Catalogue, Backup +from src.utils import stringToDate from src.schemas import User, Book from PyQt6 import QtCore, QtGui, QtWidgets import sys +import atexit +backup = Backup() class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self): @@ -64,7 +66,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_username.clear() self.input_userno.clear() self.userdata.clear() - self.mediaOverview.clearContents() + self.mediaOverview.setRowCount(0) self.btn_show_lentmedia.setText("") self.input_file_ident.clear() self.nextReturnDate.hide() @@ -106,25 +108,31 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.activeUser = user[0] if self.activeUser is not None: + print("User found", self.activeUser) self.setUserData() self.input_file_ident.setFocus() self.mode.setText("Ausleihe") self.btn_show_lentmedia.setText(self.db.getActiveLoans(self.activeUser.id)) retdate = self.db.selectClosestReturnDate(self.activeUser.id) - date = stringToDate(retdate) - self.nextReturnDate.setText(date) - self.nextReturnDate.show() - self.label_7.show() + if retdate: + date = stringToDate(retdate) + self.nextReturnDate.setText(date) + self.nextReturnDate.show() + self.label_7.show() def moveToLine(self, line): line.setFocus() def mediaAdd(self, identifier): print("Adding Media", identifier) + self.setStatusTip("") + self.input_file_ident.clear() + self.input_file_ident.setEnabled(False) + user_id = self.activeUser.id cat = Catalogue() media = cat.get_book(identifier) - + print(media) book_id = self.db.checkMediaExists(media) print(book_id) if book_id: @@ -137,6 +145,8 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): if loaned: print("Book already loaned") self.setStatusTip("Book already loaned") + self.input_file_ident.setEnabled(True) + return else: print("Book not loaned, loaning now") @@ -156,33 +166,22 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.mediaOverview.setItem( 0, 2, QtWidgets.QTableWidgetItem("Entliehen") ) + else: + book_id = self.db.insertMedia(media) + self.db.insertLoan( + userid=user_id, + mediaid=book_id, + loandate=self.currentDate.toString(), + duedate=self.duedate.date().toString(), + ) - # print(media) - # if media: - # print(book_id, type(book_id)) - # loaned = self.db.checkLoanState(book_id) - # print(loaned) - # if self.db.checkMediaExists(media): - # print("Book already exists", book_id) - # else: - # book_id = self.db.insertMedia(media) - # print(book_id) - # self.db.insertLoan( - # self.activeUser.id, - # book_id, - # self.currentDate.toString(), - # self.duedate.date().toString(), - # ) - - # self.mediaOverview.insertRow(0) - # self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(media.isbn)) - # self.mediaOverview.setItem(0, 1, QtWidgets.QTableWidgetItem(media.title)) - # self.mediaOverview.setItem(0, 2, QtWidgets.QTableWidgetItem("Entliehen")) - - # # add media to database - # # check if book present in database - # print(self.db.getActiveLoans(self.activeUser.id)) - # self.btn_show_lentmedia.setText(self.db.getActiveLoans(self.activeUser.id)) + self.btn_show_lentmedia.setText(self.db.getActiveLoans(self.activeUser.id)) + self.nextReturnDate.setText( + stringToDate(self.db.selectClosestReturnDate(self.activeUser.id)) + ) + self.nextReturnDate.show() + self.label_7.show() + self.input_file_ident.setEnabled(True) def callShortcut(self, shortcut): print("Calling Shortcut", shortcut) @@ -216,7 +215,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): # set userdata in lineedits self.activeUser = user self.setUserData() - book = self.db.returnMedia(book_id[0]) + book = self.db.returnMedia(book_id[0], self.currentDate.toString()) self.mediaOverview.insertRow(0) self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(book.isbn)) self.mediaOverview.setItem(0, 1, QtWidgets.QTableWidgetItem(book.title)) @@ -234,6 +233,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): print("Book not found") def lendMedia(self, value): + value = value.strip() if value.isnumeric(): self.mediaAdd(value) else: @@ -242,8 +242,22 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): else: self.mediaAdd(value) +def exit_handler(): + print("Exiting") + state = backup.createBackup() + # create dialog to show state + app = QtWidgets.QApplication(sys.argv) + dialog = QtWidgets.QMessageBox() + if state == True: + dialog.setText("Backup created successfully") + else: + dialog.setText("Backup creation failed") + dialog.exec() + def launch(): app = QtWidgets.QApplication(sys.argv) main_ui = MainUI() + atexit.register(exit_handler) sys.exit(app.exec()) + # sys.exit(app.exec()) From 7ea671e7b070cb74bf67f5be64a2608959b13a82 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:09:11 +0200 Subject: [PATCH 02/83] add icon, implement color change and overwrite --- src/utils/icon.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/utils/icon.py diff --git a/src/utils/icon.py b/src/utils/icon.py new file mode 100644 index 0000000..c75dd97 --- /dev/null +++ b/src/utils/icon.py @@ -0,0 +1,76 @@ +from omegaconf import OmegaConf +from PyQt6 import QtGui +import re + +config = OmegaConf.load("icons/icons.yaml") + +path = "icons/" + + +class Icon: + def __init__(self, icon_type, widget=None): + self.icon = QtGui.QIcon() + self.icon_path = path + config["icons"][icon_type] + self.add_icon(self.icon_path) + if widget is not None: + widget.setIcon(self.icon) + + def add_icon(self, icon_path): + icon = self.changeColor(icon_path) + + # use icon bytes to create a pixmap + pixmap = QtGui.QPixmap() + pixmap.loadFromData(icon) + + self.icon.addPixmap( + pixmap, + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) + + def overwriteColor(self, color): + # take the icon, read it as bytes and change the color in fill + icon = self.changeColor(self.icon_path) + cicon = str(icon) + fill = re.search(r"fill=\"(.*?)\"", cicon).group(1) + stroke = re.search(r"stroke=\"(.*?)\"", cicon) + if stroke: + stroke = stroke.group(1) + + if fill and stroke: + # replace stroke + newicon = icon.replace(stroke.encode(), color.encode()) + else: + newicon = icon.replace(fill.encode(), color.encode()) + + pixmap = QtGui.QPixmap() + pixmap.loadFromData(newicon) + + self.icon.addPixmap( + pixmap, + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) + return self.icon + + @staticmethod + def changeColor(icon_path) -> bytes: + """change the color of the svg icon to the color set in the config file + + Args: + icon_path (str): the path to the icon, usually icons/[name].svg + + Returns: + icon: a byte representation of the icon with the new color + """ + color = config.color + with open(icon_path, "rb") as file: + icon = file.read() + cicon = str(icon) + fill = re.search(r"fill=\"(.*?)\"", cicon).group(1) + icon = icon.replace(fill.encode(), config.color.encode()) + return icon + + +if __name__ == "__main__": + print("This is a module and can not be executed directly.") From 5afffbdcfa28bc29184936ce37e3c4cda7b05f86 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:09:29 +0200 Subject: [PATCH 03/83] ui changes --- .../sources/Ui_dialog_extendLoanDuration.py | 1 + src/ui/sources/Ui_main_UserInterface.py | 2 +- src/ui/sources/Ui_main_userData.py | 17 +++++++++------ src/ui/sources/dialog_extendLoanDuration.ui | 3 +++ src/ui/sources/main_UserInterface.ui | 2 +- src/ui/sources/main_userData.ui | 21 +++++++++++++------ 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/ui/sources/Ui_dialog_extendLoanDuration.py b/src/ui/sources/Ui_dialog_extendLoanDuration.py index d542ee7..e6572a7 100644 --- a/src/ui/sources/Ui_dialog_extendLoanDuration.py +++ b/src/ui/sources/Ui_dialog_extendLoanDuration.py @@ -12,6 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") + Dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) Dialog.resize(400, 300) self.gridLayout = QtWidgets.QGridLayout(Dialog) self.gridLayout.setObjectName("gridLayout") diff --git a/src/ui/sources/Ui_main_UserInterface.py b/src/ui/sources/Ui_main_UserInterface.py index 450504c..999aee7 100644 --- a/src/ui/sources/Ui_main_UserInterface.py +++ b/src/ui/sources/Ui_main_UserInterface.py @@ -175,4 +175,4 @@ class Ui_MainWindow(object): self.actionRueckgabemodus.setText(_translate("MainWindow", "Rückgabemodus")) self.actionRueckgabemodus.setShortcut(_translate("MainWindow", "F5")) self.actionNutzer.setText(_translate("MainWindow", "Nutzer")) - self.actionNutzer.setShortcut(_translate("MainWindow", "U")) + self.actionNutzer.setShortcut(_translate("MainWindow", "F6")) diff --git a/src/ui/sources/Ui_main_userData.py b/src/ui/sources/Ui_main_userData.py index 9f058f3..5b6d55d 100644 --- a/src/ui/sources/Ui_main_userData.py +++ b/src/ui/sources/Ui_main_userData.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") - MainWindow.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) + MainWindow.setWindowModality(QtCore.Qt.WindowModality.WindowModal) MainWindow.resize(800, 600) self.centralwidget = QtWidgets.QWidget(parent=MainWindow) self.centralwidget.setObjectName("centralwidget") @@ -71,6 +71,7 @@ class Ui_MainWindow(object): self.horizontalLayout_2 = QtWidgets.QHBoxLayout() self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.radio_allLoanedMedia = QtWidgets.QRadioButton(parent=self.centralwidget) + self.radio_allLoanedMedia.setChecked(True) self.radio_allLoanedMedia.setObjectName("radio_allLoanedMedia") self.horizontalLayout_2.addWidget(self.radio_allLoanedMedia) self.radio_currentlyLoaned = QtWidgets.QRadioButton(parent=self.centralwidget) @@ -120,6 +121,7 @@ class Ui_MainWindow(object): self.UserMediaTable.setHorizontalHeaderItem(5, item) self.UserMediaTable.horizontalHeader().setDefaultSectionSize(156) self.UserMediaTable.horizontalHeader().setMinimumSectionSize(43) + self.UserMediaTable.horizontalHeader().setSortIndicatorShown(True) self.UserMediaTable.verticalHeader().setDefaultSectionSize(31) self.UserMediaTable.verticalHeader().setMinimumSectionSize(25) self.verticalLayout.addWidget(self.UserMediaTable) @@ -150,13 +152,16 @@ class Ui_MainWindow(object): self.radio_overdueLoans.setText(_translate("MainWindow", "Überzogen")) self.btn_searchTableContent.setText(_translate("MainWindow", "Suchen")) self.btn_extendSelectedMedia.setText(_translate("MainWindow", "Ausgewählte Verlängern")) - item = self.UserMediaTable.horizontalHeaderItem(1) + self.UserMediaTable.setSortingEnabled(True) + item = self.UserMediaTable.horizontalHeaderItem(0) item.setText(_translate("MainWindow", "ISBN")) - item = self.UserMediaTable.horizontalHeaderItem(2) + item = self.UserMediaTable.horizontalHeaderItem(1) item.setText(_translate("MainWindow", "Signatur")) - item = self.UserMediaTable.horizontalHeaderItem(3) + item = self.UserMediaTable.horizontalHeaderItem(2) item.setText(_translate("MainWindow", "Titel")) - item = self.UserMediaTable.horizontalHeaderItem(4) + item = self.UserMediaTable.horizontalHeaderItem(3) item.setText(_translate("MainWindow", "entliehen am")) - item = self.UserMediaTable.horizontalHeaderItem(5) + item = self.UserMediaTable.horizontalHeaderItem(4) item.setText(_translate("MainWindow", "entliehen bis")) + item = self.UserMediaTable.horizontalHeaderItem(5) + item.setText(_translate("MainWindow", "Rückgabe am")) diff --git a/src/ui/sources/dialog_extendLoanDuration.ui b/src/ui/sources/dialog_extendLoanDuration.ui index c4c4c1c..ce3374e 100644 --- a/src/ui/sources/dialog_extendLoanDuration.ui +++ b/src/ui/sources/dialog_extendLoanDuration.ui @@ -2,6 +2,9 @@ Dialog + + Qt::ApplicationModal + 0 diff --git a/src/ui/sources/main_UserInterface.ui b/src/ui/sources/main_UserInterface.ui index a905129..ec53b9a 100644 --- a/src/ui/sources/main_UserInterface.ui +++ b/src/ui/sources/main_UserInterface.ui @@ -238,7 +238,7 @@ Nutzer - U + F6 diff --git a/src/ui/sources/main_userData.ui b/src/ui/sources/main_userData.ui index 13e0cb6..97d572c 100644 --- a/src/ui/sources/main_userData.ui +++ b/src/ui/sources/main_userData.ui @@ -3,7 +3,7 @@ MainWindow - Qt::ApplicationModal + Qt::WindowModal @@ -133,6 +133,9 @@ Alle Ausleihen + + true + @@ -218,23 +221,24 @@ QAbstractItemView::SelectRows + + true + 43 156 + + true + 25 31 - - - - - ISBN @@ -260,6 +264,11 @@ entliehen bis + + + Rückgabe am + + From 6203d6a4a854179cffdc6420d88e5b29daf632bf Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:14:47 +0200 Subject: [PATCH 04/83] update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e3cae33..fce84ba 100644 --- a/.gitignore +++ b/.gitignore @@ -218,3 +218,5 @@ compile_commands.json .history/* **/tempCodeRunnerFile.py + +output/** \ No newline at end of file From 0e9626546d52c214262074be3a8ee7dc54b72562 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:55:37 +0200 Subject: [PATCH 05/83] update readme --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5996908..ba0d310 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,30 @@ # LibrarySystem -universal library system for facilities in the university \ No newline at end of file +universal library system for facilities in the university + + +## What is this? + +This is a library system for the different facilities in the university. Because some facilities lend their media, some wanted a software that allows them to keep track of who had what when. + +### Installation +either clone the Repo +``` +git clone https://git.theprivateserver.de/WorldTeacher/LibrarySystem.git +cd LibrarySystem +./build(.exe) +``` + +or head over to [releases](https://git.theprivateserver.de/WorldTeacher/LibrarySystem/releases) +and download the latest version + +### Configuration + +the software contains a config file in configs, as well as a config for icons, located in icons. + +#### config +tbd + +#### icons +- color is a value to re-paint the icons +- icons/* a dict of the icons structured like: icon_Name_in_Application : icon_File_name \ No newline at end of file From 891eb3e90a470a85e233f7ddf1e58a0de7256397 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:56:32 +0200 Subject: [PATCH 06/83] update settinfs, icons --- config/settings.yaml | 3 +-- icons/icons.yaml | 6 ++++++ icons/library.svg | 1 + icons/library_add.svg | 1 + icons/settings.svg | 1 + icons/warning.svg | 1 + 6 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 icons/icons.yaml create mode 100644 icons/library.svg create mode 100644 icons/library_add.svg create mode 100644 icons/settings.svg create mode 100644 icons/warning.svg diff --git a/config/settings.yaml b/config/settings.yaml index 250b073..a96ce4b 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -3,6 +3,5 @@ default_loan_duration: 7 database: path: C:/db name: library.db - backupLocation: ./backup + backupLocation: V:/backup library_id: 20735 - diff --git a/icons/icons.yaml b/icons/icons.yaml new file mode 100644 index 0000000..1494936 --- /dev/null +++ b/icons/icons.yaml @@ -0,0 +1,6 @@ +color: '#B89230' #Hex code of the color +icons: + newentry: library_add.svg + main: library.svg + warning: warning.svg + settings: settings.svg \ No newline at end of file diff --git a/icons/library.svg b/icons/library.svg new file mode 100644 index 0000000..52dfdd1 --- /dev/null +++ b/icons/library.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/library_add.svg b/icons/library_add.svg new file mode 100644 index 0000000..a3f0045 --- /dev/null +++ b/icons/library_add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/settings.svg b/icons/settings.svg new file mode 100644 index 0000000..328e2f0 --- /dev/null +++ b/icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/warning.svg b/icons/warning.svg new file mode 100644 index 0000000..d0be798 --- /dev/null +++ b/icons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file From ae2e58878035a547c135133114a89a383b2944df Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:57:20 +0200 Subject: [PATCH 07/83] update files, bring to latest code --- main.py | 5 +- src/logic/__init__.py | 3 +- src/logic/catalogue.py | 2 +- src/logic/database.py | 60 ++++++++++- src/schemas/book.py | 4 + src/schemas/database.py | 3 +- src/ui/extendLoan.py | 31 ++++++ src/ui/main_ui.py | 103 +++++++++++------- src/ui/newentry.py | 39 +++++-- src/ui/settings.py | 137 +++++++++++++++++++++++ src/ui/sources/Ui_dialog_settings.py | 92 ++++++++++++++++ src/ui/sources/dialog_settings.ui | 156 +++++++++++++++++++++++++++ src/ui/user.py | 109 ++++++++++++++++++- src/utils/__init__.py | 2 + 14 files changed, 686 insertions(+), 60 deletions(-) create mode 100644 src/ui/extendLoan.py create mode 100644 src/ui/settings.py create mode 100644 src/ui/sources/Ui_dialog_settings.py create mode 100644 src/ui/sources/dialog_settings.ui create mode 100644 src/utils/__init__.py diff --git a/main.py b/main.py index 1d2aae4..1bce9d3 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ -hello_world = lambda: "Hello, World!" +from src.ui.main_ui import launch -print(hello_world()) +if __name__ == "__main__": + launch() diff --git a/src/logic/__init__.py b/src/logic/__init__.py index fc1efde..d9dd733 100644 --- a/src/logic/__init__.py +++ b/src/logic/__init__.py @@ -1,3 +1,4 @@ __help__ = "This package contains the logic of the application." from .database import Database -from .catalogue import Catalogue \ No newline at end of file +from .catalogue import Catalogue +from .backup import Backup \ No newline at end of file diff --git a/src/logic/catalogue.py b/src/logic/catalogue.py index c1451e7..a29756b 100644 --- a/src/logic/catalogue.py +++ b/src/logic/catalogue.py @@ -3,7 +3,7 @@ from bs4 import BeautifulSoup from src import config from src.schemas import Book -URL = "https://rds.ibs-bw.de/phfreiburg/opac/RDSIndex/Search?lookfor={}+&type=AllFields&limit=10&sort=py+desc%2C+title" +URL = 'https://rds.ibs-bw.de/phfreiburg/opac/RDSIndex/Search?lookfor="{}"+&type=AllFields&limit=10&sort=py+desc%2C+title' BASE = "https://rds.ibs-bw.de" diff --git a/src/logic/database.py b/src/logic/database.py index 066ddde..0815f0e 100644 --- a/src/logic/database.py +++ b/src/logic/database.py @@ -3,7 +3,7 @@ import os from src import config from pathlib import Path from src.schemas import USERS, MEDIA, LOANS, User, Book - +from src.utils.stringtodate import stringToDate class Database: def __init__(self, db_path: str = None): @@ -143,6 +143,32 @@ class Database: self.close_connection(conn) return cursor.lastrowid + def getMediaSimilarSignatureByID(self, media_id) -> list[Book]: + query = f"SELECT * FROM media WHERE id = '{media_id}'" + conn = self.connect() + cursor = conn.cursor() + cursor.execute(query) + result = cursor.fetchone() + signature = result[1] + print(signature) + query = f"SELECT * FROM media WHERE signature LIKE '%{signature}%'" + cursor.execute(query) + result = cursor.fetchall() + + self.close_connection(conn) + data = [] + for res in result: + data.append( + Book( + signature=res[1], + isbn=res[2], + ppn=res[3], + title=res[4], + database_id=res[0], + ) + ) + return data + def getMedia(self, media_id): query = f"SELECT * FROM media WHERE id = '{media_id}'" conn = self.connect() @@ -159,6 +185,25 @@ class Database: ) return res + def getAllMedia(self, user_id): + # get all books that have the user id in the loans table + query = f"SELECT * FROM loans WHERE user_id = '{user_id}'" + conn = self.connect() + cursor = conn.cursor() + cursor.execute(query) + result = cursor.fetchall() + self.close_connection(conn) + books = [] + for res in result: + book = self.getMedia(res[2]) + book.loan_from = res[3] + book.loan_to = res[4] + book.returned = res[5] + book.returned_date = res[6] + books.append(book) + print(book) + return books + def checkMediaExists(self, media): conn = self.connect() cursor = conn.cursor() @@ -182,8 +227,8 @@ class Database: self.close_connection(conn) return result - def returnMedia(self, media_id): - query = f"UPDATE loans SET returned = 1 WHERE media_id = '{media_id}'" + def returnMedia(self, media_id, returndate): + query = f"UPDATE loans SET returned = 1, returned_date = '{returndate}' WHERE media_id = '{media_id}' AND returned = 0" conn = self.connect() cursor = conn.cursor() cursor.execute(query) @@ -222,3 +267,12 @@ class Database: self.close_connection(conn) if result is not None: return result[0] + + def extendLoanDuration(self, signature, newDate): + book_id = self.checkMediaExists(Book(signature=signature)) + query = f"UPDATE loans SET return_date = '{newDate}' WHERE media_id = '{book_id[0]}' AND returned = 0" + conn = self.connect() + cursor = conn.cursor() + cursor.execute(query) + conn.commit() + self.close_connection(conn) diff --git a/src/schemas/book.py b/src/schemas/book.py index 1e8e292..6e9a904 100644 --- a/src/schemas/book.py +++ b/src/schemas/book.py @@ -10,3 +10,7 @@ class Book: link: str | None = None database_id: int | None = None link: str | None = None + loan_from: str | None = None + loan_to: str | None = None + returned: int | None = None + returned_date: str | None = None diff --git a/src/schemas/database.py b/src/schemas/database.py index 614e947..0a1dabb 100644 --- a/src/schemas/database.py +++ b/src/schemas/database.py @@ -9,7 +9,7 @@ signature TEXT NOT NULL, isbn TEXT NOT NULL, ppn TEXT NOT NULL, title TEXT NOT NULL, -link TEXT NOT NULL,); +link TEXT NOT NULL); """ LOANS = """CREATE TABLE IF NOT EXISTS loans ( @@ -19,6 +19,7 @@ media_id INTEGER NOT NULL, loan_date TEXT NOT NULL, return_date TEXT NOT NULL, returned INTEGER DEFAULT 0, +returned_date TEXT, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (media_id) REFERENCES media(id)); """ diff --git a/src/ui/extendLoan.py b/src/ui/extendLoan.py new file mode 100644 index 0000000..5977a29 --- /dev/null +++ b/src/ui/extendLoan.py @@ -0,0 +1,31 @@ +from .sources.Ui_dialog_extendLoanDuration import Ui_Dialog +from PyQt6 import QtWidgets, QtCore + + +class ExtendLoan(QtWidgets.QDialog, Ui_Dialog): + def __init__(self, user, media): + super(ExtendLoan, self).__init__() + self.setupUi(self) + self.user = user + self.media = media + self.currentDate = QtCore.QDate.currentDate().addDays(1) + self.extendDate = None + self.extenduntil.setMinimumDate(self.currentDate) + self.show() + self.buttonBox.accepted.connect(self.extendLoan) + + def extendLoan(self): + print("Extend Loan") + selectedDate = self.extenduntil.selectedDate() + print(selectedDate) + self.extendDate = selectedDate + self.close() + pass + + +def launch(user, media): + import sys + + app = QtWidgets.QApplication(sys.argv) + window = ExtendLoan(user, media) + sys.exit(app.exec()) diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index 844b7fd..51f53b4 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -4,9 +4,10 @@ from .user import UserUI from .createUser import CreateUser from .multiUserInfo import MultiUserFound from .newentry import NewEntry +from .settings import Settings from src import config from src.logic import Database, Catalogue, Backup -from src.utils import stringToDate +from src.utils import stringToDate, Icon from src.schemas import User, Book from PyQt6 import QtCore, QtGui, QtWidgets import sys @@ -18,6 +19,8 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self): super(MainUI, self).__init__() self.setupUi(self) + self.setWindowTitle("Handbibliotheksleihsystem") + self.setWindowIcon(Icon("main").icon) self.db = Database() self.currentDate = QtCore.QDate.currentDate() self.label_7.hide() @@ -29,7 +32,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): # hotkeys self.actionRueckgabemodus.triggered.connect(self.changeMode) self.actionNutzer.triggered.connect(self.showUser) - + self.actionEinstellungen.triggered.connect(self.showSettings) #Buttons self.btn_show_lentmedia.clicked.connect(self.showUser) @@ -40,7 +43,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_username.returnPressed.connect( lambda: self.checkUser("username", self.input_username.text()) ) - self.input_file_ident.returnPressed.connect(self.addMedia) + self.input_file_ident.returnPressed.connect(self.handleLineInput) self.input_userno.setValidator(QtGui.QIntValidator()) # TableWidget @@ -52,29 +55,29 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.show() + def showSettings(self): + settings = Settings() + settings.exec() + def changeMode(self): - current = self.mode.text() - if current == "Ausleihe": - self.mode.setText("Rückgabe") - self.input_username.clear() - self.input_userno.clear() - self.userdata.clear() - self.btn_show_lentmedia.setText("") - self.label_7.hide() - self.nextReturnDate.hide() - if current == "Rückgabe": - self.input_username.clear() - self.input_userno.clear() - self.userdata.clear() - self.mediaOverview.setRowCount(0) - self.btn_show_lentmedia.setText("") - self.input_file_ident.clear() - self.nextReturnDate.hide() - self.label_7.hide() + self.mode.setText("Rückgabe") + self.input_username.clear() + self.input_userno.clear() + self.userdata.clear() + self.btn_show_lentmedia.setText("") + self.input_file_ident.clear() + self.label_7.hide() + self.nextReturnDate.hide() def showUser(self): if self.activeUser is None: # create warning dialog + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Kein Nutzer ausgewählt") + dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning) + dialog.setWindowIcon(Icon("warning").overwriteColor("#EA3323")) + dialog.setText("Kein Nutzer ausgewählt") + dialog.exec() return self.user_ui = UserUI( @@ -123,7 +126,18 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def moveToLine(self, line): line.setFocus() + def handleLineInput(self): + value = self.input_file_ident.text().strip() + if len(value) <= 2: + self.callShortcut(value) + else: + if self.mode.text() == "Rückgabe": + self.returnMedia(value) + else: + self.mediaAdd(value) + def mediaAdd(self, identifier): + self.clearStatusTip print("Adding Media", identifier) self.setStatusTip("") self.input_file_ident.clear() @@ -138,13 +152,33 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): if book_id: if len(book_id) > 1: print("Multiple Books found") + # TODO: implement book selection dialog return else: # check if book is already loaned loaned = self.db.checkLoanState(book_id[0]) if loaned: print("Book already loaned") - self.setStatusTip("Book already loaned") + self.setStatusTipMessage("Buch bereits entliehen") + # dialog with yes no to create new entry + dialog = QtWidgets.QMessageBox() + dialog.setText( + "Buch bereits entliehen, soll ein neues hinzugefügt werden?" + ) + dialog.setStandardButtons( + QtWidgets.QMessageBox.StandardButton.Yes + | QtWidgets.QMessageBox.StandardButton.No + ) + dialog.setDefaultButton(QtWidgets.QMessageBox.StandardButton.No) + dialog.exec() + result = dialog.result() + if result == QtWidgets.QMessageBox.StandardButton.No: + return + newentry = NewEntry([book_id[0]]) + newentry.exec() + self.setStatusTipMessage("Neues Exemplar hinzugefügt") + created_ids = newentry.newIds + print(created_ids) self.input_file_ident.setEnabled(True) return @@ -189,14 +223,6 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): sysem_shortcuts = None print(sysem_shortcuts) - def addMedia(self): - mode = self.mode.text() - value = self.input_file_ident.text() - # if vaule is string, call shortcut, else mediaAdd - if mode == "Rückgabe": - self.returnMedia(value) - else: - self.lendMedia(value) def returnMedia(self, identifier): print("Returning Media", identifier) @@ -232,15 +258,12 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): else: print("Book not found") - def lendMedia(self, value): - value = value.strip() - if value.isnumeric(): - self.mediaAdd(value) - else: - if len(value) <= 2: - self.callShortcut(value) - else: - self.mediaAdd(value) + def setStatusTipMessage(self, message): + self.setStatusTip(message) + + @property + def clearStatusTip(self): + self.setStatusTip("") def exit_handler(): print("Exiting") @@ -258,6 +281,6 @@ def exit_handler(): def launch(): app = QtWidgets.QApplication(sys.argv) main_ui = MainUI() - atexit.register(exit_handler) + # atexit.register(exit_handler) sys.exit(app.exec()) # sys.exit(app.exec()) diff --git a/src/ui/newentry.py b/src/ui/newentry.py index 155f0b3..2c6a2e6 100644 --- a/src/ui/newentry.py +++ b/src/ui/newentry.py @@ -2,18 +2,20 @@ from .sources.Ui_dialog_addNewTitleEntry import Ui_Dialog from PyQt6 import QtWidgets, QtCore, QtGui from src.logic import Database from src.schemas import Book - +from src.utils import Icon class NewEntry(QtWidgets.QDialog, Ui_Dialog): - def __init__(self, title): + def __init__(self, title_id: list[int]): super(NewEntry, self).__init__() self.setupUi(self) self.setWindowTitle("Neues Exemplar hinzufügen") + self.setWindowIcon(Icon("newentry").overwriteColor("#ffffff")) self.tableWidget.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.ResizeMode.Stretch ) self.db = Database() - self.titles = title + self.titles = title_id + self.newIds = [] self.populateTable() self.btn_addNewBook.clicked.connect(self.addEntry) self.buttonBox.accepted.connect(self.insertEntry) @@ -26,16 +28,30 @@ class NewEntry(QtWidgets.QDialog, Ui_Dialog): self.tableWidget.setItem( row, i, QtWidgets.QTableWidgetItem(self.tableWidget.item(row - 1, i)) ) - + if i == 2 and "+" in self.tableWidget.item(row, i).text(): + entry = self.tableWidget.item(row, i).text().split("+")[1] + entry = str(int(entry) + 1) + self.tableWidget.setItem( + row, + i, + QtWidgets.QTableWidgetItem( + self.tableWidget.item(row, i).text().split("+")[0] + "+" + entry + ), + ) def populateTable(self): for title in self.titles: print(title) - entry = self.db.getMedia(title) - self.tableWidget.insertRow(0) - self.tableWidget.setItem(0, 0, QtWidgets.QTableWidgetItem(entry.isbn)) - self.tableWidget.setItem(0, 1, QtWidgets.QTableWidgetItem(entry.title)) - self.tableWidget.setItem(0, 2, QtWidgets.QTableWidgetItem(entry.signature)) - self.tableWidget.setItem(0, 3, QtWidgets.QTableWidgetItem(entry.ppn)) + entries = self.db.getMediaSimilarSignatureByID(title) + # sort by signature + entries.sort(key=lambda x: x.signature, reverse=True) + for entry in entries: + self.tableWidget.insertRow(0) + self.tableWidget.setItem(0, 0, QtWidgets.QTableWidgetItem(entry.isbn)) + self.tableWidget.setItem(0, 1, QtWidgets.QTableWidgetItem(entry.title)) + self.tableWidget.setItem( + 0, 2, QtWidgets.QTableWidgetItem(entry.signature) + ) + self.tableWidget.setItem(0, 3, QtWidgets.QTableWidgetItem(entry.ppn)) def insertEntry(self): # get all rows, convert them to Book and insert into database @@ -52,7 +68,8 @@ class NewEntry(QtWidgets.QDialog, Ui_Dialog): ) print(book) if not self.db.checkMediaExists(book): - self.db.insertMedia(book) + newBookId = self.db.insertMedia(book) + self.newIds.append(newBookId) def launch(): diff --git a/src/ui/settings.py b/src/ui/settings.py new file mode 100644 index 0000000..d3d79c6 --- /dev/null +++ b/src/ui/settings.py @@ -0,0 +1,137 @@ +from .sources.Ui_dialog_settings import Ui_Dialog +from PyQt6 import QtWidgets, QtCore +from src.utils import Icon +from src import config +from omegaconf import OmegaConf + + +class Settings(QtWidgets.QDialog, Ui_Dialog): + def __init__(self): + super(Settings, self).__init__() + self.setupUi(self) + self.setWindowTitle("Einstellungen") + self.setWindowIcon(Icon("settings").icon) + self.originalSettings = config + + # lineedits + self.institution_name.textChanged.connect(self.enableButtonBox) + self.default_loan_duration.textChanged.connect(self.enableButtonBox) + self.database_backupLocation.textChanged.connect(self.enableButtonBox) + self.database_path.textChanged.connect(self.enableButtonBox) + self.database_name.textChanged.connect(self.enableButtonBox) + self.database_name.textChanged.connect(self.enableButtonBox) + + # buttonbox + self.buttonBox.accepted.connect(self.saveSettings) + self.buttonBox.rejected.connect(self.close) + self.loadSettings() + self.buttonBox.button( + QtWidgets.QDialogButtonBox.StandardButton.Discard + ).clicked.connect(self.DiscardSettings) + self.buttonBox.button( + QtWidgets.QDialogButtonBox.StandardButton.Discard + ).setEnabled(False) + self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( + False + ) + + # buttons + self.btn_select_database_backupLocation.clicked.connect( + self.selectBackupLocation + ) + self.btn_select_database_path.clicked.connect(self.selectDatabasePath) + self.btn_select_database_name.clicked.connect(self.selectDatabaseName) + + def enableButtonBox(self): + self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( + True + ) + self.buttonBox.button( + QtWidgets.QDialogButtonBox.StandardButton.Discard + ).setEnabled(True) + + def selectBackupLocation(self): + backupLocation = QtWidgets.QFileDialog.getExistingDirectory( + self, + "Select Backup Location", + self.originalSettings.database.backupLocation, + ) + self.database_backupLocation.setText(backupLocation) + self.buttonBox.button( + QtWidgets.QDialogButtonBox.StandardButton.Discard + ).setEnabled(True) + self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( + True + ) + + def selectDatabasePath(self): + databasePath = QtWidgets.QFileDialog.getExistingDirectory( + self, "Select Database Path", self.originalSettings.database.path + ) + self.database_path.setText(databasePath) + self.buttonBox.button( + QtWidgets.QDialogButtonBox.StandardButton.Discard + ).setEnabled(True) + self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( + True + ) + + def selectDatabaseName(self): + # filepicker with filter to select only .db files if a file is selected, set name to the lineedit and set database_path + databaseName = QtWidgets.QFileDialog.getOpenFileName( + self, + "Select Database", + self.originalSettings.database.path, + "Database Files (*.db)", + ) + self.database_name.setText(databaseName[0]) + self.database_path.setText(databaseName[0].rsplit("/", 1)[0]) + self.buttonBox.button( + QtWidgets.QDialogButtonBox.StandardButton.Discard + ).setEnabled(True) + self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( + True + ) + + def saveSettings(self): + # save settings to config file + institution_name = self.institution_name.text() + default_loan_duration = int(self.default_loan_duration.text()) + database_backupLocation = self.database_backupLocation.text() + database_path = self.database_path.text() + database_name = self.database_name.text() + # overwrite the original settings + self.originalSettings.institution_name = institution_name + self.originalSettings.default_loan_duration = default_loan_duration + self.originalSettings.database.backupLocation = database_backupLocation + self.originalSettings.database.path = database_path + self.originalSettings.database.name = database_name + # save the new settings + OmegaConf.save(self.originalSettings, "config/settings.yaml") + + self.close() + + def DiscardSettings(self): + self.loadSettings() + pass + + def loadSettings(self): + self.institution_name.setText(self.originalSettings.institution_name) + self.default_loan_duration.setText( + str(self.originalSettings.default_loan_duration) + ) + self.database_backupLocation.setText( + self.originalSettings.database.backupLocation + ) + self.database_path.setText(self.originalSettings.database.path) + self.database_name.setText(self.originalSettings.database.name) + pass + + +def launch(): + import sys + + app = QtWidgets.QApplication([]) + settings = Settings() + settings.show() + sys.exit(app.exec()) diff --git a/src/ui/sources/Ui_dialog_settings.py b/src/ui/sources/Ui_dialog_settings.py new file mode 100644 index 0000000..c2745db --- /dev/null +++ b/src/ui/sources/Ui_dialog_settings.py @@ -0,0 +1,92 @@ +# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_settings.ui' +# +# Created by: PyQt6 UI code generator 6.6.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(377, 206) + self.formLayout = QtWidgets.QFormLayout(Dialog) + self.formLayout.setObjectName("formLayout") + self.label = QtWidgets.QLabel(parent=Dialog) + self.label.setObjectName("label") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label) + self.institution_name = QtWidgets.QLineEdit(parent=Dialog) + self.institution_name.setObjectName("institution_name") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.institution_name) + self.label_2 = QtWidgets.QLabel(parent=Dialog) + self.label_2.setObjectName("label_2") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2) + self.default_loan_duration = QtWidgets.QLineEdit(parent=Dialog) + self.default_loan_duration.setObjectName("default_loan_duration") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.default_loan_duration) + self.label_3 = QtWidgets.QLabel(parent=Dialog) + self.label_3.setObjectName("label_3") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_3) + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.database_name = QtWidgets.QLineEdit(parent=Dialog) + self.database_name.setObjectName("database_name") + self.gridLayout.addWidget(self.database_name, 1, 1, 1, 1) + self.label_4 = QtWidgets.QLabel(parent=Dialog) + self.label_4.setObjectName("label_4") + self.gridLayout.addWidget(self.label_4, 0, 0, 1, 1) + self.label_6 = QtWidgets.QLabel(parent=Dialog) + self.label_6.setObjectName("label_6") + self.gridLayout.addWidget(self.label_6, 2, 0, 1, 1) + self.database_path = QtWidgets.QLineEdit(parent=Dialog) + self.database_path.setObjectName("database_path") + self.gridLayout.addWidget(self.database_path, 0, 1, 1, 1) + self.database_backupLocation = QtWidgets.QLineEdit(parent=Dialog) + self.database_backupLocation.setObjectName("database_backupLocation") + self.gridLayout.addWidget(self.database_backupLocation, 2, 1, 1, 1) + self.label_5 = QtWidgets.QLabel(parent=Dialog) + self.label_5.setObjectName("label_5") + self.gridLayout.addWidget(self.label_5, 1, 0, 1, 1) + self.btn_select_database_path = QtWidgets.QToolButton(parent=Dialog) + self.btn_select_database_path.setObjectName("btn_select_database_path") + self.gridLayout.addWidget(self.btn_select_database_path, 0, 2, 1, 1) + self.btn_select_database_name = QtWidgets.QToolButton(parent=Dialog) + self.btn_select_database_name.setObjectName("btn_select_database_name") + self.gridLayout.addWidget(self.btn_select_database_name, 1, 2, 1, 1) + self.btn_select_database_backupLocation = QtWidgets.QToolButton(parent=Dialog) + self.btn_select_database_backupLocation.setObjectName("btn_select_database_backupLocation") + self.gridLayout.addWidget(self.btn_select_database_backupLocation, 2, 2, 1, 1) + self.formLayout.setLayout(2, QtWidgets.QFormLayout.ItemRole.FieldRole, self.gridLayout) + self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) + self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Discard|QtWidgets.QDialogButtonBox.StandardButton.Ok) + self.buttonBox.setObjectName("buttonBox") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.ItemRole.FieldRole, self.buttonBox) + + self.retranslateUi(Dialog) + self.buttonBox.accepted.connect(Dialog.accept) # type: ignore + self.buttonBox.rejected.connect(Dialog.reject) # type: ignore + QtCore.QMetaObject.connectSlotsByName(Dialog) + Dialog.setTabOrder(self.institution_name, self.default_loan_duration) + Dialog.setTabOrder(self.default_loan_duration, self.database_path) + Dialog.setTabOrder(self.database_path, self.database_name) + Dialog.setTabOrder(self.database_name, self.database_backupLocation) + Dialog.setTabOrder(self.database_backupLocation, self.btn_select_database_path) + Dialog.setTabOrder(self.btn_select_database_path, self.btn_select_database_name) + Dialog.setTabOrder(self.btn_select_database_name, self.btn_select_database_backupLocation) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Dialog")) + self.label.setText(_translate("Dialog", "Name der Einrichtung")) + self.label_2.setText(_translate("Dialog", "Leihdauer in Tagen")) + self.label_3.setText(_translate("Dialog", "Datenbank")) + self.label_4.setText(_translate("Dialog", "Speicherort")) + self.label_6.setText(_translate("Dialog", "Sicherungspfad")) + self.label_5.setText(_translate("Dialog", "Datenbankname")) + self.btn_select_database_path.setText(_translate("Dialog", "...")) + self.btn_select_database_name.setText(_translate("Dialog", "...")) + self.btn_select_database_backupLocation.setText(_translate("Dialog", "...")) diff --git a/src/ui/sources/dialog_settings.ui b/src/ui/sources/dialog_settings.ui new file mode 100644 index 0000000..cf70450 --- /dev/null +++ b/src/ui/sources/dialog_settings.ui @@ -0,0 +1,156 @@ + + + Dialog + + + + 0 + 0 + 377 + 206 + + + + Dialog + + + + + + Name der Einrichtung + + + + + + + + + + Leihdauer in Tagen + + + + + + + + + + Datenbank + + + + + + + + + + + + Speicherort + + + + + + + Sicherungspfad + + + + + + + + + + + + + Datenbankname + + + + + + + ... + + + + + + + ... + + + + + + + ... + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Discard|QDialogButtonBox::Ok + + + + + + + institution_name + default_loan_duration + database_path + database_name + database_backupLocation + btn_select_database_path + btn_select_database_name + btn_select_database_backupLocation + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/ui/user.py b/src/ui/user.py index 2bdca97..e7aafbc 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -2,6 +2,8 @@ from .sources.Ui_main_userData import Ui_MainWindow from PyQt6 import QtCore, QtGui, QtWidgets from src.logic import Database from src.schemas import User +from .extendLoan import ExtendLoan +from src.utils.stringtodate import stringToDate class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self, u_name, u_no, u_mail): super(UserUI, self).__init__() @@ -11,12 +13,27 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): self.userno = u_no self.usermail = u_mail self.setFields() + self.userMedia = [] + self.loadMedia() # Buttons self.btn_userChange_save.clicked.connect(self.saveChanges) self.btn_userchange_cancel.clicked.connect(self.discardChanges) + self.btn_extendSelectedMedia.clicked.connect(self.extendLoan) + + # radioButtons + self.radio_allLoanedMedia.clicked.connect(self.loadMedia) + self.radio_currentlyLoaned.clicked.connect(self.loadMedia) + self.radio_overdueLoans.clicked.connect(self.loadMedia) # frames self.frame.hide() + + # table + self.UserMediaTable.horizontalHeader().setSectionResizeMode( + QtWidgets.QHeaderView.ResizeMode.Stretch + ) + # if one or more rows is selected, enable btn + self.UserMediaTable.itemSelectionChanged.connect(self.userTableAction) # LineEdits # self.frame.hide() @@ -26,6 +43,32 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): self.show() + def extendLoan(self): + extend = ExtendLoan(self.username, self.userMedia) + extend.exec() + extendDate = extend.extendDate.toString() + # print columns of selected rows + for item in self.UserMediaTable.selectedItems(): + if item.column() == 1: + signature = item.text() + print(signature) + self.db.extendLoanDuration(signature, extendDate) + self.userMedia = [] + break + self.UserMediaTable.setRowCount(0) + self.loadMedia() + + def userTableAction(self): + if self.UserMediaTable.selectedItems(): + # if any selected item has a value in column 5, disable btn + for item in self.UserMediaTable.selectedItems(): + if item.column() == 5 and item.text() != "": + self.btn_extendSelectedMedia.setEnabled(False) + return + self.btn_extendSelectedMedia.setEnabled(True) + else: + self.btn_extendSelectedMedia.setEnabled(False) + def showFrame(self): self.frame.show() @@ -39,4 +82,68 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): def discardChanges(self): self.setFields() - self.frame.hide() \ No newline at end of file + self.frame.hide() + + def loadMedia(self): + mode = ( + "all" + if self.radio_allLoanedMedia.isChecked() + else "current" + if self.radio_currentlyLoaned.isChecked() + else "overdue" + ) + print(mode) + if self.userMedia == []: + books = self.db.getAllMedia(self.userno) + for book in books: + self.userMedia.append(book) + print(self.userMedia) + self.UserMediaTable.setRowCount(0) + + for book in self.userMedia: + # fromdate = stringToDate(book.loan_from) + todate = stringToDate(book.loan_to) + if mode == "current": + # book not returned + if book.returned == 0: + self.addBookToTable(book) + elif mode == "overdue": + # book not returned and todays date is greater than todate + if ( + book.returned == 0 + and QtCore.QDate.fromString(todate) > QtCore.QDate.currentDate() + ): + self.addBookToTable(book) + else: + self.addBookToTable(book) + + def addBookToTable(self, book): + self.UserMediaTable.insertRow(0) + # item0 = isbn + # item1 = signature + # item2 = title + # item3 = loan date + # item4 = return date + # item5 = returned_date + self.UserMediaTable.setItem( + 0, + 0, + QtWidgets.QTableWidgetItem(book.isbn if book.isbn != "None" else ""), + ) + self.UserMediaTable.setItem(0, 1, QtWidgets.QTableWidgetItem(book.signature)) + self.UserMediaTable.setItem(0, 2, QtWidgets.QTableWidgetItem(book.title)) + self.UserMediaTable.setItem(0, 3, QtWidgets.QTableWidgetItem(book.loan_from)) + self.UserMediaTable.setItem(0, 4, QtWidgets.QTableWidgetItem(book.loan_to)) + self.UserMediaTable.setItem( + 0, 5, QtWidgets.QTableWidgetItem(book.returned_date) + ) + + +def launch(): + import sys + + app = QtWidgets.QApplication(sys.argv) + + window = UserUI("Test", "132", "sdf@f.de") + window.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..afc91ca --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,2 @@ +from .stringtodate import stringToDate +from .icon import Icon From 7ef877850073c84eb6a770362fb933775e28c955 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:17:40 +0200 Subject: [PATCH 08/83] add reqs --- requirements.txt | Bin 0 -> 1834 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..ec2e0366ab220146842e30f3d53b1b3cbe62cb95 GIT binary patch literal 1834 zcmaJ?O;6iU5ZrU6{uHCw2}wC{=z&`+wd$!S8McAXd5&3Jx$)MQ*-=6TUgU6f4r{5ADd6@Qr}VVtT0%!<&Fm}!9T;9Ej{TGVm$Tw)mYAG`uiD0i#G?8TVm zBTdw0ZeQixG#}%Mc@gH|8xW1x$6lmu%k2|lso#vknz)&qDrQq8P9b;qJzVD7&wZXy znG~Gi--5SvO!F?CUQdjZlBz6DFIj&Fo3%$E3r?qIC)u(K80nHQ_h3cM$t3q~q`Hav z0qYL+{+}sDN%njD4CKxyL^?)Zm(h2~%$=p~t?PQpYXFBM>^-;@t{=NTz)xsSw=NU4 zBjcao@zuVZlu0|u{rH8Qr0;pJ898o%t_W`iuacYU4pn#y05$&oHJ%vdIZ%N`+vk7QI#E7rvE~^AKK)j^Q-X5eJ` Date: Mon, 29 Jul 2024 10:18:27 +0200 Subject: [PATCH 09/83] update settings --- config/settings.yaml | 13 ++++++++++--- icons/add_book.svg | 4 ++++ icons/user.svg | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 icons/add_book.svg create mode 100644 icons/user.svg diff --git a/config/settings.yaml b/config/settings.yaml index a96ce4b..63a192e 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -1,7 +1,14 @@ institution_name: HB Testbibliothek Psychologie default_loan_duration: 7 database: - path: C:/db - name: library.db + path: C:/newestdb_mew + name: libraries.db backupLocation: V:/backup -library_id: 20735 + do_backup: false +report: + generate_report: false + email: None + +debug: false +log_debug: false +catalogue: True diff --git a/icons/add_book.svg b/icons/add_book.svg new file mode 100644 index 0000000..acd98b6 --- /dev/null +++ b/icons/add_book.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/user.svg b/icons/user.svg new file mode 100644 index 0000000..9b6e823 --- /dev/null +++ b/icons/user.svg @@ -0,0 +1 @@ + \ No newline at end of file From fae59d5cf7b4e0fef11a500cb7de8dc9ae28dfef Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:20:02 +0200 Subject: [PATCH 10/83] add icons --- icons/add_user.svg | 1 + icons/backup.svg | 1 + icons/book.svg | 1 + icons/db_backup.svg | 4 ++++ icons/group.svg | 1 + icons/icons.yaml | 9 ++++++++- icons/multiple_user.svg | 1 + icons/report.svg | 1 + 8 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 icons/add_user.svg create mode 100644 icons/backup.svg create mode 100644 icons/book.svg create mode 100644 icons/db_backup.svg create mode 100644 icons/group.svg create mode 100644 icons/multiple_user.svg create mode 100644 icons/report.svg diff --git a/icons/add_user.svg b/icons/add_user.svg new file mode 100644 index 0000000..3904edf --- /dev/null +++ b/icons/add_user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/backup.svg b/icons/backup.svg new file mode 100644 index 0000000..f65b40b --- /dev/null +++ b/icons/backup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/book.svg b/icons/book.svg new file mode 100644 index 0000000..237f73c --- /dev/null +++ b/icons/book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/db_backup.svg b/icons/db_backup.svg new file mode 100644 index 0000000..93d9936 --- /dev/null +++ b/icons/db_backup.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/group.svg b/icons/group.svg new file mode 100644 index 0000000..6eae0ab --- /dev/null +++ b/icons/group.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/icons.yaml b/icons/icons.yaml index 1494936..68e6968 100644 --- a/icons/icons.yaml +++ b/icons/icons.yaml @@ -3,4 +3,11 @@ icons: newentry: library_add.svg main: library.svg warning: warning.svg - settings: settings.svg \ No newline at end of file + settings: settings.svg + user: user.svg + multiuser: multiple_user.svg + add_user: add_user.svg + borrow: book.svg + backup: db_backup.svg + addBook: add_book.svg + report: report.svg \ No newline at end of file diff --git a/icons/multiple_user.svg b/icons/multiple_user.svg new file mode 100644 index 0000000..aef3514 --- /dev/null +++ b/icons/multiple_user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/report.svg b/icons/report.svg new file mode 100644 index 0000000..e29e721 --- /dev/null +++ b/icons/report.svg @@ -0,0 +1 @@ + \ No newline at end of file From 634755452bc54d611d27ddfaf1e058e35d60a4b2 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:22:25 +0200 Subject: [PATCH 11/83] update ui files and generated files --- src/ui/sources/Ui_dialog_addBook.py | 54 ++++++ src/ui/sources/Ui_dialog_addBook.ui.py | 8 + src/ui/sources/Ui_dialog_createUser.py | 3 + src/ui/sources/Ui_dialog_generateReport.py | 99 ++++++++++ src/ui/sources/Ui_dialog_generateReport.ui.py | 8 + src/ui/sources/Ui_dialog_settings.py | 2 +- src/ui/sources/Ui_main_Loans.py | 100 ++++++++++ src/ui/sources/Ui_main_UserInterface.py | 81 +++++--- src/ui/sources/Ui_main_userData.py | 14 +- src/ui/sources/dialog_addBook.ui | 71 +++++++ src/ui/sources/dialog_createUser.ui | 15 +- src/ui/sources/dialog_generateReport.ui | 173 ++++++++++++++++++ src/ui/sources/dialog_settings.ui | 4 +- src/ui/sources/main_Loans.ui | 148 +++++++++++++++ src/ui/sources/main_UserInterface.ui | 114 ++++++++---- src/ui/sources/main_userData.ui | 20 +- 16 files changed, 840 insertions(+), 74 deletions(-) create mode 100644 src/ui/sources/Ui_dialog_addBook.py create mode 100644 src/ui/sources/Ui_dialog_addBook.ui.py create mode 100644 src/ui/sources/Ui_dialog_generateReport.py create mode 100644 src/ui/sources/Ui_dialog_generateReport.ui.py create mode 100644 src/ui/sources/Ui_main_Loans.py create mode 100644 src/ui/sources/dialog_addBook.ui create mode 100644 src/ui/sources/dialog_generateReport.ui create mode 100644 src/ui/sources/main_Loans.ui diff --git a/src/ui/sources/Ui_dialog_addBook.py b/src/ui/sources/Ui_dialog_addBook.py new file mode 100644 index 0000000..01b4004 --- /dev/null +++ b/src/ui/sources/Ui_dialog_addBook.py @@ -0,0 +1,54 @@ +# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_addBook.ui' +# +# Created by: PyQt6 UI code generator 6.6.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(262, 100) + self.gridLayout = QtWidgets.QGridLayout(Dialog) + self.gridLayout.setObjectName("gridLayout") + self.book_signature = QtWidgets.QLineEdit(parent=Dialog) + self.book_signature.setObjectName("book_signature") + self.gridLayout.addWidget(self.book_signature, 1, 1, 1, 1) + self.label_2 = QtWidgets.QLabel(parent=Dialog) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1) + self.label = QtWidgets.QLabel(parent=Dialog) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 1) + self.book_title = QtWidgets.QLineEdit(parent=Dialog) + self.book_title.setObjectName("book_title") + self.gridLayout.addWidget(self.book_title, 0, 1, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.btn_save = QtWidgets.QPushButton(parent=Dialog) + self.btn_save.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) + self.btn_save.setObjectName("btn_save") + self.horizontalLayout.addWidget(self.btn_save) + self.btn_cancel = QtWidgets.QPushButton(parent=Dialog) + self.btn_cancel.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) + self.btn_cancel.setObjectName("btn_cancel") + self.horizontalLayout.addWidget(self.btn_cancel) + self.gridLayout.addLayout(self.horizontalLayout, 2, 1, 1, 1) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + Dialog.setTabOrder(self.book_title, self.book_signature) + Dialog.setTabOrder(self.book_signature, self.btn_save) + Dialog.setTabOrder(self.btn_save, self.btn_cancel) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Dialog")) + self.label_2.setText(_translate("Dialog", "Signatur")) + self.label.setText(_translate("Dialog", "Titel")) + self.btn_save.setText(_translate("Dialog", "Speichern")) + self.btn_cancel.setText(_translate("Dialog", "Abbrechen")) diff --git a/src/ui/sources/Ui_dialog_addBook.ui.py b/src/ui/sources/Ui_dialog_addBook.ui.py new file mode 100644 index 0000000..b972dcd --- /dev/null +++ b/src/ui/sources/Ui_dialog_addBook.ui.py @@ -0,0 +1,8 @@ +# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_addBook.ui.VLqeTH' +# +# Created by: PyQt6 UI code generator 6.6.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + diff --git a/src/ui/sources/Ui_dialog_createUser.py b/src/ui/sources/Ui_dialog_createUser.py index 039367a..adb164c 100644 --- a/src/ui/sources/Ui_dialog_createUser.py +++ b/src/ui/sources/Ui_dialog_createUser.py @@ -53,6 +53,9 @@ class Ui_Dialog(object): def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "Nutzer anlegen")) + self.userno.setPlaceholderText(_translate("Dialog", "102888557")) self.label_2.setText(_translate("Dialog", "Matrikelnummer")) self.label_3.setText(_translate("Dialog", "Mail")) + self.user_mail.setPlaceholderText(_translate("Dialog", "email@ph-freiburg.de")) self.label.setText(_translate("Dialog", "Name, Vorname")) + self.username.setPlaceholderText(_translate("Dialog", "Nachname, Vorname")) diff --git a/src/ui/sources/Ui_dialog_generateReport.py b/src/ui/sources/Ui_dialog_generateReport.py new file mode 100644 index 0000000..258020d --- /dev/null +++ b/src/ui/sources/Ui_dialog_generateReport.py @@ -0,0 +1,99 @@ +# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_generateReport.ui' +# +# Created by: PyQt6 UI code generator 6.6.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(375, 206) + Dialog.setMinimumSize(QtCore.QSize(40, 0)) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.label = QtWidgets.QLabel(parent=Dialog) + self.label.setObjectName("label") + self.verticalLayout.addWidget(self.label) + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.label_2 = QtWidgets.QLabel(parent=Dialog) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 0, 0, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.radio_week = QtWidgets.QRadioButton(parent=Dialog) + self.radio_week.setObjectName("radio_week") + self.horizontalLayout.addWidget(self.radio_week) + self.radio_month = QtWidgets.QRadioButton(parent=Dialog) + self.radio_month.setObjectName("radio_month") + self.horizontalLayout.addWidget(self.radio_month) + self.radio_year = QtWidgets.QRadioButton(parent=Dialog) + self.radio_year.setObjectName("radio_year") + self.horizontalLayout.addWidget(self.radio_year) + self.gridLayout.addLayout(self.horizontalLayout, 1, 1, 1, 1) + self.reportlink = QtWidgets.QLabel(parent=Dialog) + self.reportlink.setText("") + self.reportlink.setObjectName("reportlink") + self.gridLayout.addWidget(self.reportlink, 2, 1, 1, 1) + self.dayslider = QtWidgets.QSlider(parent=Dialog) + self.dayslider.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) + self.dayslider.setMinimum(1) + self.dayslider.setMaximum(365) + self.dayslider.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.dayslider.setInvertedControls(True) + self.dayslider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksAbove) + self.dayslider.setTickInterval(10) + self.dayslider.setObjectName("dayslider") + self.gridLayout.addWidget(self.dayslider, 0, 1, 1, 1) + self.dayValue = QtWidgets.QLineEdit(parent=Dialog) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.dayValue.sizePolicy().hasHeightForWidth()) + self.dayValue.setSizePolicy(sizePolicy) + self.dayValue.setMinimumSize(QtCore.QSize(0, 0)) + self.dayValue.setMaximumSize(QtCore.QSize(40, 16777215)) + self.dayValue.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.dayValue.setReadOnly(True) + self.dayValue.setObjectName("dayValue") + self.gridLayout.addWidget(self.dayValue, 0, 2, 1, 1) + self.radioButton = QtWidgets.QRadioButton(parent=Dialog) + self.radioButton.setText("") + self.radioButton.setCheckable(True) + self.radioButton.setObjectName("radioButton") + self.gridLayout.addWidget(self.radioButton, 2, 2, 1, 1) + self.verticalLayout.addLayout(self.gridLayout) + self.label_4 = QtWidgets.QLabel(parent=Dialog) + self.label_4.setObjectName("label_4") + self.verticalLayout.addWidget(self.label_4) + self.reportprogress = QtWidgets.QProgressBar(parent=Dialog) + self.reportprogress.setProperty("value", 24) + self.reportprogress.setTextVisible(True) + self.reportprogress.setInvertedAppearance(False) + self.reportprogress.setObjectName("reportprogress") + self.verticalLayout.addWidget(self.reportprogress) + self.generateReport = QtWidgets.QPushButton(parent=Dialog) + self.generateReport.setObjectName("generateReport") + self.verticalLayout.addWidget(self.generateReport) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + Dialog.setTabOrder(self.radio_week, self.radio_month) + Dialog.setTabOrder(self.radio_month, self.radio_year) + Dialog.setTabOrder(self.radio_year, self.generateReport) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Dialog")) + self.label.setText(_translate("Dialog", "Wieviele Tage sollen im Bericht erfasst werden?")) + self.label_2.setText(_translate("Dialog", "Tage")) + self.radio_week.setText(_translate("Dialog", "Woche")) + self.radio_month.setText(_translate("Dialog", "Monat")) + self.radio_year.setText(_translate("Dialog", "Jahr")) + self.label_4.setText(_translate("Dialog", "Fortschritt:")) + self.generateReport.setText(_translate("Dialog", " Bericht erstellen")) diff --git a/src/ui/sources/Ui_dialog_generateReport.ui.py b/src/ui/sources/Ui_dialog_generateReport.ui.py new file mode 100644 index 0000000..940cd4a --- /dev/null +++ b/src/ui/sources/Ui_dialog_generateReport.ui.py @@ -0,0 +1,8 @@ +# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_generateReport.ui.nKmkjJ' +# +# Created by: PyQt6 UI code generator 6.6.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + diff --git a/src/ui/sources/Ui_dialog_settings.py b/src/ui/sources/Ui_dialog_settings.py index c2745db..aae6c32 100644 --- a/src/ui/sources/Ui_dialog_settings.py +++ b/src/ui/sources/Ui_dialog_settings.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(377, 206) + Dialog.resize(436, 184) self.formLayout = QtWidgets.QFormLayout(Dialog) self.formLayout.setObjectName("formLayout") self.label = QtWidgets.QLabel(parent=Dialog) diff --git a/src/ui/sources/Ui_main_Loans.py b/src/ui/sources/Ui_main_Loans.py new file mode 100644 index 0000000..5161cfe --- /dev/null +++ b/src/ui/sources/Ui_main_Loans.py @@ -0,0 +1,100 @@ +# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\main_Loans.ui' +# +# Created by: PyQt6 UI code generator 6.6.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(899, 658) + self.centralwidget = QtWidgets.QWidget(parent=MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) + self.verticalLayout.setObjectName("verticalLayout") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.radio_all = QtWidgets.QRadioButton(parent=self.centralwidget) + self.radio_all.setChecked(True) + self.radio_all.setObjectName("radio_all") + self.horizontalLayout.addWidget(self.radio_all) + self.radio_current = QtWidgets.QRadioButton(parent=self.centralwidget) + self.radio_current.setObjectName("radio_current") + self.horizontalLayout.addWidget(self.radio_current) + self.radio_overdue = QtWidgets.QRadioButton(parent=self.centralwidget) + self.radio_overdue.setObjectName("radio_overdue") + self.horizontalLayout.addWidget(self.radio_overdue) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.verticalLayout.addLayout(self.horizontalLayout) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.searchbar = QtWidgets.QLineEdit(parent=self.centralwidget) + self.searchbar.setObjectName("searchbar") + self.horizontalLayout_2.addWidget(self.searchbar) + self.searchFields = QtWidgets.QComboBox(parent=self.centralwidget) + self.searchFields.setObjectName("searchFields") + self.searchFields.addItem("") + self.searchFields.addItem("") + self.searchFields.addItem("") + self.horizontalLayout_2.addWidget(self.searchFields) + self.verticalLayout.addLayout(self.horizontalLayout_2) + self.loanTable = QtWidgets.QTableWidget(parent=self.centralwidget) + self.loanTable.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + self.loanTable.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) + self.loanTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) + self.loanTable.setObjectName("loanTable") + self.loanTable.setColumnCount(7) + self.loanTable.setRowCount(0) + item = QtWidgets.QTableWidgetItem() + self.loanTable.setHorizontalHeaderItem(0, item) + item = QtWidgets.QTableWidgetItem() + self.loanTable.setHorizontalHeaderItem(1, item) + item = QtWidgets.QTableWidgetItem() + self.loanTable.setHorizontalHeaderItem(2, item) + item = QtWidgets.QTableWidgetItem() + self.loanTable.setHorizontalHeaderItem(3, item) + item = QtWidgets.QTableWidgetItem() + self.loanTable.setHorizontalHeaderItem(4, item) + item = QtWidgets.QTableWidgetItem() + self.loanTable.setHorizontalHeaderItem(5, item) + item = QtWidgets.QTableWidgetItem() + self.loanTable.setHorizontalHeaderItem(6, item) + self.verticalLayout.addWidget(self.loanTable) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(parent=MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 899, 22)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.radio_all.setText(_translate("MainWindow", "Alle Ausleihen")) + self.radio_current.setText(_translate("MainWindow", "Aktuell Entliehene Medien")) + self.radio_overdue.setText(_translate("MainWindow", "Überzgene Medien")) + self.searchFields.setItemText(0, _translate("MainWindow", "Titel")) + self.searchFields.setItemText(1, _translate("MainWindow", "Signatur")) + self.searchFields.setItemText(2, _translate("MainWindow", "Nutzer")) + item = self.loanTable.horizontalHeaderItem(0) + item.setText(_translate("MainWindow", "ISBN")) + item = self.loanTable.horizontalHeaderItem(1) + item.setText(_translate("MainWindow", "Signatur")) + item = self.loanTable.horizontalHeaderItem(2) + item.setText(_translate("MainWindow", "Titel")) + item = self.loanTable.horizontalHeaderItem(3) + item.setText(_translate("MainWindow", "Nutzerkonto")) + item = self.loanTable.horizontalHeaderItem(4) + item.setText(_translate("MainWindow", "entliehen am")) + item = self.loanTable.horizontalHeaderItem(5) + item.setText(_translate("MainWindow", "entliehen bis")) + item = self.loanTable.horizontalHeaderItem(6) + item.setText(_translate("MainWindow", "Zurückgegeben am")) diff --git a/src/ui/sources/Ui_main_UserInterface.py b/src/ui/sources/Ui_main_UserInterface.py index 999aee7..b1374ba 100644 --- a/src/ui/sources/Ui_main_UserInterface.py +++ b/src/ui/sources/Ui_main_UserInterface.py @@ -19,15 +19,13 @@ class Ui_MainWindow(object): self.verticalLayout.setObjectName("verticalLayout") self.gridLayout = QtWidgets.QGridLayout() self.gridLayout.setObjectName("gridLayout") - self.label_2 = QtWidgets.QLabel(parent=self.centralwidget) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) - self.input_userno = QtWidgets.QLineEdit(parent=self.centralwidget) - self.input_userno.setObjectName("input_userno") - self.gridLayout.addWidget(self.input_userno, 1, 1, 1, 1) - self.duedate = QtWidgets.QDateEdit(parent=self.centralwidget) - self.duedate.setObjectName("duedate") - self.gridLayout.addWidget(self.duedate, 5, 1, 1, 1) + self.label_3 = QtWidgets.QLabel(parent=self.centralwidget) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1) + self.label_6 = QtWidgets.QLabel(parent=self.centralwidget) + self.label_6.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.label_6.setObjectName("label_6") + self.gridLayout.addWidget(self.label_6, 5, 0, 1, 1) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.label_5 = QtWidgets.QLabel(parent=self.centralwidget) @@ -43,22 +41,32 @@ class Ui_MainWindow(object): self.mode.setObjectName("mode") self.horizontalLayout.addWidget(self.mode) self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) - self.label_3 = QtWidgets.QLabel(parent=self.centralwidget) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1) - self.label_6 = QtWidgets.QLabel(parent=self.centralwidget) - self.label_6.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignVCenter) - self.label_6.setObjectName("label_6") - self.gridLayout.addWidget(self.label_6, 5, 0, 1, 1) self.label = QtWidgets.QLabel(parent=self.centralwidget) self.label.setObjectName("label") self.gridLayout.addWidget(self.label, 1, 0, 1, 1) self.input_username = QtWidgets.QLineEdit(parent=self.centralwidget) self.input_username.setObjectName("input_username") self.gridLayout.addWidget(self.input_username, 2, 1, 1, 1) + self.duedate = QtWidgets.QDateEdit(parent=self.centralwidget) + self.duedate.setObjectName("duedate") + self.gridLayout.addWidget(self.duedate, 5, 1, 1, 1) self.input_file_ident = QtWidgets.QLineEdit(parent=self.centralwidget) self.input_file_ident.setObjectName("input_file_ident") self.gridLayout.addWidget(self.input_file_ident, 3, 1, 1, 1) + self.label_2 = QtWidgets.QLabel(parent=self.centralwidget) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) + self.input_userno = QtWidgets.QLineEdit(parent=self.centralwidget) + self.input_userno.setObjectName("input_userno") + self.gridLayout.addWidget(self.input_userno, 1, 1, 1, 1) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_3.addItem(spacerItem) + self.btn_createNewUser = QtWidgets.QPushButton(parent=self.centralwidget) + self.btn_createNewUser.setObjectName("btn_createNewUser") + self.horizontalLayout_3.addWidget(self.btn_createNewUser) + self.gridLayout.addLayout(self.horizontalLayout_3, 0, 1, 1, 1) self.verticalLayout.addLayout(self.gridLayout) self.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget) self.groupBox.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) @@ -119,8 +127,10 @@ class Ui_MainWindow(object): self.menubar.setObjectName("menubar") self.menuDatei = QtWidgets.QMenu(parent=self.menubar) self.menuDatei.setObjectName("menuDatei") - self.menuShortkeys = QtWidgets.QMenu(parent=self.menubar) - self.menuShortkeys.setObjectName("menuShortkeys") + self.menuHotkeys = QtWidgets.QMenu(parent=self.menubar) + self.menuHotkeys.setObjectName("menuHotkeys") + self.menuFenster = QtWidgets.QMenu(parent=self.menubar) + self.menuFenster.setObjectName("menuFenster") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(parent=MainWindow) self.statusbar.setObjectName("statusbar") @@ -133,31 +143,40 @@ class Ui_MainWindow(object): self.actionRueckgabemodus.setObjectName("actionRueckgabemodus") self.actionNutzer = QtGui.QAction(parent=MainWindow) self.actionNutzer.setObjectName("actionNutzer") + self.actionNutzer_2 = QtGui.QAction(parent=MainWindow) + self.actionNutzer_2.setObjectName("actionNutzer_2") + self.actionAusleihistorie = QtGui.QAction(parent=MainWindow) + self.actionAusleihistorie.setObjectName("actionAusleihistorie") + self.actionBericht_erstellen = QtGui.QAction(parent=MainWindow) + self.actionBericht_erstellen.setObjectName("actionBericht_erstellen") self.menuDatei.addAction(self.actionEinstellungen) self.menuDatei.addAction(self.actionBeenden) - self.menuShortkeys.addAction(self.actionRueckgabemodus) - self.menuShortkeys.addAction(self.actionNutzer) + self.menuHotkeys.addAction(self.actionRueckgabemodus) + self.menuHotkeys.addAction(self.actionNutzer) + self.menuFenster.addAction(self.actionAusleihistorie) + self.menuFenster.addAction(self.actionBericht_erstellen) self.menubar.addAction(self.menuDatei.menuAction()) - self.menubar.addAction(self.menuShortkeys.menuAction()) + self.menubar.addAction(self.menuHotkeys.menuAction()) + self.menubar.addAction(self.menuFenster.menuAction()) self.retranslateUi(MainWindow) self.actionBeenden.triggered.connect(MainWindow.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(MainWindow) + MainWindow.setTabOrder(self.btn_createNewUser, self.input_userno) MainWindow.setTabOrder(self.input_userno, self.input_username) MainWindow.setTabOrder(self.input_username, self.input_file_ident) - MainWindow.setTabOrder(self.input_file_ident, self.groupBox) - MainWindow.setTabOrder(self.groupBox, self.mediaOverview) - MainWindow.setTabOrder(self.mediaOverview, self.btn_show_lentmedia) + MainWindow.setTabOrder(self.input_file_ident, self.duedate) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) - self.label_2.setText(_translate("MainWindow", "Benutzername")) - self.label_5.setText(_translate("MainWindow", "Modus")) - self.mode.setText(_translate("MainWindow", "Rückgabe")) self.label_3.setText(_translate("MainWindow", "Suchbegriff")) self.label_6.setText(_translate("MainWindow", "Ausleihe bis")) + self.label_5.setText(_translate("MainWindow", "Modus")) + self.mode.setText(_translate("MainWindow", "Rückgabe")) self.label.setText(_translate("MainWindow", "Matrikelnummer")) + self.label_2.setText(_translate("MainWindow", "Benutzername")) + self.btn_createNewUser.setText(_translate("MainWindow", "Neuen Nutzer anlegen")) self.groupBox.setTitle(_translate("MainWindow", "Nutzerdaten")) self.groupBox_2.setTitle(_translate("MainWindow", "Ausleihdaten")) self.label_4.setText(_translate("MainWindow", "Anzahl Ausleihen")) @@ -169,10 +188,16 @@ class Ui_MainWindow(object): item = self.mediaOverview.horizontalHeaderItem(2) item.setText(_translate("MainWindow", "Status")) self.menuDatei.setTitle(_translate("MainWindow", "Datei")) - self.menuShortkeys.setTitle(_translate("MainWindow", "Shortkeys")) + self.menuHotkeys.setTitle(_translate("MainWindow", "Hotkeys")) + self.menuFenster.setTitle(_translate("MainWindow", "Fenster")) self.actionEinstellungen.setText(_translate("MainWindow", "Einstellungen")) self.actionBeenden.setText(_translate("MainWindow", "Beenden")) self.actionRueckgabemodus.setText(_translate("MainWindow", "Rückgabemodus")) self.actionRueckgabemodus.setShortcut(_translate("MainWindow", "F5")) self.actionNutzer.setText(_translate("MainWindow", "Nutzer")) self.actionNutzer.setShortcut(_translate("MainWindow", "F6")) + self.actionNutzer_2.setText(_translate("MainWindow", "Nutzer")) + self.actionAusleihistorie.setText(_translate("MainWindow", "Ausleihhistorie")) + self.actionAusleihistorie.setShortcut(_translate("MainWindow", "F8")) + self.actionBericht_erstellen.setText(_translate("MainWindow", "Bericht erstellen")) + self.actionBericht_erstellen.setShortcut(_translate("MainWindow", "F7")) diff --git a/src/ui/sources/Ui_main_userData.py b/src/ui/sources/Ui_main_userData.py index 5b6d55d..d517853 100644 --- a/src/ui/sources/Ui_main_userData.py +++ b/src/ui/sources/Ui_main_userData.py @@ -71,10 +71,11 @@ class Ui_MainWindow(object): self.horizontalLayout_2 = QtWidgets.QHBoxLayout() self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.radio_allLoanedMedia = QtWidgets.QRadioButton(parent=self.centralwidget) - self.radio_allLoanedMedia.setChecked(True) + self.radio_allLoanedMedia.setChecked(False) self.radio_allLoanedMedia.setObjectName("radio_allLoanedMedia") self.horizontalLayout_2.addWidget(self.radio_allLoanedMedia) self.radio_currentlyLoaned = QtWidgets.QRadioButton(parent=self.centralwidget) + self.radio_currentlyLoaned.setChecked(True) self.radio_currentlyLoaned.setObjectName("radio_currentlyLoaned") self.horizontalLayout_2.addWidget(self.radio_currentlyLoaned) self.radio_overdueLoans = QtWidgets.QRadioButton(parent=self.centralwidget) @@ -88,9 +89,11 @@ class Ui_MainWindow(object): self.searchbox = QtWidgets.QLineEdit(parent=self.centralwidget) self.searchbox.setObjectName("searchbox") self.horizontalLayout_3.addWidget(self.searchbox) - self.btn_searchTableContent = QtWidgets.QPushButton(parent=self.centralwidget) - self.btn_searchTableContent.setObjectName("btn_searchTableContent") - self.horizontalLayout_3.addWidget(self.btn_searchTableContent) + self.searchfilter = QtWidgets.QComboBox(parent=self.centralwidget) + self.searchfilter.setObjectName("searchfilter") + self.searchfilter.addItem("") + self.searchfilter.addItem("") + self.horizontalLayout_3.addWidget(self.searchfilter) spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) self.horizontalLayout_3.addItem(spacerItem3) self.btn_extendSelectedMedia = QtWidgets.QPushButton(parent=self.centralwidget) @@ -150,7 +153,8 @@ class Ui_MainWindow(object): self.radio_allLoanedMedia.setText(_translate("MainWindow", "Alle Ausleihen")) self.radio_currentlyLoaned.setText(_translate("MainWindow", "Aktuell entliehen")) self.radio_overdueLoans.setText(_translate("MainWindow", "Überzogen")) - self.btn_searchTableContent.setText(_translate("MainWindow", "Suchen")) + self.searchfilter.setItemText(0, _translate("MainWindow", "Titel")) + self.searchfilter.setItemText(1, _translate("MainWindow", "Signatur")) self.btn_extendSelectedMedia.setText(_translate("MainWindow", "Ausgewählte Verlängern")) self.UserMediaTable.setSortingEnabled(True) item = self.UserMediaTable.horizontalHeaderItem(0) diff --git a/src/ui/sources/dialog_addBook.ui b/src/ui/sources/dialog_addBook.ui new file mode 100644 index 0000000..cab6162 --- /dev/null +++ b/src/ui/sources/dialog_addBook.ui @@ -0,0 +1,71 @@ + + + Dialog + + + + 0 + 0 + 262 + 100 + + + + Dialog + + + + + + + + + Signatur + + + + + + + Titel + + + + + + + + + + + + Qt::ClickFocus + + + Speichern + + + + + + + Qt::ClickFocus + + + Abbrechen + + + + + + + + + book_title + book_signature + btn_save + btn_cancel + + + + diff --git a/src/ui/sources/dialog_createUser.ui b/src/ui/sources/dialog_createUser.ui index 60efe56..125ab67 100644 --- a/src/ui/sources/dialog_createUser.ui +++ b/src/ui/sources/dialog_createUser.ui @@ -22,6 +22,9 @@ Qt::ImhDigitsOnly + + 102888557 + @@ -39,7 +42,11 @@ - + + + email@ph-freiburg.de + + @@ -49,7 +56,11 @@ - + + + Nachname, Vorname + + diff --git a/src/ui/sources/dialog_generateReport.ui b/src/ui/sources/dialog_generateReport.ui new file mode 100644 index 0000000..1aa90e1 --- /dev/null +++ b/src/ui/sources/dialog_generateReport.ui @@ -0,0 +1,173 @@ + + + Dialog + + + + 0 + 0 + 375 + 206 + + + + + 40 + 0 + + + + Dialog + + + + + + Wieviele Tage sollen im Bericht erfasst werden? + + + + + + + + + Tage + + + + + + + + + Woche + + + + + + + Monat + + + + + + + Jahr + + + + + + + + + + + + + + + + Qt::ClickFocus + + + 1 + + + 365 + + + Qt::Horizontal + + + true + + + QSlider::TicksAbove + + + 10 + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 40 + 16777215 + + + + Qt::NoFocus + + + true + + + + + + + + + + true + + + + + + + + + Fortschritt: + + + + + + + 24 + + + true + + + false + + + + + + + Bericht erstellen + + + + + + + radio_week + radio_month + radio_year + generateReport + + + + diff --git a/src/ui/sources/dialog_settings.ui b/src/ui/sources/dialog_settings.ui index cf70450..7f1806a 100644 --- a/src/ui/sources/dialog_settings.ui +++ b/src/ui/sources/dialog_settings.ui @@ -6,8 +6,8 @@ 0 0 - 377 - 206 + 436 + 184 diff --git a/src/ui/sources/main_Loans.ui b/src/ui/sources/main_Loans.ui new file mode 100644 index 0000000..cc4ec7e --- /dev/null +++ b/src/ui/sources/main_Loans.ui @@ -0,0 +1,148 @@ + + + MainWindow + + + + 0 + 0 + 899 + 658 + + + + MainWindow + + + + + + + + + Alle Ausleihen + + + true + + + + + + + Aktuell Entliehene Medien + + + + + + + Überzgene Medien + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + Titel + + + + + Signatur + + + + + Nutzer + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + ISBN + + + + + Signatur + + + + + Titel + + + + + Nutzerkonto + + + + + entliehen am + + + + + entliehen bis + + + + + Zurückgegeben am + + + + + + + + + + 0 + 0 + 899 + 22 + + + + + + + diff --git a/src/ui/sources/main_UserInterface.ui b/src/ui/sources/main_UserInterface.ui index ec53b9a..c8a85ca 100644 --- a/src/ui/sources/main_UserInterface.ui +++ b/src/ui/sources/main_UserInterface.ui @@ -17,18 +17,22 @@ - - + + - Benutzername + Suchbegriff - - - - - + + + + Ausleihe bis + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + @@ -60,23 +64,6 @@ - - - - Suchbegriff - - - - - - - Ausleihe bis - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - @@ -87,9 +74,46 @@ + + + + + + + Benutzername + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Neuen Nutzer anlegen + + + + + @@ -204,15 +228,23 @@ - + - Shortkeys + Hotkeys + + + Fenster + + + + - + + @@ -241,14 +273,34 @@ F6 + + + Nutzer + + + + + Ausleihhistorie + + + F8 + + + + + Bericht erstellen + + + F7 + + + btn_createNewUser input_userno input_username input_file_ident - groupBox - mediaOverview - btn_show_lentmedia + duedate diff --git a/src/ui/sources/main_userData.ui b/src/ui/sources/main_userData.ui index 97d572c..1ce0cdb 100644 --- a/src/ui/sources/main_userData.ui +++ b/src/ui/sources/main_userData.ui @@ -134,7 +134,7 @@ Alle Ausleihen - true + false @@ -143,6 +143,9 @@ Aktuell entliehen + + true + @@ -173,10 +176,17 @@ - - - Suchen - + + + + Titel + + + + + Signatur + + From dd429c9cd06944bc7edfeea40dc926b9daf6f212 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:25:33 +0200 Subject: [PATCH 12/83] add log class, debug function --- src/utils/debug.py | 24 ++++++++++++++++++++++++ src/utils/log.py | 26 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/utils/debug.py create mode 100644 src/utils/log.py diff --git a/src/utils/debug.py b/src/utils/debug.py new file mode 100644 index 0000000..306de51 --- /dev/null +++ b/src/utils/debug.py @@ -0,0 +1,24 @@ +from icecream import ic +from src.utils import Log +from src import __version__, config + + +log = Log("debugMessage") + + +def debugMessage(*args, **kwargs): + startmessage = "Logging debug message" + # join args and kwargs to a string + message = " ".join(args) + for key, value in kwargs.items(): + message += f" {key}: {value}" + if config.debug: + if config.log_debug: + log.info(f"{startmessage}: {message}") + + ic(message) + return message + + +if __name__ == "__main__": + debugMessage("This is a debug message ", test="test", url="https://www.google.com") diff --git a/src/utils/log.py b/src/utils/log.py new file mode 100644 index 0000000..fb815fa --- /dev/null +++ b/src/utils/log.py @@ -0,0 +1,26 @@ +import logging + + +class Log: + def __init__(self, name): + self.logger = logging.getLogger(name) + self.logger.setLevel(logging.DEBUG) + self.formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + self.file_handler = logging.FileHandler("log.log") + self.file_handler.setLevel(logging.DEBUG) + self.file_handler.setFormatter(self.formatter) + self.logger.addHandler(self.file_handler) + + def info(self, message): + self.logger.info(message) + + def debug(self, message): + self.logger.debug(message) + + def error(self, message): + self.logger.error(message) + + def warning(self, message): + self.logger.warning(message) From 0e8e6c5c40c539ecd2d49add2e2d8ae8207491b3 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:28:46 +0200 Subject: [PATCH 13/83] add schemas --- src/schemas/__init__.py | 3 ++- src/schemas/book.py | 22 +++++++++++----------- src/schemas/database.py | 12 ++++++------ src/schemas/loan.py | 14 ++++++++++++++ 4 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 src/schemas/loan.py diff --git a/src/schemas/__init__.py b/src/schemas/__init__.py index 68e5795..6b2c6aa 100644 --- a/src/schemas/__init__.py +++ b/src/schemas/__init__.py @@ -1,3 +1,4 @@ from .database import LOANS, MEDIA, USERS from .user import User -from .book import Book \ No newline at end of file +from .book import Book +from .loan import Loan \ No newline at end of file diff --git a/src/schemas/book.py b/src/schemas/book.py index 6e9a904..76893c8 100644 --- a/src/schemas/book.py +++ b/src/schemas/book.py @@ -3,14 +3,14 @@ from dataclasses import dataclass @dataclass class Book: - title: str | None = None - ppn: int | None = None - signature: str | None = None - isbn: str | None = None - link: str | None = None - database_id: int | None = None - link: str | None = None - loan_from: str | None = None - loan_to: str | None = None - returned: int | None = None - returned_date: str | None = None + title: str = "" + ppn: int = "" + signature: str = "" + isbn: str = "" + link: str = "" + database_id: int = "" + link: str = "" + loan_from: str = "" + loan_to: str = "" + returned: int = "" + returned_date: str = "" diff --git a/src/schemas/database.py b/src/schemas/database.py index 0a1dabb..29e554f 100644 --- a/src/schemas/database.py +++ b/src/schemas/database.py @@ -6,20 +6,20 @@ usermail TEXT NOT NULL); MEDIA = """CREATE TABLE IF NOT EXISTS media ( id INTEGER PRIMARY KEY AUTOINCREMENT, signature TEXT NOT NULL, -isbn TEXT NOT NULL, -ppn TEXT NOT NULL, +isbn TEXT, +ppn TEXT, title TEXT NOT NULL, -link TEXT NOT NULL); +link TEXT); """ LOANS = """CREATE TABLE IF NOT EXISTS loans ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, media_id INTEGER NOT NULL, -loan_date TEXT NOT NULL, -return_date TEXT NOT NULL, +loan_date DATETIME NOT NULL, +return_date DATETIME NOT NULL, returned INTEGER DEFAULT 0, -returned_date TEXT, +returned_date DATETIME, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (media_id) REFERENCES media(id)); """ diff --git a/src/schemas/loan.py b/src/schemas/loan.py new file mode 100644 index 0000000..fe0c944 --- /dev/null +++ b/src/schemas/loan.py @@ -0,0 +1,14 @@ +from .book import Book +from dataclasses import dataclass + + +@dataclass +class Loan: + id: int + user_id: int + media_id: int + loan_date: str + return_date: str + returned: int + returned_date: str + book: Book From a088c723fbd9225e3e15d5f59cd3bff76f6c25c0 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:29:21 +0200 Subject: [PATCH 14/83] add reverse function --- src/utils/stringtodate.py | 49 +++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/utils/stringtodate.py b/src/utils/stringtodate.py index be623a8..edaf24c 100644 --- a/src/utils/stringtodate.py +++ b/src/utils/stringtodate.py @@ -1,7 +1,6 @@ # import qdate from PyQt6 import QtCore - - +from .debug import debugMessage def stringToDate(date: str) -> QtCore.QDate: """Takes an input string and returns a QDate object. @@ -11,21 +10,31 @@ def stringToDate(date: str) -> QtCore.QDate: Returns: QtCore.QDate: the QDate object in string format DD.MM.yyyy """ - - datedata = date.split(" ")[1:] - month = datedata[0] - day = datedata[1] - year = datedata[2] - month = month.replace("Jan", "01") - month = month.replace("Feb", "02") - month = month.replace("Mar", "03") - month = month.replace("Apr", "04") - month = month.replace("May", "05") - month = month.replace("Jun", "06") - month = month.replace("Jul", "07") - month = month.replace("Aug", "08") - month = month.replace("Sep", "09") - month = month.replace("Oct", "10") - month = month.replace("Nov", "11") - month = month.replace("Dec", "12") - return QtCore.QDate(int(year), int(month), int(day)).toString("dd.MM.yyyy") + debugMessage(date=date) + if not date: + return "" + if "." in date: + # converts the date from dd.mm.yyyy to qdate + datedata = date.split(".") + day = datedata[0] + month = datedata[1] + year = datedata[2] + return QtCore.QDate(int(year), int(month), int(day)) # .toString("dd.MM.yyyy") + else: + datedata = date.split(" ")[1:] + month = datedata[0] + day = datedata[1] + year = datedata[2] + month = month.replace("Jan", "01") + month = month.replace("Feb", "02") + month = month.replace("Mar", "03") + month = month.replace("Apr", "04") + month = month.replace("May", "05") + month = month.replace("Jun", "06") + month = month.replace("Jul", "07") + month = month.replace("Aug", "08") + month = month.replace("Sep", "09") + month = month.replace("Oct", "10") + month = month.replace("Nov", "11") + month = month.replace("Dec", "12") + return QtCore.QDate(int(year), int(month), int(day)).toString("dd.MM.yyyy") From 0f0bcd48ba546aac7779311f9edfc21f6468ca34 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:29:51 +0200 Subject: [PATCH 15/83] add try except for icons that do not have fill --- src/utils/icon.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/icon.py b/src/utils/icon.py index c75dd97..3b75378 100644 --- a/src/utils/icon.py +++ b/src/utils/icon.py @@ -67,8 +67,12 @@ class Icon: with open(icon_path, "rb") as file: icon = file.read() cicon = str(icon) - fill = re.search(r"fill=\"(.*?)\"", cicon).group(1) - icon = icon.replace(fill.encode(), config.color.encode()) + try: + fill = re.search(r"fill=\"(.*?)\"", cicon).group(1) + except AttributeError: + fill = None + if fill: + icon = icon.replace(fill.encode(), config.color.encode()) return icon From 6ac92e46e0c7fc3587a2138a56869e1fa48bbdde Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:32:35 +0200 Subject: [PATCH 16/83] user updates --- src/ui/user.py | 67 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/src/ui/user.py b/src/ui/user.py index e7aafbc..1c59e99 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -3,11 +3,20 @@ from PyQt6 import QtCore, QtGui, QtWidgets from src.logic import Database from src.schemas import User from .extendLoan import ExtendLoan -from src.utils.stringtodate import stringToDate +from src.utils import stringToDate, Icon + +TABLETOFIELDTRANSLATE = { + "Titel": "title", + "Signatur": "signature", +} + + class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self, u_name, u_no, u_mail): super(UserUI, self).__init__() self.setupUi(self) + self.setWindowTitle("Nutzerdaten") + self.setWindowIcon(Icon("user").icon) self.db = Database() self.username = u_name self.userno = u_no @@ -35,6 +44,7 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): # if one or more rows is selected, enable btn self.UserMediaTable.itemSelectionChanged.connect(self.userTableAction) # LineEdits + self.searchbox.textChanged.connect(self.limitResults) # self.frame.hide() self.name.textChanged.connect(self.showFrame) @@ -46,17 +56,34 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): def extendLoan(self): extend = ExtendLoan(self.username, self.userMedia) extend.exec() - extendDate = extend.extendDate.toString() - # print columns of selected rows - for item in self.UserMediaTable.selectedItems(): - if item.column() == 1: - signature = item.text() - print(signature) - self.db.extendLoanDuration(signature, extendDate) - self.userMedia = [] - break + if extend.result() == 1: + extendDate = extend.extendDate.toString() + # print columns of selected rows + for item in self.UserMediaTable.selectedItems(): + if item.column() == 1: + signature = item.text() + print(signature) + self.db.extendLoanDuration(signature, extendDate) + self.userMedia = [] + break + self.UserMediaTable.setRowCount(0) + self.loadMedia() + return + + def limitResults(self): + limiter = self.searchbox.text().lower() + searchfield = self.searchfilter.currentText() + searchfield = TABLETOFIELDTRANSLATE[searchfield] + # dbg(limiter=limiter, search=searchfield) + self.UserMediaTable.setRowCount(0) - self.loadMedia() + for loan in self.userMedia: + print("looping loans") + fielddata = eval(f"loan.{searchfield}") + if isinstance(fielddata, str): + fielddata = fielddata.lower() + if limiter in fielddata: + self.addBookToTable(loan) def userTableAction(self): if self.UserMediaTable.selectedItems(): @@ -105,17 +132,15 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): todate = stringToDate(book.loan_to) if mode == "current": # book not returned - if book.returned == 0: - self.addBookToTable(book) + if book.returned == 1: + continue elif mode == "overdue": # book not returned and todays date is greater than todate - if ( - book.returned == 0 - and QtCore.QDate.fromString(todate) > QtCore.QDate.currentDate() - ): - self.addBookToTable(book) - else: - self.addBookToTable(book) + if book.returned_date != "": + continue + if todate > QtCore.QDate.currentDate(): + continue + self.addBookToTable(book) def addBookToTable(self, book): self.UserMediaTable.insertRow(0) @@ -144,6 +169,6 @@ def launch(): app = QtWidgets.QApplication(sys.argv) - window = UserUI("Test", "132", "sdf@f.de") + window = UserUI("Test", "3613899476", "sdf@f.de") window.show() sys.exit(app.exec()) \ No newline at end of file From 6edcca13ba4b0c048f96ccddf1638aba20137998 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:38:54 +0200 Subject: [PATCH 17/83] update code to allow both txt and csv generation --- src/ui/reportUi.py | 106 ++++++++++++++++++++++++++++++++++++++ src/utils/reportThread.py | 66 ++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 src/ui/reportUi.py create mode 100644 src/utils/reportThread.py diff --git a/src/ui/reportUi.py b/src/ui/reportUi.py new file mode 100644 index 0000000..e68bc3e --- /dev/null +++ b/src/ui/reportUi.py @@ -0,0 +1,106 @@ +from PyQt6 import QtCore, QtWidgets, QtGui +from .sources.Ui_dialog_generateReport import Ui_Dialog +from src.utils import Icon +from src.utils.reportThread import ReportThread +from src.logic import Database +import os + + +class ReportUi(QtWidgets.QDialog, Ui_Dialog): + def __init__(self, parent=None): + super(ReportUi, self).__init__(parent) + self.setupUi(self) + self.setWindowIcon(Icon("report").icon) + self.setWindowTitle("Report erstellen") + self.radioButton.hide() + self.db = Database() + + self.reportprogress.hide() + + # variables + self.maxrecords = 0 + self.days = 0 + self.rthread = ReportThread() + + # buttons + self.generateReport.setEnabled(False) + self.generateReport.clicked.connect(self.generate_report) + self.radio_year.clicked.connect(self.set_days_by_radio) + self.radio_month.clicked.connect(self.set_days_by_radio) + self.radio_week.clicked.connect(self.set_days_by_radio) + self.format_txt.clicked.connect(lambda: self.rthread.setFormat("txt")) + self.format_csv.clicked.connect(lambda: self.rthread.setFormat("csv")) + self.format_csv.clicked.connect(lambda: self.generateReport.setEnabled(True)) + self.format_txt.clicked.connect(lambda: self.generateReport.setEnabled(True)) + # sliders + self.dayslider.valueChanged.connect(self.set_days) + self.show() + + # labels + self.label_4.hide() + + def set_days_by_radio(self): + if self.radio_year.isChecked(): + self.set_days(365) + self.dayslider.setValue(365) + elif self.radio_month.isChecked(): + self.set_days(30) + self.dayslider.setValue(30) + elif self.radio_week.isChecked(): + self.set_days(7) + self.dayslider.setValue(7) + + def set_days(self, value): + # if value is not 7,30,365, deactivate radio buttons + if value != 7 and value != 30 and value != 365: + self.radioButton.setChecked(True) + + self.days = value + self.dayValue.setText(str(value)) + + def generate_report(self): + print(self.days) + self.rthread.setDays(self.days) + self.rthread.report_signal.connect(self.report_generated) + self.rthread.report_nums_signal.connect(self.show_progress) + self.rthread.report_progress_signal.connect(self.update_progress) + self.rthread.finished.connect(self.reset) + # self.rthread.finished.connect(self.rthread.deleteLater) + self.rthread.start() + + def reset(self): + self.days = 0 + self.reportprogress.hide() + self.reportprogress.setValue(0) + self.label_4.setText("Fortschritt:") + self.label_4.hide() + + def update_progress(self, num): + self.reportlink.clear() + self.reportprogress.setValue(num) + self.label_4.setText("Fortschritt: " + str(num) + "/" + str(self.maxrecords)) + if num == self.reportprogress.maximum(): + self.label_4.setText("Datei wird generiert") + + def show_progress(self, num): + self.reportprogress.show() + self.label_4.show() + self.maxrecords = num + self.reportprogress.setMaximum(self.maxrecords) + + def report_generated(self): + self.reportlink.setOpenExternalLinks(True) + fileformat = self.rthread.format + print(fileformat) + self.reportlink.setText( + f'Report' + ) + self.reportprogress.hide() + + +def launch(): + import sys + + app = QtWidgets.QApplication(sys.argv) + window = ReportUi() + sys.exit(app.exec()) diff --git a/src/utils/reportThread.py b/src/utils/reportThread.py new file mode 100644 index 0000000..ac83103 --- /dev/null +++ b/src/utils/reportThread.py @@ -0,0 +1,66 @@ +from PyQt6.QtCore import QThread, pyqtSignal, QDate +from prettytable import PrettyTable +from src.logic import Database +from src.utils import stringToDate +import sqlite3 as sql + + +class ReportThread(QThread): + report_signal = pyqtSignal(str) + report_progress_signal = pyqtSignal(int) + report_nums_signal = pyqtSignal(int) + + def __init__(self, parent=None): + super(ReportThread, self).__init__(parent) + self.days = None + self.format = "txt" + + def setFormat(self, format): + self.format = format + + def setDays(self, days): + self.days = days + + def run(self): + db = Database() + + path = db.db_path + day = QDate.currentDate().addDays(-self.days).toString("yyyy-MM-dd") + query = f"""SELECT * FROM loans WHERE loan_date >= '{day}';""" + print(query) + colnames = ["UserId", "Title", "Action", "Datum"] + table = PrettyTable(colnames) + table.align[colnames[0]] = "l" + table.align[colnames[1]] = "l" + table.align[colnames[2]] = "l" + + with sql.connect(path) as conn: + cursor = conn.cursor() + cursor.execute(query) + loans = cursor.fetchall() + self.report_nums_signal.emit(len(loans)) + counter = 0 + for loan in loans: + counter += 1 + self.report_progress_signal.emit(counter) + loan_action = "Ausleihe" if loan[5] == 0 else "Rückgabe" + loan_action_date = stringToDate( + loan[3] if loan[5] == 0 else loan[6] + ).toString("dd.MM.yyyy") + table.add_row( + [ + loan[1], + db.getMedia(loan[2]).title, + loan_action, + loan_action_date, + ] + ) + # # print(table) + # # wruitng the table to a file + if self.format == "csv": + with open("report.csv", "w", encoding="utf-8") as f: + f.write(table.get_csv_string()) + else: + with open("report.txt", "w", encoding="utf-8") as f: + f.write(str(table)) + self.report_signal.emit("Report generated successfully.") From cff311cb9e4c12052bd49028b77b140210bd73ac Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:39:08 +0200 Subject: [PATCH 18/83] icon attributions --- ATTRIBUTIONS.MD | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 ATTRIBUTIONS.MD diff --git a/ATTRIBUTIONS.MD b/ATTRIBUTIONS.MD new file mode 100644 index 0000000..156321f --- /dev/null +++ b/ATTRIBUTIONS.MD @@ -0,0 +1,5 @@ +## Icons + if not stated elsewhere, all Icons are part of the Material Symbols Package provided by Google + ### Specific Icons + - borrow_boks.svg Credits to Maxicons, Icon downloaded frmo thenounproject.com [Link](https://thenounproject.com/icon/borrow-book-3317975/) + - borrow \ No newline at end of file From 55abe8e8ef80221b1d4a52524f8f09e7725b0381 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:39:33 +0200 Subject: [PATCH 19/83] ui updates --- src/ui/sources/Ui_dialog_generateReport.py | 52 ++++++++---- src/ui/sources/Ui_dialog_settings.py | 2 +- src/ui/sources/Ui_main_Loans.py | 2 +- src/ui/sources/Ui_main_UserInterface.py | 20 ++++- src/ui/sources/Ui_main_userData.py | 14 ++++ src/ui/sources/dialog_generateReport.ui | 98 +++++++++++++++------- src/ui/sources/dialog_settings.ui | 14 ++-- src/ui/sources/main_Loans.ui | 2 +- src/ui/sources/main_UserInterface.ui | 49 ++++++++++- src/ui/sources/main_userData.ui | 26 +++++- 10 files changed, 213 insertions(+), 66 deletions(-) diff --git a/src/ui/sources/Ui_dialog_generateReport.py b/src/ui/sources/Ui_dialog_generateReport.py index 258020d..76022fe 100644 --- a/src/ui/sources/Ui_dialog_generateReport.py +++ b/src/ui/sources/Ui_dialog_generateReport.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(375, 206) + Dialog.resize(375, 247) Dialog.setMinimumSize(QtCore.QSize(40, 0)) self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) self.verticalLayout.setObjectName("verticalLayout") @@ -36,20 +36,6 @@ class Ui_Dialog(object): self.radio_year.setObjectName("radio_year") self.horizontalLayout.addWidget(self.radio_year) self.gridLayout.addLayout(self.horizontalLayout, 1, 1, 1, 1) - self.reportlink = QtWidgets.QLabel(parent=Dialog) - self.reportlink.setText("") - self.reportlink.setObjectName("reportlink") - self.gridLayout.addWidget(self.reportlink, 2, 1, 1, 1) - self.dayslider = QtWidgets.QSlider(parent=Dialog) - self.dayslider.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) - self.dayslider.setMinimum(1) - self.dayslider.setMaximum(365) - self.dayslider.setOrientation(QtCore.Qt.Orientation.Horizontal) - self.dayslider.setInvertedControls(True) - self.dayslider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksAbove) - self.dayslider.setTickInterval(10) - self.dayslider.setObjectName("dayslider") - self.gridLayout.addWidget(self.dayslider, 0, 1, 1, 1) self.dayValue = QtWidgets.QLineEdit(parent=Dialog) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -66,7 +52,38 @@ class Ui_Dialog(object): self.radioButton.setText("") self.radioButton.setCheckable(True) self.radioButton.setObjectName("radioButton") - self.gridLayout.addWidget(self.radioButton, 2, 2, 1, 1) + self.gridLayout.addWidget(self.radioButton, 3, 2, 1, 1) + self.reportlink = QtWidgets.QLabel(parent=Dialog) + self.reportlink.setText("") + self.reportlink.setObjectName("reportlink") + self.gridLayout.addWidget(self.reportlink, 3, 1, 1, 1) + self.dayslider = QtWidgets.QSlider(parent=Dialog) + self.dayslider.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) + self.dayslider.setMinimum(1) + self.dayslider.setMaximum(365) + self.dayslider.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.dayslider.setInvertedControls(True) + self.dayslider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksAbove) + self.dayslider.setTickInterval(10) + self.dayslider.setObjectName("dayslider") + self.gridLayout.addWidget(self.dayslider, 0, 1, 1, 1) + self.frame = QtWidgets.QFrame(parent=Dialog) + self.frame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + self.frame.setLineWidth(0) + self.frame.setObjectName("frame") + self.gridLayout_2 = QtWidgets.QGridLayout(self.frame) + self.gridLayout_2.setObjectName("gridLayout_2") + self.format_txt = QtWidgets.QRadioButton(parent=self.frame) + self.format_txt.setObjectName("format_txt") + self.gridLayout_2.addWidget(self.format_txt, 0, 0, 1, 1) + self.format_csv = QtWidgets.QRadioButton(parent=self.frame) + self.format_csv.setObjectName("format_csv") + self.gridLayout_2.addWidget(self.format_csv, 0, 1, 1, 1) + self.gridLayout.addWidget(self.frame, 2, 1, 1, 1) + self.label_3 = QtWidgets.QLabel(parent=Dialog) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1) self.verticalLayout.addLayout(self.gridLayout) self.label_4 = QtWidgets.QLabel(parent=Dialog) self.label_4.setObjectName("label_4") @@ -95,5 +112,8 @@ class Ui_Dialog(object): self.radio_week.setText(_translate("Dialog", "Woche")) self.radio_month.setText(_translate("Dialog", "Monat")) self.radio_year.setText(_translate("Dialog", "Jahr")) + self.format_txt.setText(_translate("Dialog", "Text")) + self.format_csv.setText(_translate("Dialog", "Excel")) + self.label_3.setText(_translate("Dialog", "Dateiformat")) self.label_4.setText(_translate("Dialog", "Fortschritt:")) self.generateReport.setText(_translate("Dialog", " Bericht erstellen")) diff --git a/src/ui/sources/Ui_dialog_settings.py b/src/ui/sources/Ui_dialog_settings.py index aae6c32..1224deb 100644 --- a/src/ui/sources/Ui_dialog_settings.py +++ b/src/ui/sources/Ui_dialog_settings.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(436, 184) + Dialog.resize(422, 184) self.formLayout = QtWidgets.QFormLayout(Dialog) self.formLayout.setObjectName("formLayout") self.label = QtWidgets.QLabel(parent=Dialog) diff --git a/src/ui/sources/Ui_main_Loans.py b/src/ui/sources/Ui_main_Loans.py index 5161cfe..6c1edd2 100644 --- a/src/ui/sources/Ui_main_Loans.py +++ b/src/ui/sources/Ui_main_Loans.py @@ -80,7 +80,7 @@ class Ui_MainWindow(object): MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) self.radio_all.setText(_translate("MainWindow", "Alle Ausleihen")) self.radio_current.setText(_translate("MainWindow", "Aktuell Entliehene Medien")) - self.radio_overdue.setText(_translate("MainWindow", "Überzgene Medien")) + self.radio_overdue.setText(_translate("MainWindow", "Überzogene Medien")) self.searchFields.setItemText(0, _translate("MainWindow", "Titel")) self.searchFields.setItemText(1, _translate("MainWindow", "Signatur")) self.searchFields.setItemText(2, _translate("MainWindow", "Nutzer")) diff --git a/src/ui/sources/Ui_main_UserInterface.py b/src/ui/sources/Ui_main_UserInterface.py index b1374ba..c929a66 100644 --- a/src/ui/sources/Ui_main_UserInterface.py +++ b/src/ui/sources/Ui_main_UserInterface.py @@ -47,9 +47,6 @@ class Ui_MainWindow(object): self.input_username = QtWidgets.QLineEdit(parent=self.centralwidget) self.input_username.setObjectName("input_username") self.gridLayout.addWidget(self.input_username, 2, 1, 1, 1) - self.duedate = QtWidgets.QDateEdit(parent=self.centralwidget) - self.duedate.setObjectName("duedate") - self.gridLayout.addWidget(self.duedate, 5, 1, 1, 1) self.input_file_ident = QtWidgets.QLineEdit(parent=self.centralwidget) self.input_file_ident.setObjectName("input_file_ident") self.gridLayout.addWidget(self.input_file_ident, 3, 1, 1, 1) @@ -67,6 +64,22 @@ class Ui_MainWindow(object): self.btn_createNewUser.setObjectName("btn_createNewUser") self.horizontalLayout_3.addWidget(self.btn_createNewUser) self.gridLayout.addLayout(self.horizontalLayout_3, 0, 1, 1, 1) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.duedate = QtWidgets.QDateEdit(parent=self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.duedate.sizePolicy().hasHeightForWidth()) + self.duedate.setSizePolicy(sizePolicy) + self.duedate.setMinimumSize(QtCore.QSize(130, 0)) + self.duedate.setMaximumSize(QtCore.QSize(100, 16777215)) + self.duedate.setBaseSize(QtCore.QSize(70, 0)) + self.duedate.setObjectName("duedate") + self.horizontalLayout_4.addWidget(self.duedate) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_4.addItem(spacerItem1) + self.gridLayout.addLayout(self.horizontalLayout_4, 5, 1, 1, 1) self.verticalLayout.addLayout(self.gridLayout) self.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget) self.groupBox.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) @@ -165,7 +178,6 @@ class Ui_MainWindow(object): MainWindow.setTabOrder(self.btn_createNewUser, self.input_userno) MainWindow.setTabOrder(self.input_userno, self.input_username) MainWindow.setTabOrder(self.input_username, self.input_file_ident) - MainWindow.setTabOrder(self.input_file_ident, self.duedate) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate diff --git a/src/ui/sources/Ui_main_userData.py b/src/ui/sources/Ui_main_userData.py index d517853..e09d822 100644 --- a/src/ui/sources/Ui_main_userData.py +++ b/src/ui/sources/Ui_main_userData.py @@ -36,6 +36,8 @@ class Ui_MainWindow(object): self.label.setObjectName("label") self.gridLayout.addWidget(self.label, 0, 0, 1, 1) self.user_no = QtWidgets.QLineEdit(parent=self.centralwidget) + self.user_no.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.user_no.setReadOnly(True) self.user_no.setObjectName("user_no") self.gridLayout.addWidget(self.user_no, 1, 1, 1, 1) self.frame = QtWidgets.QFrame(parent=self.centralwidget) @@ -103,6 +105,7 @@ class Ui_MainWindow(object): self.verticalLayout.addLayout(self.horizontalLayout_3) self.UserMediaTable = QtWidgets.QTableWidget(parent=self.centralwidget) self.UserMediaTable.setMouseTracking(True) + self.UserMediaTable.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) self.UserMediaTable.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.UserMediaTable.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) self.UserMediaTable.setAlternatingRowColors(True) @@ -140,6 +143,17 @@ class Ui_MainWindow(object): self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) + MainWindow.setTabOrder(self.name, self.mail) + MainWindow.setTabOrder(self.mail, self.btn_userChange_save) + MainWindow.setTabOrder(self.btn_userChange_save, self.btn_userchange_cancel) + MainWindow.setTabOrder(self.btn_userchange_cancel, self.radio_allLoanedMedia) + MainWindow.setTabOrder(self.radio_allLoanedMedia, self.radio_currentlyLoaned) + MainWindow.setTabOrder(self.radio_currentlyLoaned, self.radio_overdueLoans) + MainWindow.setTabOrder(self.radio_overdueLoans, self.searchbox) + MainWindow.setTabOrder(self.searchbox, self.searchfilter) + MainWindow.setTabOrder(self.searchfilter, self.btn_extendSelectedMedia) + MainWindow.setTabOrder(self.btn_extendSelectedMedia, self.UserMediaTable) + MainWindow.setTabOrder(self.UserMediaTable, self.user_no) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate diff --git a/src/ui/sources/dialog_generateReport.ui b/src/ui/sources/dialog_generateReport.ui index 1aa90e1..1700beb 100644 --- a/src/ui/sources/dialog_generateReport.ui +++ b/src/ui/sources/dialog_generateReport.ui @@ -7,7 +7,7 @@ 0 0 375 - 206 + 247 @@ -61,7 +61,45 @@ - + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 40 + 16777215 + + + + Qt::NoFocus + + + true + + + + + + + + + + true + + + + @@ -93,41 +131,39 @@ - - - - - 0 - 0 - + + + + QFrame::NoFrame - - - 0 - 0 - + + QFrame::Plain - - - 40 - 16777215 - - - - Qt::NoFocus - - - true + + 0 + + + + + Text + + + + + + + Excel + + + + - - + + - - - - true + Dateiformat diff --git a/src/ui/sources/dialog_settings.ui b/src/ui/sources/dialog_settings.ui index 7f1806a..201e573 100644 --- a/src/ui/sources/dialog_settings.ui +++ b/src/ui/sources/dialog_settings.ui @@ -6,7 +6,7 @@ 0 0 - 436 + 422 184 @@ -127,12 +127,12 @@ accept() - 248 - 254 + 379 + 174 157 - 274 + 183 @@ -143,12 +143,12 @@ reject() - 316 - 260 + 426 + 174 286 - 274 + 183 diff --git a/src/ui/sources/main_Loans.ui b/src/ui/sources/main_Loans.ui index cc4ec7e..f7d5a23 100644 --- a/src/ui/sources/main_Loans.ui +++ b/src/ui/sources/main_Loans.ui @@ -37,7 +37,7 @@ - Überzgene Medien + Überzogene Medien diff --git a/src/ui/sources/main_UserInterface.ui b/src/ui/sources/main_UserInterface.ui index c8a85ca..32334b0 100644 --- a/src/ui/sources/main_UserInterface.ui +++ b/src/ui/sources/main_UserInterface.ui @@ -74,9 +74,6 @@ - - - @@ -114,6 +111,51 @@ + + + + + + + 0 + 0 + + + + + 130 + 0 + + + + + 100 + 16777215 + + + + + 70 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -300,7 +342,6 @@ input_userno input_username input_file_ident - duedate diff --git a/src/ui/sources/main_userData.ui b/src/ui/sources/main_userData.ui index 1ce0cdb..fd6ef9f 100644 --- a/src/ui/sources/main_userData.ui +++ b/src/ui/sources/main_userData.ui @@ -48,7 +48,14 @@ - + + + Qt::NoFocus + + + true + + @@ -219,6 +226,9 @@ true + + Qt::NoFocus + Qt::ScrollBarAlwaysOff @@ -299,6 +309,20 @@ + + name + mail + btn_userChange_save + btn_userchange_cancel + radio_allLoanedMedia + radio_currentlyLoaned + radio_overdueLoans + searchbox + searchfilter + btn_extendSelectedMedia + UserMediaTable + user_no + From 131c858ac368f2ea446da3f69412ca8a6ae8f7de Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:39:51 +0200 Subject: [PATCH 20/83] update stringtodate --- src/utils/stringtodate.py | 50 ++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/utils/stringtodate.py b/src/utils/stringtodate.py index edaf24c..9a8ad55 100644 --- a/src/utils/stringtodate.py +++ b/src/utils/stringtodate.py @@ -13,28 +13,30 @@ def stringToDate(date: str) -> QtCore.QDate: debugMessage(date=date) if not date: return "" - if "." in date: - # converts the date from dd.mm.yyyy to qdate - datedata = date.split(".") - day = datedata[0] - month = datedata[1] - year = datedata[2] - return QtCore.QDate(int(year), int(month), int(day)) # .toString("dd.MM.yyyy") + if isinstance(date, QtCore.QDate): + return date.toString("yyyy-MM-dd") else: - datedata = date.split(" ")[1:] - month = datedata[0] - day = datedata[1] - year = datedata[2] - month = month.replace("Jan", "01") - month = month.replace("Feb", "02") - month = month.replace("Mar", "03") - month = month.replace("Apr", "04") - month = month.replace("May", "05") - month = month.replace("Jun", "06") - month = month.replace("Jul", "07") - month = month.replace("Aug", "08") - month = month.replace("Sep", "09") - month = month.replace("Oct", "10") - month = month.replace("Nov", "11") - month = month.replace("Dec", "12") - return QtCore.QDate(int(year), int(month), int(day)).toString("dd.MM.yyyy") + datedata = date.split("-") + day = datedata[2] + month = datedata[1] + year = datedata[0] + return QtCore.QDate(int(year), int(month), int(day)) + + # else: + # datedata = date.split(" ")[1:] + # month = datedata[0] + # day = datedata[1] + # year = datedata[2] + # month = month.replace("Jan", "01") + # month = month.replace("Feb", "02") + # month = month.replace("Mar", "03") + # month = month.replace("Apr", "04") + # month = month.replace("May", "05") + # month = month.replace("Jun", "06") + # month = month.replace("Jul", "07") + # month = month.replace("Aug", "08") + # month = month.replace("Sep", "09") + # month = month.replace("Oct", "10") + # month = month.replace("Nov", "11") + # month = month.replace("Dec", "12") + # return QtCore.QDate(int(year), int(month), int(day)).toString("dd.MM.yyyy") From d2aa0650f24c15b176a00e3e5f4ff38bc463df98 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:40:16 +0200 Subject: [PATCH 21/83] ui to create new books in case of dupes --- src/ui/newBook.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/ui/newBook.py diff --git a/src/ui/newBook.py b/src/ui/newBook.py new file mode 100644 index 0000000..9de1d0a --- /dev/null +++ b/src/ui/newBook.py @@ -0,0 +1,38 @@ +from .sources.Ui_dialog_addBook import Ui_Dialog +from src.schemas import Book +from src.utils import Icon + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class NewBook(QtWidgets.QDialog, Ui_Dialog): + def __init__(self): + super(NewBook, self).__init__() + self.setupUi(self) + self.setWindowTitle("Buch hinzufügen") + self.setWindowIcon(Icon("addBook").icon) + self.btn_save.setEnabled(False) + self.btn_save.clicked.connect(self.saveBook) + self.btn_cancel.clicked.connect(self.reject) + self.book_title.setFocus() + self.book_title.textChanged.connect(self.checkFields) + self.book_signature.textChanged.connect(self.checkFields) + self.book = None + + self.show() + + def checkFields(self): + if ( + self.book_title.hasAcceptableInput() + and self.book_signature.hasAcceptableInput + ): + self.btn_save.setEnabled(True) + else: + self.btn_save.setEnabled(False) + + def saveBook(self): + title = self.book_title.text() + signature = self.book_signature.text() + book = Book(title=title, signature=signature) + self.book = book + self.accept() From e5fb461394c888cab24cf17c45f3e50a44b58267 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:40:31 +0200 Subject: [PATCH 22/83] set icons, validators --- src/ui/createUser.py | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/ui/createUser.py b/src/ui/createUser.py index 6481aa3..a91c5a4 100644 --- a/src/ui/createUser.py +++ b/src/ui/createUser.py @@ -1,13 +1,17 @@ from .sources.Ui_dialog_createUser import Ui_Dialog from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtGui import QRegularExpressionValidator from src.logic import Database +from src.utils import Icon, Log from src.schemas import User - +import re class CreateUser(QtWidgets.QDialog, Ui_Dialog): def __init__(self, fieldname, data): super(CreateUser, self).__init__() self.setupUi(self) + self.setWindowTitle("Benutzer erstellen") + self.setWindowIcon(Icon("user").icon) # disable buttonbox save self.db = Database() self.buttonBox.button( @@ -25,12 +29,22 @@ class CreateUser(QtWidgets.QDialog, Ui_Dialog): QtWidgets.QDialogButtonBox.StandardButton.Save ).clicked.connect(self.saveUser) self.userid = None - + self.userno.setMaxLength(20) + self.user_mail.setValidator( + QRegularExpressionValidator( + QtCore.QRegularExpression( + r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}" + ) + ) + ) + self.userno.textChanged.connect( + lambda: self.validateInputUserno(self.userno.text(), "int") + ) def checkFields(self): if ( - self.username.text() != "" - and self.userno.text() != "" - and self.user_mail.text() != "" + self.username.hasAcceptableInput() + and self.userno.hasAcceptableInput() + and self.user_mail.hasAcceptableInput() ): self.buttonBox.button( QtWidgets.QDialogButtonBox.StandardButton.Save @@ -46,3 +60,19 @@ class CreateUser(QtWidgets.QDialog, Ui_Dialog): usermail = self.user_mail.text() self.db.insertUser(username, userno, usermail) self.userid = userno + + def validateInputUserno(self, value, type): + lastchar = value[-1] if value else "" + # if lastchar is not of the type, remove it + if type == "int": + if not lastchar.isdigit(): + self.userno.setText(value[:-1]) + + def validateInputMail(self, value): + pass + + +def launch(): + app = QtWidgets.QApplication([]) + window = CreateUser("id", "123456") + window.exec() \ No newline at end of file From 5cf59e29a29bb958dd9feeffd49d6233e3a632d3 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:41:34 +0200 Subject: [PATCH 23/83] set icon, title --- src/ui/extendLoan.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ui/extendLoan.py b/src/ui/extendLoan.py index 5977a29..39e18e4 100644 --- a/src/ui/extendLoan.py +++ b/src/ui/extendLoan.py @@ -1,26 +1,27 @@ from .sources.Ui_dialog_extendLoanDuration import Ui_Dialog from PyQt6 import QtWidgets, QtCore - +from src.utils import Icon class ExtendLoan(QtWidgets.QDialog, Ui_Dialog): def __init__(self, user, media): super(ExtendLoan, self).__init__() self.setupUi(self) + self.setWindowTitle("Leihfrist verlängern") + self.setWindowIcon(Icon("loan_extend").icon) self.user = user self.media = media self.currentDate = QtCore.QDate.currentDate().addDays(1) - self.extendDate = None + self.extendDate = "" self.extenduntil.setMinimumDate(self.currentDate) self.show() self.buttonBox.accepted.connect(self.extendLoan) def extendLoan(self): - print("Extend Loan") + # print("Extend Loan") selectedDate = self.extenduntil.selectedDate() - print(selectedDate) + # print(selectedDate) self.extendDate = selectedDate self.close() - pass def launch(user, media): From e3d8403b7fc1efe4b2a897d94b8b2dc8e2d3cc66 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:41:58 +0200 Subject: [PATCH 24/83] check backup only if backup enables --- src/logic/backup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/logic/backup.py b/src/logic/backup.py index cf514e2..aea1d30 100644 --- a/src/logic/backup.py +++ b/src/logic/backup.py @@ -9,7 +9,9 @@ class Backup: self.source_path = config.database.path + "/" + config.database.name self.backup_path = config.database.backupLocation + "/" + config.database.name self.backup = False - self.checkpaths() + if config.database.do_backup == True: + self.checkpaths() + config.database.do_backup = self.backup def checkpaths(self): if os.path.exists(config.database.backupLocation): From eb568db55547fdfb760d9966e24dd8464b5274e8 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:42:39 +0200 Subject: [PATCH 25/83] database changes --- src/logic/database.py | 77 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/src/logic/database.py b/src/logic/database.py index 0815f0e..0200bdd 100644 --- a/src/logic/database.py +++ b/src/logic/database.py @@ -2,9 +2,10 @@ import sqlite3 as sql import os from src import config from pathlib import Path -from src.schemas import USERS, MEDIA, LOANS, User, Book -from src.utils.stringtodate import stringToDate +from src.schemas import USERS, MEDIA, LOANS, User, Book, Loan +from src.utils import stringToDate, Log +log = Log("Database") class Database: def __init__(self, db_path: str = None): """ @@ -23,6 +24,7 @@ class Database: self.checkDatabaseStatus() def checkDatabaseStatus(self): + log.info("Checking Database Status") if self.tableCheck() == []: # self.logger.log_critical("Database does not exist, creating tables") self.createDatabase() @@ -48,17 +50,30 @@ class Database: conn.close() def createDatabase(self): - print("Creating Database") + log.info("Creating Database") + # print("Creating Database") if not os.path.exists(config.database.path): os.makedirs(config.database.path) conn = self.connect() cursor = conn.cursor() cursor.execute(USERS) + log.debug("Creating Users Table") cursor.execute(MEDIA) + log.debug("Creating Media Table") cursor.execute(LOANS) + log.debug("Creating Loans Table") conn.commit() self.close_connection(conn) + def getLoanCount(self): + query = "SELECT COUNT(*) FROM loans" + conn = self.connect() + cursor = conn.cursor() + cursor.execute(query) + result = cursor.fetchone() + self.close_connection(conn) + return result[0] + def tableCheck(self): # check if database has tables """ @@ -96,6 +111,7 @@ class Database: return users def insertUser(self, username, userno, usermail): + log.debug(f"Inserting User {userno}, {username}, {usermail}") conn = self.connect() cursor = conn.cursor() cursor.execute( @@ -111,8 +127,20 @@ class Database: result = cursor.fetchone() self.close_connection(conn) user = User(id=result[0], username=result[1], email=result[2]) + log.info(f"Returning User {user}") return user + def updateUser(self, username, userno, usermail): + log.debug(f"Updating User {userno}, {username}, {usermail}") + conn = self.connect() + cursor = conn.cursor() + cursor.execute( + f"UPDATE users SET username = '{username}', usermail = '{usermail}' WHERE id = '{userno}'" + ) + conn.commit() + + self.close_connection(conn) + def getActiveLoans(self, userid): conn = self.connect() cursor = conn.cursor() @@ -124,9 +152,31 @@ class Database: except sql.OperationalError: result = [] self.close_connection(conn) + log.info(f"Returning Active Loans {result}") return str(len(result)) + def getAllLoans(self): + loan_data = [] + query = "SELECT * FROM loans" + conn = self.connect() + cursor = conn.cursor() + cursor.execute(query) + loans = cursor.fetchall() + for loan in loans: + l = Loan( + loan[0], + loan[1], + loan[2], + stringToDate(loan[3]), + stringToDate(loan[4]), + loan[5], + stringToDate(loan[6]), + self.getMedia(loan[2]), + ) + loan_data.append(l) + return loan_data def insertLoan(self, userid, mediaid, loandate, duedate): + log.debug(f"Inserting Loan {userid}, {mediaid}, {loandate}, {duedate}") query = f"INSERT INTO loans (user_id, media_id, loan_date, return_date) Values ('{userid}', '{mediaid}', '{loandate}', '{duedate}')" conn = self.connect() cursor = conn.cursor() @@ -135,22 +185,30 @@ class Database: self.close_connection(conn) def insertMedia(self, media): - query = f"INSERT OR IGNORE INTO media (ppn, title, signature, isbn) VALUES ('{media.ppn}', '{media.title}', '{media.signature}', '{media.isbn}')" # , '{media.link}' + log.debug(f"Inserting Media {media}") + query = f"INSERT OR IGNORE INTO media (ppn, title, signature, isbn,link) VALUES ('{media.ppn}', '{media.title}', '{media.signature}', '{media.isbn}','{media.link}')" # , '{media.link}' + log.info(f"Query: |{query}|") conn = self.connect() cursor = conn.cursor() cursor.execute(query) + conn.commit() + self.close_connection(conn) return cursor.lastrowid + def getLoansBy(self, field, value): + # query all loans, sort by date descending and return + pass def getMediaSimilarSignatureByID(self, media_id) -> list[Book]: + log.info(f"Getting Media Similar to {media_id}") query = f"SELECT * FROM media WHERE id = '{media_id}'" conn = self.connect() cursor = conn.cursor() cursor.execute(query) result = cursor.fetchone() signature = result[1] - print(signature) + # print(signature) query = f"SELECT * FROM media WHERE signature LIKE '%{signature}%'" cursor.execute(query) result = cursor.fetchall() @@ -167,9 +225,11 @@ class Database: database_id=res[0], ) ) + log.debug(f"Returning Similar Media {data}") return data def getMedia(self, media_id): + # log.info(f"Getting Media {media_id}") query = f"SELECT * FROM media WHERE id = '{media_id}'" conn = self.connect() cursor = conn.cursor() @@ -201,10 +261,11 @@ class Database: book.returned = res[5] book.returned_date = res[6] books.append(book) - print(book) + log.info(f"Returning All Media entries ({len(books)}) for user {user_id}") return books def checkMediaExists(self, media): + log.info(f"Checking Media {media}") conn = self.connect() cursor = conn.cursor() query = f"SELECT id, signature FROM media WHERE ppn = '{media.ppn}' OR title = '{media.title}' OR isbn = '{media.isbn}' OR signature = '{media.signature}'" @@ -219,6 +280,7 @@ class Database: return False def checkLoanState(self, book_id): + log.info(f"Checking Loan State for {book_id}") query = f"SELECT * FROM loans WHERE media_id = '{book_id}' AND returned = 0" conn = self.connect() cursor = conn.cursor() @@ -228,6 +290,7 @@ class Database: return result def returnMedia(self, media_id, returndate): + log.info(f"Returning Media {media_id}") query = f"UPDATE loans SET returned = 1, returned_date = '{returndate}' WHERE media_id = '{media_id}' AND returned = 0" conn = self.connect() cursor = conn.cursor() @@ -259,6 +322,7 @@ class Database: # def selectClosestReturnDate(self, user_id): + log.info(f"Selecting Closest Return Date for {user_id}") query = f"SELECT return_date FROM loans WHERE user_id = '{user_id}' AND returned = 0 ORDER BY return_date ASC LIMIT 1" conn = self.connect() cursor = conn.cursor() @@ -269,6 +333,7 @@ class Database: return result[0] def extendLoanDuration(self, signature, newDate): + log.info(f"Extending Loan Duration for {signature} to {newDate}") book_id = self.checkMediaExists(Book(signature=signature)) query = f"UPDATE loans SET return_date = '{newDate}' WHERE media_id = '{book_id[0]}' AND returned = 0" conn = self.connect() From 2bce811f8844adab09563004cce2b735b62d11a4 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:43:09 +0200 Subject: [PATCH 26/83] save userdata changes, format date strings --- src/ui/user.py | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/ui/user.py b/src/ui/user.py index 1c59e99..86ca5b2 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -58,11 +58,11 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): extend.exec() if extend.result() == 1: extendDate = extend.extendDate.toString() - # print columns of selected rows + # # print columns of selected rows for item in self.UserMediaTable.selectedItems(): if item.column() == 1: signature = item.text() - print(signature) + # print(signature) self.db.extendLoanDuration(signature, extendDate) self.userMedia = [] break @@ -78,7 +78,7 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): self.UserMediaTable.setRowCount(0) for loan in self.userMedia: - print("looping loans") + # print("looping loans") fielddata = eval(f"loan.{searchfield}") if isinstance(fielddata, str): fielddata = fielddata.lower() @@ -105,6 +105,15 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): self.mail.setText(self.usermail) def saveChanges(self): + username = self.name.text() + userno = int(self.user_no.text()) + usermail = self.mail.text() + self.db.updateUser(username, userno, usermail) + self.username = username + self.userno = userno + self.usermail = usermail + self.frame.hide() + self.discardChanges() pass def discardChanges(self): @@ -119,12 +128,12 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): if self.radio_currentlyLoaned.isChecked() else "overdue" ) - print(mode) + # print(mode) if self.userMedia == []: books = self.db.getAllMedia(self.userno) for book in books: self.userMedia.append(book) - print(self.userMedia) + # print(self.userMedia) self.UserMediaTable.setRowCount(0) for book in self.userMedia: @@ -157,10 +166,28 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): ) self.UserMediaTable.setItem(0, 1, QtWidgets.QTableWidgetItem(book.signature)) self.UserMediaTable.setItem(0, 2, QtWidgets.QTableWidgetItem(book.title)) - self.UserMediaTable.setItem(0, 3, QtWidgets.QTableWidgetItem(book.loan_from)) - self.UserMediaTable.setItem(0, 4, QtWidgets.QTableWidgetItem(book.loan_to)) self.UserMediaTable.setItem( - 0, 5, QtWidgets.QTableWidgetItem(book.returned_date) + 0, + 3, + QtWidgets.QTableWidgetItem( + stringToDate(book.loan_from).toString("dd.MM.yyyy") + ), + ) + self.UserMediaTable.setItem( + 0, + 4, + QtWidgets.QTableWidgetItem( + stringToDate(book.loan_to).toString("dd.MM.yyyy") + ), + ) + self.UserMediaTable.setItem( + 0, + 5, + QtWidgets.QTableWidgetItem( + "" + if book.returned_date is None + else stringToDate(book.returned_date).toString("dd.MM.yyyy") + ), ) From 7a58dbc5ecd7acc1ea9e92081ef798a75660d3f2 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:43:31 +0200 Subject: [PATCH 27/83] set icon, title, headerstretch --- src/ui/multiUserInfo.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/ui/multiUserInfo.py b/src/ui/multiUserInfo.py index 4298dda..ff03a85 100644 --- a/src/ui/multiUserInfo.py +++ b/src/ui/multiUserInfo.py @@ -1,23 +1,33 @@ from .sources.Ui_dialog_multipleUserfound import Ui_Dialog from PyQt6 import QtCore, QtGui, QtWidgets from src.schemas import User +from src.utils import Icon class MultiUserFound(QtWidgets.QDialog, Ui_Dialog): def __init__(self, users: list[User]): super(MultiUserFound, self).__init__() self.setupUi(self) + self.setWindowTitle("Mehrere Benutzer gefunden") + self.setWindowIcon(Icon("multiuser").icon) self.users = users self.userdata = None + self.row = None + self.displayUsers() self.buttonBox.button( QtWidgets.QDialogButtonBox.StandardButton.Ok - ).clicked.connect(self.selectUser) + ).clicked.connect(self.accept) self.buttonBox.button( QtWidgets.QDialogButtonBox.StandardButton.Cancel ).clicked.connect(self.reject) + self.tableWidget.horizontalHeader().setSectionResizeMode( + QtWidgets.QHeaderView.ResizeMode.Stretch + ) + self.tableWidget.cellClicked.connect(self.selectUser) - def selectUser(self, row): + def selectUser(self, row, column): + # print(row, column) user = User( id=self.tableWidget.item(row, 0).text(), username=self.tableWidget.item(row, 1).text(), @@ -25,7 +35,6 @@ class MultiUserFound(QtWidgets.QDialog, Ui_Dialog): ) self.userdata = user - self.accept() def displayUsers(self): for user in self.users: From 4e04dd26a6d92a065d381fa28bc4f2c7ed374d47 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:43:53 +0200 Subject: [PATCH 28/83] ui to display all loans for all users --- src/ui/loans.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/ui/loans.py diff --git a/src/ui/loans.py b/src/ui/loans.py new file mode 100644 index 0000000..3032699 --- /dev/null +++ b/src/ui/loans.py @@ -0,0 +1,112 @@ +from .sources.Ui_main_Loans import Ui_MainWindow +from PyQt6 import QtCore, QtGui, QtWidgets +from .user import UserUI +from src.logic import Database +from src.utils import stringToDate, Icon +from src.utils import debugMessage as dbg +from icecream import ic + +TABLETOFIELDTRANSLATE = { + "Titel": "book.title", + "Signatur": "book.signature", + "Nutzer": "user_id", +} + + +class LoanWindow(QtWidgets.QMainWindow, Ui_MainWindow): + def __init__(self): + super(LoanWindow, self).__init__() + self.setupUi(self) + self.setWindowTitle("Ausleihhistorie") + self.setWindowIcon(Icon("borrow").icon) + self.loanTable.horizontalHeader().setSectionResizeMode( + QtWidgets.QHeaderView.ResizeMode.Stretch + ) + self.db = Database() + self.loans = [] + self.loadLoans() + + # lineedits + self.searchbar.textChanged.connect(self.limitResults) + + # radio buttons + self.radio_all.clicked.connect(self.filterResults) + self.radio_current.clicked.connect(self.filterResults) + self.radio_overdue.clicked.connect(self.filterResults) + + # table + self.loanTable.doubleClicked.connect(self.showUser) + self.show() + + def insertRow(self, data): + dbg(contents=data) + self.loanTable.insertRow(0) + self.loanTable.setItem(0, 0, QtWidgets.QTableWidgetItem(data.book.isbn)) + self.loanTable.setItem(0, 1, QtWidgets.QTableWidgetItem(data.book.signature)) + self.loanTable.setItem(0, 2, QtWidgets.QTableWidgetItem(data.book.title)) + self.loanTable.setItem(0, 3, QtWidgets.QTableWidgetItem(str(data.user_id))) + self.loanTable.setItem(0, 4, QtWidgets.QTableWidgetItem(data.loan_date)) + self.loanTable.setItem(0, 5, QtWidgets.QTableWidgetItem(data.return_date)) + self.loanTable.setItem(0, 6, QtWidgets.QTableWidgetItem(data.returned_date)) + + def loadLoans(self): + loans = self.db.getAllLoans() + for loan in loans: + self.insertRow(loan) + self.loans = loans + + def filterResults(self): + mode = ( + "all" + if self.radio_all.isChecked() + else "current" + if self.radio_current.isChecked() + else "overdue" + ) + self.loanTable.setRowCount(0) + today = QtCore.QDate.currentDate() + for loan in self.loans: + # convert string to Qdate + date = loan.return_date + qdate = stringToDate(date) + # current mode + if mode == "current": + if loan.returned == 1: + continue + elif mode == "overdue": + if loan.returned_date != "": + continue + else: + if qdate > today: + continue + self.insertRow(loan) + + def limitResults(self): + limiter = self.searchbar.text().lower() + searchfield = self.searchFields.currentText() + searchfield = TABLETOFIELDTRANSLATE[searchfield] + # dbg(limiter=limiter, search=searchfield) + self.loanTable.setRowCount(0) + for loan in self.loans: + fielddata = eval(f"loan.{searchfield}") + if isinstance(fielddata, str): + fielddata = fielddata.lower() + if limiter in fielddata: + self.insertRow(loan) + + def showUser(self): + row = self.loanTable.currentRow() + user_id = self.loanTable.item(row, 3).text() + user_id = int(user_id) + user = self.db.getUser(user_id) + self.user = UserUI(user.username, user.id, user.email) + self.user.show() + + +def launch(): + import sys + + app = QtWidgets.QApplication(sys.argv) + main_ui = LoanWindow() + # atexit.register(exit_handler) + sys.exit(app.exec()) From 541f2b8b8f28190350b16b7ff36d822feed52007 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 09:44:55 +0200 Subject: [PATCH 29/83] update db schema --- src/schemas/database.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/schemas/database.py b/src/schemas/database.py index 29e554f..e18dc07 100644 --- a/src/schemas/database.py +++ b/src/schemas/database.py @@ -16,10 +16,10 @@ LOANS = """CREATE TABLE IF NOT EXISTS loans ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, media_id INTEGER NOT NULL, -loan_date DATETIME NOT NULL, -return_date DATETIME NOT NULL, +loan_date TEXT NOT NULL, +return_date TEXT NOT NULL, returned INTEGER DEFAULT 0, -returned_date DATETIME, +returned_date TEXT, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (media_id) REFERENCES media(id)); """ From 7ea612d9ef784ce1cf30669f6cec1e0af1551cf5 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:07:35 +0200 Subject: [PATCH 30/83] current state --- src/ui/main_ui.py | 278 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 206 insertions(+), 72 deletions(-) diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index 51f53b4..7882275 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -1,31 +1,38 @@ -import ast +import sys +import atexit +from src import config +from src.logic import Database, Catalogue, Backup +from src.utils import stringToDate, Icon, Log +from src.utils import debugMessage as dbg +from src.schemas import Book from .sources.Ui_main_UserInterface import Ui_MainWindow from .user import UserUI from .createUser import CreateUser from .multiUserInfo import MultiUserFound from .newentry import NewEntry from .settings import Settings -from src import config -from src.logic import Database, Catalogue, Backup -from src.utils import stringToDate, Icon -from src.schemas import User, Book -from PyQt6 import QtCore, QtGui, QtWidgets -import sys -import atexit +from .newBook import NewBook +from .loans import LoanWindow +from .reportUi import ReportUi + +from PyQt6 import QtCore, QtWidgets backup = Backup() +cat = Catalogue() +log = Log("main") +dbg(backup=config.database.do_backup, catalogue=config.catalogue) + class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self): super(MainUI, self).__init__() self.setupUi(self) - self.setWindowTitle("Handbibliotheksleihsystem") + self.setWindowTitle(f"Handbibliotheksleihsystem {config.institution_name}") self.setWindowIcon(Icon("main").icon) self.db = Database() self.currentDate = QtCore.QDate.currentDate() self.label_7.hide() self.nextReturnDate.hide() - self.activeUser = None # add default loan duration to current date loanDate = self.currentDate.addDays(config.default_loan_duration) self.duedate.setDate(loanDate) @@ -33,8 +40,13 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.actionRueckgabemodus.triggered.connect(self.changeMode) self.actionNutzer.triggered.connect(self.showUser) self.actionEinstellungen.triggered.connect(self.showSettings) - #Buttons + self.actionAusleihistorie.triggered.connect(self.showLoanHistory) + self.actionBericht_erstellen.triggered.connect(self.generateReport) + # Buttons self.btn_show_lentmedia.clicked.connect(self.showUser) + self.btn_createNewUser.clicked.connect(self.createUser) + self.btn_createNewUser.setText("") + self.btn_createNewUser.setIcon(Icon("add_user").overwriteColor("#1E90FF")) # LineEdits self.input_userno.returnPressed.connect( @@ -44,22 +56,54 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): lambda: self.checkUser("username", self.input_username.text()) ) self.input_file_ident.returnPressed.connect(self.handleLineInput) - self.input_userno.setValidator(QtGui.QIntValidator()) - + self.input_userno.setMaxLength(40) + self.input_userno.textChanged.connect( + lambda: self.validateInput(self.input_userno.text(), "int") + ) # TableWidget # set header size to be width/number of columns self.mediaOverview.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.ResizeMode.Stretch ) + self.input_file_ident.setFocus() + # self.userdata.textChanged.connect(lambda: self.mode.setText("Ausleihe")) # self.input_userno. + # variables + self.activeUser = None + self.activeState = "Rückgabe" + if backup.backup: + log.info("Backup enabled") + else: + log.warning("Backup disabled") self.show() + def generateReport(self): + log.info("Generating Report") + report = ReportUi() + report.exec() + + def showLoanHistory(self): + log.info("Showing Loan History") + self.loan = LoanWindow() + self.loan.show() + + def validateInput(self, value, type): + lastchar = value[-1] if value else "" + # if lastchar is not of the type, remove it + if type == "int": + if not lastchar.isdigit(): + self.input_userno.setText(value[:-1]) + def showSettings(self): + log.info("Showing Settings") settings = Settings() settings.exec() def changeMode(self): + log.info("Changing Mode") + dbg(f"Current mode: {self.mode.text()}") + self.mode.setText("Rückgabe") self.input_username.clear() self.input_userno.clear() @@ -68,10 +112,13 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_file_ident.clear() self.label_7.hide() self.nextReturnDate.hide() + self.mediaOverview.setRowCount(0) def showUser(self): + log.info(f"Showing User {self.activeUser}") if self.activeUser is None: # create warning dialog + log.info("Showing no user selected warning") dialog = QtWidgets.QMessageBox() dialog.setWindowTitle("Kein Nutzer ausgewählt") dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning) @@ -87,81 +134,156 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.user_ui.show() def setUserData(self): + log.info("Setting User Data") self.input_username.setText(self.activeUser.username) self.input_userno.setText(str(self.activeUser.id)) self.userdata.setText(self.activeUser.__repr__()) + self.mode.setText("Ausleihe") + + def createUser(self): + log.info("Creating User") + user = CreateUser(fieldname="id", data="") + user.exec() + userid = user.userid + if userid: + log.info(f"User created {userid}") + data = self.db.getUser(userid) + self.activeUser = data + # set user to active user + self.setUserData() + + self.input_file_ident.setFocus() + + return def checkUser(self, fieldname, data): - print("Checking User", fieldname, data) + log.info(f"Checking User {fieldname}, {data}") + # print("Checking User", fieldname, data) # set fieldname as key and data as variable user = self.db.checkUserExists(fieldname, data) if not user: - user = CreateUser(fieldname, data) - user.exec() - userid = user.userid - if userid: - data = self.db.getUser(userid) - self.activeUser = data + warning = QtWidgets.QMessageBox() + warning.setWindowTitle("Nutzer nicht gefunden") + warning.setIcon(QtWidgets.QMessageBox.Icon.Warning) + warning.setWindowIcon(Icon("warning").overwriteColor("#EA3323")) + warning.setText("Nutzer nicht gefunden, bitte erst anlegen") + warning.exec() + self.input_username.clear() + self.input_userno.clear() + return else: if len(user) > 1: + log.info("Multiple Users found") multi = MultiUserFound(user) multi.exec() self.activeUser = multi.userdata else: + # print("User found", user[0]) self.activeUser = user[0] if self.activeUser is not None: - print("User found", self.activeUser) + log.info(f"User found {self.activeUser}") + # print("User found", self.activeUser) self.setUserData() self.input_file_ident.setFocus() self.mode.setText("Ausleihe") self.btn_show_lentmedia.setText(self.db.getActiveLoans(self.activeUser.id)) retdate = self.db.selectClosestReturnDate(self.activeUser.id) if retdate: - date = stringToDate(retdate) + date = stringToDate(retdate).toString("dd.MM.yyyy") self.nextReturnDate.setText(date) self.nextReturnDate.show() self.label_7.show() def moveToLine(self, line): + log.debug("Moving to Line", line) line.setFocus() def handleLineInput(self): value = self.input_file_ident.text().strip() - if len(value) <= 2: - self.callShortcut(value) + log.debug(f"Handling Line Input {value}") + if self.mode.text() == "Rückgabe": + self.returnMedia(value) else: - if self.mode.text() == "Rückgabe": - self.returnMedia(value) - else: - self.mediaAdd(value) + if not " " in value: + # create warning dialog + log.info("Invalid Input") + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Ungültige Eingabe") + dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning) + dialog.setWindowIcon(Icon("warning").overwriteColor("#EA3323")) + dialog.setText( + "Die Eingabe enthält kein Leerzeichen\nBitte prüfen und erneut eingeben" + ) + dialog.exec() + return + self.mediaAdd(value) def mediaAdd(self, identifier): self.clearStatusTip - print("Adding Media", identifier) + # print("Adding Media", identifier) self.setStatusTip("") self.input_file_ident.clear() self.input_file_ident.setEnabled(False) user_id = self.activeUser.id - cat = Catalogue() - media = cat.get_book(identifier) - print(media) + media = Book(signature=identifier) book_id = self.db.checkMediaExists(media) - print(book_id) + dbg(f"Book ID: {book_id}, User ID: {user_id}", media=media) + if not book_id: + dbg("Book not found, searching catalogue") + if config.catalogue == True: + media = cat.get_book(identifier) + if not media: + self.setStatusTipMessage("Buch nicht gefunden") + self.input_file_ident.setEnabled(True) + return + book_id = self.db.insertMedia(media) + # self.db.insertLoan( + # userid=user_id, + # mediaid=book_id, + # loandate=self.currentDate.toString("yyyy-MM-dd"), + # duedate=self.duedate.date().toString("yyyy-MM-dd"), + # ) + # self.mediaOverview.insertRow(0) + # self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(media.isbn)) + # self.mediaOverview.setItem( + # 0, 1, QtWidgets.QTableWidgetItem(media.title) + # ) + # self.mediaOverview.setItem( + # 0, 2, QtWidgets.QTableWidgetItem("Entliehen") + # ) + # return + else: + newbook = NewBook() + newbook.exec() + if newbook.result() == 1: + media = newbook.book + book_id = self.db.insertMedia(media) + # self.db.insertLoan( + # userid=user_id, + # mediaid=book_id, + # loandate=self.currentDate.toString(), + # duedate=self.duedate.date().toString(), + # ) + if book_id: - if len(book_id) > 1: - print("Multiple Books found") + if isinstance(book_id, list) and len(book_id) > 1: + # print("Multiple Books found") # TODO: implement book selection dialog return else: + if isinstance(book_id, int): + book_id = [book_id] # check if book is already loaned loaned = self.db.checkLoanState(book_id[0]) if loaned: - print("Book already loaned") + # print("Book already loaned") self.setStatusTipMessage("Buch bereits entliehen") # dialog with yes no to create new entry dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Buch bereits entliehen") + dialog.setWindowIcon(Icon("duplicate").icon) dialog.setText( "Buch bereits entliehen, soll ein neues hinzugefügt werden?" ) @@ -173,22 +295,22 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): dialog.exec() result = dialog.result() if result == QtWidgets.QMessageBox.StandardButton.No: + self.input_file_ident.setEnabled(True) return newentry = NewEntry([book_id[0]]) newentry.exec() self.setStatusTipMessage("Neues Exemplar hinzugefügt") - created_ids = newentry.newIds - print(created_ids) + # print(created_ids) self.input_file_ident.setEnabled(True) return else: - print("Book not loaned, loaning now") + # print("Book not loaned, loaning now") self.db.insertLoan( user_id, book_id[0], - self.currentDate.toString(), - self.duedate.date().toString(), + self.currentDate.toString("yyyy-MM-dd"), + self.duedate.date().toString("yyyy-MM-dd"), ) self.mediaOverview.insertRow(0) self.mediaOverview.setItem( @@ -200,48 +322,39 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.mediaOverview.setItem( 0, 2, QtWidgets.QTableWidgetItem("Entliehen") ) - else: - book_id = self.db.insertMedia(media) - self.db.insertLoan( - userid=user_id, - mediaid=book_id, - loandate=self.currentDate.toString(), - duedate=self.duedate.date().toString(), - ) + # else: self.btn_show_lentmedia.setText(self.db.getActiveLoans(self.activeUser.id)) self.nextReturnDate.setText( - stringToDate(self.db.selectClosestReturnDate(self.activeUser.id)) + stringToDate(self.db.selectClosestReturnDate(self.activeUser.id)).toString( + "dd.MM.yyyy" + ) ) self.nextReturnDate.show() self.label_7.show() self.input_file_ident.setEnabled(True) - def callShortcut(self, shortcut): - print("Calling Shortcut", shortcut) - # check if actions have shortcut key and call them - sysem_shortcuts = None - print(sysem_shortcuts) - - def returnMedia(self, identifier): - print("Returning Media", identifier) + # print("Returning Media", identifier) # get book id from database + # self. identifier = Book( isbn=identifier, title=identifier, signature=identifier, ppn=identifier ) book_id = self.db.checkMediaExists(identifier) - print(book_id) + # print(book_id) if book_id: # check if book is already loaned loaned = self.db.checkLoanState(book_id[0]) if loaned: - print("Book already loaned, returning now") + # print("Book already loaned, returning now") user = self.db.getUserByLoan(book_id[0]) # set userdata in lineedits self.activeUser = user self.setUserData() - book = self.db.returnMedia(book_id[0], self.currentDate.toString()) + book = self.db.returnMedia( + book_id[0], self.currentDate.toString("yyyy-MM-dd") + ) self.mediaOverview.insertRow(0) self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(book.isbn)) self.mediaOverview.setItem(0, 1, QtWidgets.QTableWidgetItem(book.title)) @@ -254,7 +367,9 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): ) # else: - print("Book not loaned") + # print("Book not loaned") + self.setStatusTipMessage("Buch nicht entliehen") + self.input_file_ident.clear() else: print("Book not found") @@ -265,19 +380,38 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def clearStatusTip(self): self.setStatusTip("") + def exit_handler(): - print("Exiting") - state = backup.createBackup() - # create dialog to show state + dbg("Exiting, creating backup") app = QtWidgets.QApplication(sys.argv) - dialog = QtWidgets.QMessageBox() - if state == True: - dialog.setText("Backup created successfully") + if config.database.do_backup: + state = backup.createBackup() + # create dialog to show state + if state == True: + return + else: + dialog = QtWidgets.QMessageBox() + # set icon + dialog.setWindowIcon(Icon("backup").icon) + dialog.setWindowTitle("Backup") + dialog.setText("Backup konnte nicht erstellt werden") + + dialog.exec() + dbg("Exiting", backupstate=state) else: - dialog.setText("Backup creation failed") - dialog.exec() - - + dialog = QtWidgets.QMessageBox() + # set icon + reason = ( + "Backup deaktiviert" + if config.database.do_backup is False + else "Backuppfad nicht gefunden" + if not backup.backup + else "Unbekannter Fehler" + ) + dialog.setWindowIcon(Icon("backup").icon) + dialog.setWindowTitle("Backup nicht möglich") + dialog.setText("Backup konnte nicht erstellt werden\nGrund: {}".format(reason)) + dialog.exec() def launch(): app = QtWidgets.QApplication(sys.argv) main_ui = MainUI() From f1a33e7ea8ba372b012cae3e71a186c91261cd41 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:08:41 +0200 Subject: [PATCH 31/83] changes --- config/settings.yaml | 5 ++--- icons/calendar_event.svg | 1 + icons/duplicate.svg | 1 + icons/icons.yaml | 4 +++- src/__init__.py | 13 ++++++++++++- src/logic/catalogue.py | 17 ++++++++++++++++- src/ui/newentry.py | 4 ++-- src/utils/__init__.py | 4 +++- 8 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 icons/calendar_event.svg create mode 100644 icons/duplicate.svg diff --git a/config/settings.yaml b/config/settings.yaml index 63a192e..3998a38 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -1,14 +1,13 @@ institution_name: HB Testbibliothek Psychologie default_loan_duration: 7 database: - path: C:/newestdb_mew - name: libraries.db + path: C:/testdatabase_1 + name: libr.db backupLocation: V:/backup do_backup: false report: generate_report: false email: None - debug: false log_debug: false catalogue: True diff --git a/icons/calendar_event.svg b/icons/calendar_event.svg new file mode 100644 index 0000000..87dd2e3 --- /dev/null +++ b/icons/calendar_event.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/duplicate.svg b/icons/duplicate.svg new file mode 100644 index 0000000..4d5cc44 --- /dev/null +++ b/icons/duplicate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/icons.yaml b/icons/icons.yaml index 68e6968..10ed483 100644 --- a/icons/icons.yaml +++ b/icons/icons.yaml @@ -10,4 +10,6 @@ icons: borrow: book.svg backup: db_backup.svg addBook: add_book.svg - report: report.svg \ No newline at end of file + report: report.svg + duplicate: duplicate.svg + loan_extend: calendar_event.svg \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py index 0fc8435..9a407eb 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,2 +1,13 @@ import omegaconf -config = omegaconf.OmegaConf.load("config/settings.yaml") \ No newline at end of file +import sys + +__version__ = "0.0.1" +__author__ = "Alexander Kirchner" + +config = omegaconf.OmegaConf.load("config/settings.yaml") + +# if programm launched with argument --debug, set debug to True +if "--debug" in sys.argv: + config.debug = True +if "--log" in sys.argv: + config.log_debug = True \ No newline at end of file diff --git a/src/logic/catalogue.py b/src/logic/catalogue.py index a29756b..895a772 100644 --- a/src/logic/catalogue.py +++ b/src/logic/catalogue.py @@ -2,15 +2,28 @@ import requests from bs4 import BeautifulSoup from src import config from src.schemas import Book - +from src.utils import Log URL = 'https://rds.ibs-bw.de/phfreiburg/opac/RDSIndex/Search?lookfor="{}"+&type=AllFields&limit=10&sort=py+desc%2C+title' BASE = "https://rds.ibs-bw.de" +log = Log("Catalogue") class Catalogue: def __init__(self, timeout=5): self.timeout = timeout + reachable = self.check_connection() + if reachable: + config.catalogue = True + else: + config.catalogue = False + def check_connection(self): + try: + response = requests.get("https://www.google.com", timeout=self.timeout) + if response.status_code == 200: + return True + except requests.exceptions.RequestException as e: + log.error(f"Could not connect to google.com: {e}") def search_book(self, searchterm: str): response = requests.get(URL.format(searchterm), timeout=self.timeout) return response.text @@ -29,6 +42,8 @@ class Catalogue: return res def get_book(self, searchterm: str): + log.info(f"Searching for term: {searchterm}") + links = self.get_book_links(searchterm) for link in links: result = self.search(link) diff --git a/src/ui/newentry.py b/src/ui/newentry.py index 2c6a2e6..3a7b437 100644 --- a/src/ui/newentry.py +++ b/src/ui/newentry.py @@ -40,7 +40,7 @@ class NewEntry(QtWidgets.QDialog, Ui_Dialog): ) def populateTable(self): for title in self.titles: - print(title) + # print(title) entries = self.db.getMediaSimilarSignatureByID(title) # sort by signature entries.sort(key=lambda x: x.signature, reverse=True) @@ -66,7 +66,7 @@ class NewEntry(QtWidgets.QDialog, Ui_Dialog): signature=signature, ppn=eval(ppn), ) - print(book) + # print(book) if not self.db.checkMediaExists(book): newBookId = self.db.insertMedia(book) self.newIds.append(newBookId) diff --git a/src/utils/__init__.py b/src/utils/__init__.py index afc91ca..ea00422 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,2 +1,4 @@ -from .stringtodate import stringToDate +from .log import Log from .icon import Icon +from .debug import debugMessage +from .stringtodate import stringToDate \ No newline at end of file From e1658644ab12427fed26d9a621f0a38a06875c4e Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:07:44 +0200 Subject: [PATCH 32/83] rework loan system to use f5 to change mode to avoid confusion --- src/ui/main_ui.py | 109 ++++++------ src/ui/sources/Ui_main_UserInterface.py | 65 ++++--- src/ui/sources/main_UserInterface.ui | 223 ++++++++++++++---------- 3 files changed, 226 insertions(+), 171 deletions(-) diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index 7882275..c1d3bb8 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -29,13 +29,9 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.setupUi(self) self.setWindowTitle(f"Handbibliotheksleihsystem {config.institution_name}") self.setWindowIcon(Icon("main").icon) - self.db = Database() - self.currentDate = QtCore.QDate.currentDate() self.label_7.hide() self.nextReturnDate.hide() # add default loan duration to current date - loanDate = self.currentDate.addDays(config.default_loan_duration) - self.duedate.setDate(loanDate) # hotkeys self.actionRueckgabemodus.triggered.connect(self.changeMode) self.actionNutzer.triggered.connect(self.showUser) @@ -60,6 +56,9 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_userno.textChanged.connect( lambda: self.validateInput(self.input_userno.text(), "int") ) + self.input_username.setEnabled(False) + self.input_userno.setEnabled(False) + self.duedate.setEnabled(False) # TableWidget # set header size to be width/number of columns self.mediaOverview.horizontalHeader().setSectionResizeMode( @@ -69,9 +68,16 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): # self.userdata.textChanged.connect(lambda: self.mode.setText("Ausleihe")) # self.input_userno. # variables + self.db = Database() + self.currentDate = QtCore.QDate.currentDate() + loanDate = self.currentDate.addDays(config.default_loan_duration) self.activeUser = None self.activeState = "Rückgabe" + self.duedate.setDate(loanDate) + # functions + self.activateReturnMode() + if backup.backup: log.info("Backup enabled") else: @@ -102,17 +108,41 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def changeMode(self): log.info("Changing Mode") - dbg(f"Current mode: {self.mode.text()}") - - self.mode.setText("Rückgabe") + dbg(f"Current mode: {self.activeState}") self.input_username.clear() - self.input_userno.clear() self.userdata.clear() + self.input_userno.clear() self.btn_show_lentmedia.setText("") self.input_file_ident.clear() self.label_7.hide() self.nextReturnDate.hide() self.mediaOverview.setRowCount(0) + if self.activeState == "Rückgabe": + self.activateLoanMode() + else: + self.activateReturnMode() + + def activateLoanMode(self): + dbg("Activating Loan Mode") + self.input_username.setEnabled(True) + self.input_userno.setEnabled(True) + self.duedate.setEnabled(True) + self.input_userno.setFocus() + # set mode background color to blue with rounded edges + # self.mode.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.mode.setStyleSheet("background-color: #1E90FF") + self.mode.setText("Ausleihe") + self.activeState = "Ausleihe" + + def activateReturnMode(self): + dbg("Activating Return Mode") + self.input_username.setEnabled(False) + self.input_userno.setEnabled(False) + # set mode background color to orange + self.mode.setStyleSheet("background-color: #FFA500") + self.duedate.setEnabled(False) + self.activeState = "Rückgabe" + self.mode.setText("Rückgabe") def showUser(self): log.info(f"Showing User {self.activeUser}") @@ -138,7 +168,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_username.setText(self.activeUser.username) self.input_userno.setText(str(self.activeUser.id)) self.userdata.setText(self.activeUser.__repr__()) - self.mode.setText("Ausleihe") + # self.mode.setText("Ausleihe") def createUser(self): log.info("Creating User") @@ -151,6 +181,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.activeUser = data # set user to active user self.setUserData() + self.activateLoanMode() self.input_file_ident.setFocus() @@ -239,35 +270,15 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_file_ident.setEnabled(True) return book_id = self.db.insertMedia(media) - # self.db.insertLoan( - # userid=user_id, - # mediaid=book_id, - # loandate=self.currentDate.toString("yyyy-MM-dd"), - # duedate=self.duedate.date().toString("yyyy-MM-dd"), - # ) - # self.mediaOverview.insertRow(0) - # self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(media.isbn)) - # self.mediaOverview.setItem( - # 0, 1, QtWidgets.QTableWidgetItem(media.title) - # ) - # self.mediaOverview.setItem( - # 0, 2, QtWidgets.QTableWidgetItem("Entliehen") - # ) - # return + self.loanMedia(user_id, [book_id], media) else: newbook = NewBook() newbook.exec() if newbook.result() == 1: media = newbook.book book_id = self.db.insertMedia(media) - # self.db.insertLoan( - # userid=user_id, - # mediaid=book_id, - # loandate=self.currentDate.toString(), - # duedate=self.duedate.date().toString(), - # ) - if book_id: + elif book_id: if isinstance(book_id, list) and len(book_id) > 1: # print("Multiple Books found") # TODO: implement book selection dialog @@ -302,28 +313,28 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.setStatusTipMessage("Neues Exemplar hinzugefügt") # print(created_ids) self.input_file_ident.setEnabled(True) - + newentries = newentry.newIds + if newentries: + for entry in newentries: + book = self.db.getMedia(entry) + self.loanMedia(user_id, [entry], book) + dbg("inserted duplicated book into database") return else: # print("Book not loaned, loaning now") - self.db.insertLoan( - user_id, - book_id[0], - self.currentDate.toString("yyyy-MM-dd"), - self.duedate.date().toString("yyyy-MM-dd"), - ) - self.mediaOverview.insertRow(0) - self.mediaOverview.setItem( - 0, 0, QtWidgets.QTableWidgetItem(media.isbn) - ) - self.mediaOverview.setItem( - 0, 1, QtWidgets.QTableWidgetItem(media.title) - ) - self.mediaOverview.setItem( - 0, 2, QtWidgets.QTableWidgetItem("Entliehen") - ) - # else: + self.loanMedia(user_id, book_id, media) + def loanMedia(self, user_id, book_id, media): + self.db.insertLoan( + user_id, + book_id[0], + self.currentDate.toString("yyyy-MM-dd"), + self.duedate.date().toString("yyyy-MM-dd"), + ) + self.mediaOverview.insertRow(0) + self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(media.signature)) + self.mediaOverview.setItem(0, 1, QtWidgets.QTableWidgetItem(media.title)) + self.mediaOverview.setItem(0, 2, QtWidgets.QTableWidgetItem("Entliehen")) self.btn_show_lentmedia.setText(self.db.getActiveLoans(self.activeUser.id)) self.nextReturnDate.setText( stringToDate(self.db.selectClosestReturnDate(self.activeUser.id)).toString( diff --git a/src/ui/sources/Ui_main_UserInterface.py b/src/ui/sources/Ui_main_UserInterface.py index c929a66..15ce2ca 100644 --- a/src/ui/sources/Ui_main_UserInterface.py +++ b/src/ui/sources/Ui_main_UserInterface.py @@ -19,6 +19,22 @@ class Ui_MainWindow(object): self.verticalLayout.setObjectName("verticalLayout") self.gridLayout = QtWidgets.QGridLayout() self.gridLayout.setObjectName("gridLayout") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.duedate = QtWidgets.QDateEdit(parent=self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.duedate.sizePolicy().hasHeightForWidth()) + self.duedate.setSizePolicy(sizePolicy) + self.duedate.setMinimumSize(QtCore.QSize(130, 0)) + self.duedate.setMaximumSize(QtCore.QSize(100, 16777215)) + self.duedate.setBaseSize(QtCore.QSize(70, 0)) + self.duedate.setObjectName("duedate") + self.horizontalLayout_4.addWidget(self.duedate) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_4.addItem(spacerItem) + self.gridLayout.addLayout(self.horizontalLayout_4, 5, 1, 1, 1) self.label_3 = QtWidgets.QLabel(parent=self.centralwidget) self.label_3.setObjectName("label_3") self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1) @@ -36,8 +52,19 @@ class Ui_MainWindow(object): self.label_5.setObjectName("label_5") self.horizontalLayout.addWidget(self.label_5) self.mode = QtWidgets.QLabel(parent=self.centralwidget) - self.mode.setFrameShape(QtWidgets.QFrame.Shape.Box) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.mode.sizePolicy().hasHeightForWidth()) + self.mode.setSizePolicy(sizePolicy) + self.mode.setMinimumSize(QtCore.QSize(62, 0)) + self.mode.setMaximumSize(QtCore.QSize(62, 16777215)) + self.mode.setBaseSize(QtCore.QSize(62, 0)) + self.mode.setAutoFillBackground(False) + self.mode.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.mode.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) self.mode.setLineWidth(2) + self.mode.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.mode.setObjectName("mode") self.horizontalLayout.addWidget(self.mode) self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) @@ -50,36 +77,20 @@ class Ui_MainWindow(object): self.input_file_ident = QtWidgets.QLineEdit(parent=self.centralwidget) self.input_file_ident.setObjectName("input_file_ident") self.gridLayout.addWidget(self.input_file_ident, 3, 1, 1, 1) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_3.addItem(spacerItem1) + self.btn_createNewUser = QtWidgets.QPushButton(parent=self.centralwidget) + self.btn_createNewUser.setObjectName("btn_createNewUser") + self.horizontalLayout_3.addWidget(self.btn_createNewUser) + self.gridLayout.addLayout(self.horizontalLayout_3, 0, 1, 1, 1) self.label_2 = QtWidgets.QLabel(parent=self.centralwidget) self.label_2.setObjectName("label_2") self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) self.input_userno = QtWidgets.QLineEdit(parent=self.centralwidget) self.input_userno.setObjectName("input_userno") self.gridLayout.addWidget(self.input_userno, 1, 1, 1, 1) - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_3.addItem(spacerItem) - self.btn_createNewUser = QtWidgets.QPushButton(parent=self.centralwidget) - self.btn_createNewUser.setObjectName("btn_createNewUser") - self.horizontalLayout_3.addWidget(self.btn_createNewUser) - self.gridLayout.addLayout(self.horizontalLayout_3, 0, 1, 1, 1) - self.horizontalLayout_4 = QtWidgets.QHBoxLayout() - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.duedate = QtWidgets.QDateEdit(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.duedate.sizePolicy().hasHeightForWidth()) - self.duedate.setSizePolicy(sizePolicy) - self.duedate.setMinimumSize(QtCore.QSize(130, 0)) - self.duedate.setMaximumSize(QtCore.QSize(100, 16777215)) - self.duedate.setBaseSize(QtCore.QSize(70, 0)) - self.duedate.setObjectName("duedate") - self.horizontalLayout_4.addWidget(self.duedate) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_4.addItem(spacerItem1) - self.gridLayout.addLayout(self.horizontalLayout_4, 5, 1, 1, 1) self.verticalLayout.addLayout(self.gridLayout) self.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget) self.groupBox.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) @@ -187,14 +198,14 @@ class Ui_MainWindow(object): self.label_5.setText(_translate("MainWindow", "Modus")) self.mode.setText(_translate("MainWindow", "Rückgabe")) self.label.setText(_translate("MainWindow", "Matrikelnummer")) - self.label_2.setText(_translate("MainWindow", "Benutzername")) self.btn_createNewUser.setText(_translate("MainWindow", "Neuen Nutzer anlegen")) + self.label_2.setText(_translate("MainWindow", "Benutzername")) self.groupBox.setTitle(_translate("MainWindow", "Nutzerdaten")) self.groupBox_2.setTitle(_translate("MainWindow", "Ausleihdaten")) self.label_4.setText(_translate("MainWindow", "Anzahl Ausleihen")) self.label_7.setText(_translate("MainWindow", "Nächstes Rückgabedatum")) item = self.mediaOverview.horizontalHeaderItem(0) - item.setText(_translate("MainWindow", "ISBN")) + item.setText(_translate("MainWindow", "Signatur")) item = self.mediaOverview.horizontalHeaderItem(1) item.setText(_translate("MainWindow", "Titel")) item = self.mediaOverview.horizontalHeaderItem(2) diff --git a/src/ui/sources/main_UserInterface.ui b/src/ui/sources/main_UserInterface.ui index 32334b0..dca3a46 100644 --- a/src/ui/sources/main_UserInterface.ui +++ b/src/ui/sources/main_UserInterface.ui @@ -17,100 +17,6 @@ - - - - Suchbegriff - - - - - - - Ausleihe bis - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - - - 14 - true - - - - Modus - - - - - - - QFrame::Box - - - 2 - - - Rückgabe - - - - - - - - - Matrikelnummer - - - - - - - - - - - - - Benutzername - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Neuen Nutzer anlegen - - - - - @@ -156,6 +62,133 @@ + + + + Suchbegriff + + + + + + + Ausleihe bis + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + + 14 + true + + + + Modus + + + + + + + + 0 + 0 + + + + + 62 + 0 + + + + + 62 + 16777215 + + + + + 62 + 0 + + + + false + + + QFrame::StyledPanel + + + QFrame::Sunken + + + 2 + + + Rückgabe + + + Qt::AlignCenter + + + + + + + + + Matrikelnummer + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Neuen Nutzer anlegen + + + + + + + + + Benutzername + + + + + + @@ -237,7 +270,7 @@ - ISBN + Signatur From 27ec7c296abd82572c87f38b0eb63102ad1464e5 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:29:16 +0200 Subject: [PATCH 33/83] add error icon, statustip -> dialog --- config/settings.yaml | 4 ++-- icons/error.svg | 1 + icons/icons.yaml | 21 +++++++++++---------- src/ui/main_ui.py | 17 +++++++---------- 4 files changed, 21 insertions(+), 22 deletions(-) create mode 100644 icons/error.svg diff --git a/config/settings.yaml b/config/settings.yaml index 3998a38..c1682f4 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -1,8 +1,9 @@ institution_name: HB Testbibliothek Psychologie default_loan_duration: 7 +catalogue: True database: path: C:/testdatabase_1 - name: libr.db + name: librr.db backupLocation: V:/backup do_backup: false report: @@ -10,4 +11,3 @@ report: email: None debug: false log_debug: false -catalogue: True diff --git a/icons/error.svg b/icons/error.svg new file mode 100644 index 0000000..5ed37a1 --- /dev/null +++ b/icons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/icons.yaml b/icons/icons.yaml index 10ed483..306f78f 100644 --- a/icons/icons.yaml +++ b/icons/icons.yaml @@ -1,15 +1,16 @@ color: '#B89230' #Hex code of the color icons: - newentry: library_add.svg + addBook: add_book.svg + add_user: add_user.svg + backup: db_backup.svg + borrow: book.svg + duplicate: duplicate.svg + error: error.svg + loan_extend: calendar_event.svg main: library.svg - warning: warning.svg + multiuser: multiple_user.svg + newentry: library_add.svg + report: report.svg settings: settings.svg user: user.svg - multiuser: multiple_user.svg - add_user: add_user.svg - borrow: book.svg - backup: db_backup.svg - addBook: add_book.svg - report: report.svg - duplicate: duplicate.svg - loan_extend: calendar_event.svg \ No newline at end of file + warning: warning.svg \ No newline at end of file diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index c1d3bb8..da9412f 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -251,9 +251,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.mediaAdd(value) def mediaAdd(self, identifier): - self.clearStatusTip # print("Adding Media", identifier) - self.setStatusTip("") self.input_file_ident.clear() self.input_file_ident.setEnabled(False) @@ -385,13 +383,12 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): print("Book not found") def setStatusTipMessage(self, message): - self.setStatusTip(message) - - @property - def clearStatusTip(self): - self.setStatusTip("") - - + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Fehler") + dialog.setIcon(QtWidgets.QMessageBox.Icon.Information) + dialog.setWindowIcon(Icon("error").overwriteColor("#EA3323")) + dialog.setText(message) + dialog.exec() def exit_handler(): dbg("Exiting, creating backup") app = QtWidgets.QApplication(sys.argv) @@ -426,6 +423,6 @@ def exit_handler(): def launch(): app = QtWidgets.QApplication(sys.argv) main_ui = MainUI() - # atexit.register(exit_handler) + atexit.register(exit_handler) sys.exit(app.exec()) # sys.exit(app.exec()) From c3ff3e93ee570ff99d50f6a0c5f9617128142da7 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:10:02 +0200 Subject: [PATCH 34/83] bugfixes --- src/schemas/loan.py | 1 + src/ui/createUser.py | 18 ++++++++++++---- src/ui/loans.py | 43 +++++++++++++++++++++++++++++++-------- src/ui/main_ui.py | 2 +- src/ui/reportUi.py | 3 +-- src/ui/user.py | 2 +- src/utils/reportThread.py | 10 +++++---- src/utils/stringtodate.py | 2 +- 8 files changed, 59 insertions(+), 22 deletions(-) diff --git a/src/schemas/loan.py b/src/schemas/loan.py index fe0c944..1e4c8cf 100644 --- a/src/schemas/loan.py +++ b/src/schemas/loan.py @@ -12,3 +12,4 @@ class Loan: returned: int returned_date: str book: Book + user_name: str \ No newline at end of file diff --git a/src/ui/createUser.py b/src/ui/createUser.py index a91c5a4..b90cd97 100644 --- a/src/ui/createUser.py +++ b/src/ui/createUser.py @@ -3,8 +3,6 @@ from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtGui import QRegularExpressionValidator from src.logic import Database from src.utils import Icon, Log -from src.schemas import User -import re class CreateUser(QtWidgets.QDialog, Ui_Dialog): def __init__(self, fieldname, data): @@ -58,8 +56,20 @@ class CreateUser(QtWidgets.QDialog, Ui_Dialog): username = self.username.text() userno = int(self.userno.text()) usermail = self.user_mail.text() - self.db.insertUser(username, userno, usermail) - self.userid = userno + if self.db.insertUser(username, userno, usermail): + self.userid = userno + else: + self.setStatusTipMessage( + "Benutzer konnte nicht erstellt werden, bitte überprüfen Sie die Eingaben" + ) + + def setStatusTipMessage(self, message): + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Information") + dialog.setIcon(QtWidgets.QMessageBox.Icon.Information) + dialog.setWindowIcon(Icon("error").overwriteColor("#EA3323")) + dialog.setText(message) + dialog.exec() def validateInputUserno(self, value, type): lastchar = value[-1] if value else "" diff --git a/src/ui/loans.py b/src/ui/loans.py index 3032699..f50488b 100644 --- a/src/ui/loans.py +++ b/src/ui/loans.py @@ -9,7 +9,7 @@ from icecream import ic TABLETOFIELDTRANSLATE = { "Titel": "book.title", "Signatur": "book.signature", - "Nutzer": "user_id", + "Nutzer": "user_name", } @@ -28,6 +28,7 @@ class LoanWindow(QtWidgets.QMainWindow, Ui_MainWindow): # lineedits self.searchbar.textChanged.connect(self.limitResults) + self.searchbar.returnPressed.connect(self.passThis) # radio buttons self.radio_all.clicked.connect(self.filterResults) @@ -38,16 +39,40 @@ class LoanWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.loanTable.doubleClicked.connect(self.showUser) self.show() + def passThis(self): + pass + def insertRow(self, data): dbg(contents=data) + retdate = ( + stringToDate(data.return_date).toString("dd.MM.yyyy") + if data.return_date != "" + else "" + ) self.loanTable.insertRow(0) self.loanTable.setItem(0, 0, QtWidgets.QTableWidgetItem(data.book.isbn)) self.loanTable.setItem(0, 1, QtWidgets.QTableWidgetItem(data.book.signature)) self.loanTable.setItem(0, 2, QtWidgets.QTableWidgetItem(data.book.title)) - self.loanTable.setItem(0, 3, QtWidgets.QTableWidgetItem(str(data.user_id))) - self.loanTable.setItem(0, 4, QtWidgets.QTableWidgetItem(data.loan_date)) - self.loanTable.setItem(0, 5, QtWidgets.QTableWidgetItem(data.return_date)) - self.loanTable.setItem(0, 6, QtWidgets.QTableWidgetItem(data.returned_date)) + self.loanTable.setItem( + 0, + 3, + QtWidgets.QTableWidgetItem(str(self.db.getUser(data.user_id).username)), + ) + self.loanTable.setItem( + 0, + 4, + QtWidgets.QTableWidgetItem( + stringToDate(data.loan_date).toString("dd.MM.yyyy") + ), + ) + self.loanTable.setItem( + 0, + 5, + QtWidgets.QTableWidgetItem( + stringToDate(data.return_date).toString("dd.MM.yyyy") + ), + ) + self.loanTable.setItem(0, 6, QtWidgets.QTableWidgetItem(retdate)) def loadLoans(self): loans = self.db.getAllLoans() @@ -83,9 +108,10 @@ class LoanWindow(QtWidgets.QMainWindow, Ui_MainWindow): def limitResults(self): limiter = self.searchbar.text().lower() + limiter = str(limiter) searchfield = self.searchFields.currentText() searchfield = TABLETOFIELDTRANSLATE[searchfield] - # dbg(limiter=limiter, search=searchfield) + dbg(limiter=limiter, search=searchfield) self.loanTable.setRowCount(0) for loan in self.loans: fielddata = eval(f"loan.{searchfield}") @@ -96,9 +122,8 @@ class LoanWindow(QtWidgets.QMainWindow, Ui_MainWindow): def showUser(self): row = self.loanTable.currentRow() - user_id = self.loanTable.item(row, 3).text() - user_id = int(user_id) - user = self.db.getUser(user_id) + user_name = self.loanTable.item(row, 3).text() + user = self.db.getUserId(user_name) self.user = UserUI(user.username, user.id, user.email) self.user.show() diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index da9412f..ff766a5 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -384,7 +384,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def setStatusTipMessage(self, message): dialog = QtWidgets.QMessageBox() - dialog.setWindowTitle("Fehler") + dialog.setWindowTitle("Information") dialog.setIcon(QtWidgets.QMessageBox.Icon.Information) dialog.setWindowIcon(Icon("error").overwriteColor("#EA3323")) dialog.setText(message) diff --git a/src/ui/reportUi.py b/src/ui/reportUi.py index e68bc3e..9a1695c 100644 --- a/src/ui/reportUi.py +++ b/src/ui/reportUi.py @@ -29,7 +29,7 @@ class ReportUi(QtWidgets.QDialog, Ui_Dialog): self.radio_month.clicked.connect(self.set_days_by_radio) self.radio_week.clicked.connect(self.set_days_by_radio) self.format_txt.clicked.connect(lambda: self.rthread.setFormat("txt")) - self.format_csv.clicked.connect(lambda: self.rthread.setFormat("csv")) + self.format_csv.clicked.connect(lambda: self.rthread.setFormat("tsv")) self.format_csv.clicked.connect(lambda: self.generateReport.setEnabled(True)) self.format_txt.clicked.connect(lambda: self.generateReport.setEnabled(True)) # sliders @@ -59,7 +59,6 @@ class ReportUi(QtWidgets.QDialog, Ui_Dialog): self.dayValue.setText(str(value)) def generate_report(self): - print(self.days) self.rthread.setDays(self.days) self.rthread.report_signal.connect(self.report_generated) self.rthread.report_nums_signal.connect(self.show_progress) diff --git a/src/ui/user.py b/src/ui/user.py index 86ca5b2..1176021 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -57,7 +57,7 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): extend = ExtendLoan(self.username, self.userMedia) extend.exec() if extend.result() == 1: - extendDate = extend.extendDate.toString() + extendDate = extend.extendDate.toString("yyyy-MM-dd") # # print columns of selected rows for item in self.UserMediaTable.selectedItems(): if item.column() == 1: diff --git a/src/utils/reportThread.py b/src/utils/reportThread.py index ac83103..b5eff7a 100644 --- a/src/utils/reportThread.py +++ b/src/utils/reportThread.py @@ -27,7 +27,6 @@ class ReportThread(QThread): path = db.db_path day = QDate.currentDate().addDays(-self.days).toString("yyyy-MM-dd") query = f"""SELECT * FROM loans WHERE loan_date >= '{day}';""" - print(query) colnames = ["UserId", "Title", "Action", "Datum"] table = PrettyTable(colnames) table.align[colnames[0]] = "l" @@ -57,9 +56,12 @@ class ReportThread(QThread): ) # # print(table) # # wruitng the table to a file - if self.format == "csv": - with open("report.csv", "w", encoding="utf-8") as f: - f.write(table.get_csv_string()) + if self.format == "tsv": + table = table.get_csv_string() + tsv_table = table.replace(",", "\t") + # write the file + with open("report.tsv", "w", encoding="utf-8") as f: + f.write(tsv_table) else: with open("report.txt", "w", encoding="utf-8") as f: f.write(str(table)) diff --git a/src/utils/stringtodate.py b/src/utils/stringtodate.py index 9a8ad55..98cabb3 100644 --- a/src/utils/stringtodate.py +++ b/src/utils/stringtodate.py @@ -14,7 +14,7 @@ def stringToDate(date: str) -> QtCore.QDate: if not date: return "" if isinstance(date, QtCore.QDate): - return date.toString("yyyy-MM-dd") + return date else: datedata = date.split("-") day = datedata[2] From a0fce94298d03203bfb296226d94efc080fc7f80 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:01:11 +0200 Subject: [PATCH 35/83] enable backup by default --- config/settings.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/settings.yaml b/config/settings.yaml index c1682f4..2f6c1ff 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -2,10 +2,10 @@ institution_name: HB Testbibliothek Psychologie default_loan_duration: 7 catalogue: True database: - path: C:/testdatabase_1 + path: C:/testdatabase_12 name: librr.db backupLocation: V:/backup - do_backup: false + do_backup: True report: generate_report: false email: None From 6d4b274b30e8abd3aa198a4763799a3497e76181 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:02:27 +0200 Subject: [PATCH 36/83] ui changes, loan quit with hotkey q if no lineedit in focus --- src/__init__.py | 54 +++++++++++++++++++++++++++++++-- src/ui/main_ui.py | 1 - src/ui/sources/Ui_main_Loans.py | 10 ++++++ src/ui/sources/main_Loans.ui | 34 ++++++++++++++++++++- src/ui/user.py | 10 ++++-- 5 files changed, 102 insertions(+), 7 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 9a407eb..0ae70f2 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,6 +1,6 @@ import omegaconf import sys - +# import argparse __version__ = "0.0.1" __author__ = "Alexander Kirchner" @@ -9,5 +9,55 @@ config = omegaconf.OmegaConf.load("config/settings.yaml") # if programm launched with argument --debug, set debug to True if "--debug" in sys.argv: config.debug = True +# if programm launched with argument --log, set log_debug if "--log" in sys.argv: - config.log_debug = True \ No newline at end of file + config.log_debug = True + +# arguments = argparse.ArgumentParser( +# prog="Ausleihsystem", +# description="Ein Ausleihsystem für Handbibliotheken", +# epilog="Version: {}".format(__version__), +# ) +# arguments.add_argument( +# "-d", +# "--debug", +# action="store_true", +# help="Display debug messages in terminal", +# default=False, +# required=False, +# ) +# arguments.add_argument( +# "-v", +# "--version", +# action="store_true", +# help="Display version number", +# default=False, +# required=False, +# ) +# arguments.add_argument( +# "-l", +# "--log", +# action="store_true", +# help="Log debug messages to logfile", +# default=False, +# required=False, +# ) +# arguments.add_argument( +# "--no-backup", +# action="store_true", +# help="Disable backup", +# default=False, +# required=False, +# ) + +# args = arguments.parse_args() +# # based on the arguments, set the config values +# if args.debug: +# config.debug = True +# if args.version: +# print(f"Version: {__version__}") +# sys.exit() +# if args.log: +# config.log_debug = True +# if args.no_backup: +# config.database.do_backup = False diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index ff766a5..284a556 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -374,7 +374,6 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.btn_show_lentmedia.setText( self.db.getActiveLoans(self.activeUser.id) ) - # else: # print("Book not loaned") self.setStatusTipMessage("Buch nicht entliehen") diff --git a/src/ui/sources/Ui_main_Loans.py b/src/ui/sources/Ui_main_Loans.py index 6c1edd2..4ef2f59 100644 --- a/src/ui/sources/Ui_main_Loans.py +++ b/src/ui/sources/Ui_main_Loans.py @@ -70,9 +70,16 @@ class Ui_MainWindow(object): self.menubar = QtWidgets.QMenuBar(parent=MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 899, 22)) self.menubar.setObjectName("menubar") + self.menuDatei = QtWidgets.QMenu(parent=self.menubar) + self.menuDatei.setObjectName("menuDatei") MainWindow.setMenuBar(self.menubar) + self.actionBeenden = QtGui.QAction(parent=MainWindow) + self.actionBeenden.setObjectName("actionBeenden") + self.menuDatei.addAction(self.actionBeenden) + self.menubar.addAction(self.menuDatei.menuAction()) self.retranslateUi(MainWindow) + self.actionBeenden.triggered.connect(MainWindow.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -98,3 +105,6 @@ class Ui_MainWindow(object): item.setText(_translate("MainWindow", "entliehen bis")) item = self.loanTable.horizontalHeaderItem(6) item.setText(_translate("MainWindow", "Zurückgegeben am")) + self.menuDatei.setTitle(_translate("MainWindow", "Datei")) + self.actionBeenden.setText(_translate("MainWindow", "Beenden")) + self.actionBeenden.setShortcut(_translate("MainWindow", "Q")) diff --git a/src/ui/sources/main_Loans.ui b/src/ui/sources/main_Loans.ui index f7d5a23..94aac73 100644 --- a/src/ui/sources/main_Loans.ui +++ b/src/ui/sources/main_Loans.ui @@ -141,8 +141,40 @@ 22 + + + Datei + + + + + + + Beenden + + + Q + + - + + + actionBeenden + triggered() + MainWindow + close() + + + -1 + -1 + + + 449 + 328 + + + + diff --git a/src/ui/user.py b/src/ui/user.py index 1176021..7055c93 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -4,6 +4,7 @@ from src.logic import Database from src.schemas import User from .extendLoan import ExtendLoan from src.utils import stringToDate, Icon +from src.utils import debugMessage as dbg TABLETOFIELDTRANSLATE = { "Titel": "title", @@ -128,7 +129,7 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): if self.radio_currentlyLoaned.isChecked() else "overdue" ) - # print(mode) + print(mode) if self.userMedia == []: books = self.db.getAllMedia(self.userno) for book in books: @@ -145,12 +146,15 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): continue elif mode == "overdue": # book not returned and todays date is greater than todate - if book.returned_date != "": + dbg(book=book) + if book.returned_date is not None: continue + # if todate is greater than current date, continue if todate > QtCore.QDate.currentDate(): continue - self.addBookToTable(book) + self.addBookToTable(book) + print(book.title) def addBookToTable(self, book): self.UserMediaTable.insertRow(0) # item0 = isbn From 3df8b4dd3f4b5212faabe0d40c821a582534e035 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:13:37 +0200 Subject: [PATCH 37/83] update ui --- src/ui/sources/Ui_main_UserInterface.py | 2 +- src/ui/sources/Ui_main_userData.py | 54 ++++++++------- src/ui/sources/main_UserInterface.ui | 2 +- src/ui/sources/main_userData.ui | 91 +++++++++++++++---------- 4 files changed, 87 insertions(+), 62 deletions(-) diff --git a/src/ui/sources/Ui_main_UserInterface.py b/src/ui/sources/Ui_main_UserInterface.py index 15ce2ca..af55f8d 100644 --- a/src/ui/sources/Ui_main_UserInterface.py +++ b/src/ui/sources/Ui_main_UserInterface.py @@ -193,7 +193,7 @@ class Ui_MainWindow(object): def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) - self.label_3.setText(_translate("MainWindow", "Suchbegriff")) + self.label_3.setText(_translate("MainWindow", "Signatur")) self.label_6.setText(_translate("MainWindow", "Ausleihe bis")) self.label_5.setText(_translate("MainWindow", "Modus")) self.mode.setText(_translate("MainWindow", "Rückgabe")) diff --git a/src/ui/sources/Ui_main_userData.py b/src/ui/sources/Ui_main_userData.py index e09d822..529080b 100644 --- a/src/ui/sources/Ui_main_userData.py +++ b/src/ui/sources/Ui_main_userData.py @@ -20,26 +20,6 @@ class Ui_MainWindow(object): self.verticalLayout.setObjectName("verticalLayout") self.gridLayout = QtWidgets.QGridLayout() self.gridLayout.setObjectName("gridLayout") - self.name = QtWidgets.QLineEdit(parent=self.centralwidget) - self.name.setObjectName("name") - self.gridLayout.addWidget(self.name, 0, 1, 1, 1) - self.label_3 = QtWidgets.QLabel(parent=self.centralwidget) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1) - self.label_2 = QtWidgets.QLabel(parent=self.centralwidget) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1) - self.mail = QtWidgets.QLineEdit(parent=self.centralwidget) - self.mail.setObjectName("mail") - self.gridLayout.addWidget(self.mail, 2, 1, 1, 1) - self.label = QtWidgets.QLabel(parent=self.centralwidget) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 0, 0, 1, 1) - self.user_no = QtWidgets.QLineEdit(parent=self.centralwidget) - self.user_no.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.user_no.setReadOnly(True) - self.user_no.setObjectName("user_no") - self.gridLayout.addWidget(self.user_no, 1, 1, 1, 1) self.frame = QtWidgets.QFrame(parent=self.centralwidget) self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) @@ -58,6 +38,34 @@ class Ui_MainWindow(object): spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) self.horizontalLayout.addItem(spacerItem1) self.gridLayout.addWidget(self.frame, 3, 1, 1, 1) + self.label = QtWidgets.QLabel(parent=self.centralwidget) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 1) + self.user_no = QtWidgets.QLineEdit(parent=self.centralwidget) + self.user_no.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.user_no.setReadOnly(True) + self.user_no.setObjectName("user_no") + self.gridLayout.addWidget(self.user_no, 1, 1, 1, 1) + self.label_3 = QtWidgets.QLabel(parent=self.centralwidget) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1) + self.name = QtWidgets.QLineEdit(parent=self.centralwidget) + self.name.setObjectName("name") + self.gridLayout.addWidget(self.name, 0, 1, 1, 1) + self.label_2 = QtWidgets.QLabel(parent=self.centralwidget) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1) + self.mail = QtWidgets.QLineEdit(parent=self.centralwidget) + self.mail.setObjectName("mail") + self.gridLayout.addWidget(self.mail, 2, 1, 1, 1) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.deleteUser = QtWidgets.QToolButton(parent=self.centralwidget) + self.deleteUser.setMinimumSize(QtCore.QSize(30, 30)) + self.deleteUser.setText("") + self.deleteUser.setObjectName("deleteUser") + self.horizontalLayout_4.addWidget(self.deleteUser) + self.gridLayout.addLayout(self.horizontalLayout_4, 3, 0, 1, 1) self.verticalLayout.addLayout(self.gridLayout) self.line = QtWidgets.QFrame(parent=self.centralwidget) self.line.setFrameShape(QtWidgets.QFrame.Shape.HLine) @@ -158,11 +166,11 @@ class Ui_MainWindow(object): def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) - self.label_3.setText(_translate("MainWindow", "Mail")) - self.label_2.setText(_translate("MainWindow", "Matrikelnummer")) - self.label.setText(_translate("MainWindow", "Name, Vorname")) self.btn_userChange_save.setText(_translate("MainWindow", "Speichern")) self.btn_userchange_cancel.setText(_translate("MainWindow", "Abbrechen")) + self.label.setText(_translate("MainWindow", "Name, Vorname")) + self.label_3.setText(_translate("MainWindow", "Mail")) + self.label_2.setText(_translate("MainWindow", "Matrikelnummer")) self.label_4.setText(_translate("MainWindow", "Medien")) self.radio_allLoanedMedia.setText(_translate("MainWindow", "Alle Ausleihen")) self.radio_currentlyLoaned.setText(_translate("MainWindow", "Aktuell entliehen")) diff --git a/src/ui/sources/main_UserInterface.ui b/src/ui/sources/main_UserInterface.ui index dca3a46..fe32f42 100644 --- a/src/ui/sources/main_UserInterface.ui +++ b/src/ui/sources/main_UserInterface.ui @@ -65,7 +65,7 @@ - Suchbegriff + Signatur diff --git a/src/ui/sources/main_userData.ui b/src/ui/sources/main_userData.ui index fd6ef9f..45817f1 100644 --- a/src/ui/sources/main_userData.ui +++ b/src/ui/sources/main_userData.ui @@ -20,43 +20,6 @@ - - - - - - - Mail - - - - - - - Matrikelnummer - - - - - - - - - - Name, Vorname - - - - - - - Qt::NoFocus - - - true - - - @@ -112,6 +75,60 @@ + + + + Name, Vorname + + + + + + + Qt::NoFocus + + + true + + + + + + + Mail + + + + + + + + + + Matrikelnummer + + + + + + + + + + + + + 30 + 30 + + + + + + + + + From 9e50586668297ab3be95d1c284ff19d2de113beb Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:14:00 +0200 Subject: [PATCH 38/83] add function to delete user --- src/ui/user.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ui/user.py b/src/ui/user.py index 7055c93..2d67f81 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -29,6 +29,8 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): self.btn_userChange_save.clicked.connect(self.saveChanges) self.btn_userchange_cancel.clicked.connect(self.discardChanges) self.btn_extendSelectedMedia.clicked.connect(self.extendLoan) + self.deleteUser.clicked.connect(self.userDelete) + self.deleteUser.setIcon(Icon("delete").overwriteColor("red")) # radioButtons self.radio_allLoanedMedia.clicked.connect(self.loadMedia) @@ -54,6 +56,16 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): self.show() + def userDelete(self): + self.db.deleteUser(self.userno) + dialog = QtWidgets.QMessageBox() + dialog.setIcon(QtWidgets.QMessageBox.Icon.Information) + dialog.setWindowIcon(Icon("delete").icon) + dialog.setText("Nutzer wurde gelöscht") + dialog.setWindowTitle("Nutzer gelöscht") + dialog.exec() + self.close() + def extendLoan(self): extend = ExtendLoan(self.username, self.userMedia) extend.exec() From 5e706022bc45d3a9bab1208e6b5efadaa0d102c7 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Fri, 2 Aug 2024 08:42:15 +0200 Subject: [PATCH 39/83] database reason rework, auto-report and auto user deletion after set time in settings --- src/ui/main_ui.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index 284a556..9d656f5 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -1,9 +1,11 @@ import sys import atexit +import datetime from src import config from src.logic import Database, Catalogue, Backup from src.utils import stringToDate, Icon, Log from src.utils import debugMessage as dbg +from src.utils.createReport import generate_report from src.schemas import Book from .sources.Ui_main_UserInterface import Ui_MainWindow from .user import UserUI @@ -168,6 +170,8 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_username.setText(self.activeUser.username) self.input_userno.setText(str(self.activeUser.id)) self.userdata.setText(self.activeUser.__repr__()) + today = QtCore.QDate.currentDate().toString("yyyy-MM-dd") + self.db.setUserActiveDate(self.activeUser.id, today) # self.mode.setText("Ausleihe") def createUser(self): @@ -391,6 +395,12 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def exit_handler(): dbg("Exiting, creating backup") app = QtWidgets.QApplication(sys.argv) + print(backup.backup) + # generate report if monday + if datetime.datetime.now().weekday() == 0: + generate_report() + dbg("Generated Report") + Database().renameInactiveUsers() if config.database.do_backup: state = backup.createBackup() # create dialog to show state @@ -408,13 +418,11 @@ def exit_handler(): else: dialog = QtWidgets.QMessageBox() # set icon - reason = ( - "Backup deaktiviert" - if config.database.do_backup is False - else "Backuppfad nicht gefunden" - if not backup.backup - else "Unbekannter Fehler" - ) + reason = "Unbekannter Grund" + if config.database.do_backup is False: + reason = "Backup deaktiviert" + if backup.backup is False: + reason = "Backuppfad nicht gefunden" dialog.setWindowIcon(Icon("backup").icon) dialog.setWindowTitle("Backup nicht möglich") dialog.setText("Backup konnte nicht erstellt werden\nGrund: {}".format(reason)) From 0fb525ad4e55a90d3af78a2e0a8441264ebe37eb Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Fri, 2 Aug 2024 08:42:52 +0200 Subject: [PATCH 40/83] implement logic to use backup folder if origin is unreachable and once reachable, move data to origin --- src/logic/backup.py | 2 + src/logic/database.py | 156 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 137 insertions(+), 21 deletions(-) diff --git a/src/logic/backup.py b/src/logic/backup.py index aea1d30..8e194af 100644 --- a/src/logic/backup.py +++ b/src/logic/backup.py @@ -9,6 +9,8 @@ class Backup: self.source_path = config.database.path + "/" + config.database.name self.backup_path = config.database.backupLocation + "/" + config.database.name self.backup = False + if not os.path.exists(config.database.backupLocation): + os.makedirs(config.database.backupLocation) if config.database.do_backup == True: self.checkpaths() config.database.do_backup = self.backup diff --git a/src/logic/database.py b/src/logic/database.py index 0200bdd..d1e2939 100644 --- a/src/logic/database.py +++ b/src/logic/database.py @@ -1,28 +1,102 @@ import sqlite3 as sql import os +import shutil from src import config -from pathlib import Path from src.schemas import USERS, MEDIA, LOANS, User, Book, Loan -from src.utils import stringToDate, Log +from src.utils import stringToDate, Log, debugMessage as dbg +from PyQt6 import QtCore log = Log("Database") +FILE = config.database.name class Database: def __init__(self, db_path: str = None): - """ + ''' Default constructor for the database class Args: db_path (str, optional): Optional Path for testing / specific purposes. Defaults to None. - """ + ''' if db_path is None: - self.db_path = config.database.path + "/" + config.database.name + self.db_path = self.handle_folder_reachability(config.database.path, config.database.backupLocation) else: self.db_path = db_path + if not os.path.exists(config.database.path): - os.makedirs(config.database.path) + try: + os.makedirs(config.database.path) + except FileNotFoundError: + print(self.db_path) + if not os.path.exists(config.database.backupLocation): + os.makedirs(config.database.backupLocation) + #if main path does not exist, try to create it. if that fails, use the backuplocation + print(self.db_path) self.checkDatabaseStatus() + def handle_folder_reachability(self, original_path, backup_path): + """ + Checks if the original folder is reachable. If not, creates a backup. + If the original folder becomes reachable again, restores the backup. + + Args: + original_path (str): Path to the original folder. + backup_path (str): Path to the backup folder. + + Returns: + str: Path to the current accessible folder. + """ + + backup_file = os.path.join(backup_path, ".backup") + + if not os.path.exists(original_path): + #original folder not reachable, use backup path and create .backup file + if not os.path.exists(backup_path): + os.makedirs(backup_path) + with open(backup_file, "w") as f: + f.write("") + + # Create an empty backup file as a marker + return backup_path +"/" + FILE + + else: + print("Original Path Exists, ") + # Original folder is reachable, check for backup + if os.path.exists(backup_file): + # Restore backup + shutil.rmtree(original_path) # Remove original folder to avoid conflicts + os.rename(backup_path, original_path) + # (backup_path, original_path) + #os.remove(backup_file) + #remove backup file from original path + os.remove(original_path + "/.backup") + os.makedirs(backup_path) + return original_path +"/" + FILE + + def checkDatabasePath(self): + self.db_path = config.database.path + "/" + config.database.name + #if backup file in backup location, move database to main location, delete backup file + if os.path.exists(config.database.backupLocation + "/backup"): + if os.path.exists(self.db_path): + os.remove(self.db_path) + os.rename(f"{config.database.backupLocation}/{config.database.name}", self.db_path) + #remove backup file + os.remove(config.database.backupLocation + "/backup") + return self.db_path + else: + #keep using backup file + self.db_path = config.database.backupLocation + "/" + config.database.name + if not os.path.exists(config.database.path): + try: + os.makedirs(config.database.path) + except: + self.db_path = config.database.backupLocation + "/" + config.database.name + if not os.path.exists(config.database.backupLocation): + os.makedirs(config.database.backupLocation) + #create a backup file in the backup location + with open(f"{config.database.backupLocation}/backup.txt", "w") as f: + f.write("Backup File") + return self.db_path + def checkDatabaseStatus(self): log.info("Checking Database Status") if self.tableCheck() == []: @@ -31,29 +105,29 @@ class Database: # self.insertSubjects() def connect(self) -> sql.Connection: - """ + ''' Connect to the database Returns: sql.Connection: The active connection to the database - """ + ''' return sql.connect(self.db_path) def close_connection(self, conn: sql.Connection): - """ + ''' closes the connection to the database Args: ---- - conn (sql.Connection): the connection to be closed - """ + ''' conn.close() def createDatabase(self): log.info("Creating Database") # print("Creating Database") - if not os.path.exists(config.database.path): - os.makedirs(config.database.path) + if not os.path.exists(self.db_path): + os.makedirs(self.db_path) conn = self.connect() cursor = conn.cursor() cursor.execute(USERS) @@ -76,12 +150,12 @@ class Database: def tableCheck(self): # check if database has tables - """ + ''' Get the contents of the Returns: - Union[List[Tuple], None]: _description_ - """ + Union[List[Tuple], None]: Returns a list of tuples containing the table names or None if no tables are present + ''' try: with sql.connect(self.db_path) as conn: cursor = conn.cursor() @@ -114,11 +188,15 @@ class Database: log.debug(f"Inserting User {userno}, {username}, {usermail}") conn = self.connect() cursor = conn.cursor() - cursor.execute( - f"INSERT INTO users (id,username, usermail) VALUES ('{userno}', '{username}', '{usermail}' )" - ) - conn.commit() + try: + cursor.execute( + f"INSERT INTO users (id,username, usermail) VALUES ('{userno}', '{username}', '{usermail}' )" + ) + conn.commit() + except sql.IntegrityError: + return False self.close_connection(conn) + return True def getUser(self, userid) -> User: conn = self.connect() @@ -130,6 +208,16 @@ class Database: log.info(f"Returning User {user}") return user + def getUserId(self, username) -> User: + conn = self.connect() + cursor = conn.cursor() + cursor.execute(f"SELECT * FROM users WHERE username = '{username}'") + result = cursor.fetchone() + self.close_connection(conn) + user = User(id=result[0], username=result[1], email=result[2]) + log.info(f"Returning User {user}") + return user + def updateUser(self, username, userno, usermail): log.debug(f"Updating User {userno}, {username}, {usermail}") conn = self.connect() @@ -138,9 +226,34 @@ class Database: f"UPDATE users SET username = '{username}', usermail = '{usermail}' WHERE id = '{userno}'" ) conn.commit() - self.close_connection(conn) - + + def setUserActiveDate(self, userid,date): + query = f"UPDATE users SET lastActive = '{date}' WHERE id = '{userid}'" + conn = self.connect() + cursor = conn.cursor() + cursor.execute(query) + conn.commit() + dbg(f"Setting User {userid} to active on {date}") + + def renameInactiveUsers(self): + lastYear = QtCore.QDate.currentDate().addDays(int(f"-{config.inactive_user_deletion}")).toString("yyyy-MM-dd") + query = f"SELECT * FROM users WHERE lastActive < '{lastYear}'" + conn = self.connect() + cursor = conn.cursor() + result = cursor.execute(query).fetchall() + self.close_connection(conn) + for user in result: + self.updateUser("gelöscht", user[0], "gelöscht") + + def deleteUser(self, userid): + log.debug(f"Deleting User {userid}") + conn = self.connect() + cursor = conn.cursor() + cursor.execute(f"UPDATE users SET username='gelöscht', usermail = 'gelöscht' WHERE id = '{userid}'") + conn.commit() + self.close_connection(conn) + def getActiveLoans(self, userid): conn = self.connect() cursor = conn.cursor() @@ -172,6 +285,7 @@ class Database: loan[5], stringToDate(loan[6]), self.getMedia(loan[2]), + user_name=self.getUser(loan[1]).username, ) loan_data.append(l) return loan_data From 05bd36a4f7a87626e4e1ceec3f61ee9e7a549f4d Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Fri, 2 Aug 2024 08:44:18 +0200 Subject: [PATCH 41/83] add lastactive to schema to allow deletion of users --- src/schemas/database.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/schemas/database.py b/src/schemas/database.py index e18dc07..0242154 100644 --- a/src/schemas/database.py +++ b/src/schemas/database.py @@ -1,8 +1,10 @@ USERS = """CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, username TEXT NOT NULL, -usermail TEXT NOT NULL); +usermail TEXT NOT NULL, +lastActive TEXT); """ # id == matrikelnr, +#matrikelnr TEXT NOT NULL, MEDIA = """CREATE TABLE IF NOT EXISTS media ( id INTEGER PRIMARY KEY AUTOINCREMENT, signature TEXT NOT NULL, From aad179deb58a377abff5e95ad76665f9213862f6 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Fri, 2 Aug 2024 08:44:33 +0200 Subject: [PATCH 42/83] add delete icon --- icons/delete.svg | 1 + icons/icons.yaml | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 icons/delete.svg diff --git a/icons/delete.svg b/icons/delete.svg new file mode 100644 index 0000000..339e314 --- /dev/null +++ b/icons/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/icons.yaml b/icons/icons.yaml index 306f78f..1dc2e7c 100644 --- a/icons/icons.yaml +++ b/icons/icons.yaml @@ -7,10 +7,11 @@ icons: duplicate: duplicate.svg error: error.svg loan_extend: calendar_event.svg - main: library.svg + main: library.svg multiuser: multiple_user.svg newentry: library_add.svg report: report.svg settings: settings.svg user: user.svg - warning: warning.svg \ No newline at end of file + warning: warning.svg + delete: delete.svg From 7d6cfc9b64ee0b18fc2b647c0fce8aa36b70b3be Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Fri, 2 Aug 2024 08:44:53 +0200 Subject: [PATCH 43/83] create report every seven days --- src/utils/createReport.py | 61 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/utils/createReport.py diff --git a/src/utils/createReport.py b/src/utils/createReport.py new file mode 100644 index 0000000..0c94f16 --- /dev/null +++ b/src/utils/createReport.py @@ -0,0 +1,61 @@ +import os +from prettytable import PrettyTable +from src.logic import Database +from src.utils import stringToDate +import sqlite3 as sql +from PyQt6.QtCore import QDate +from src import config +import datetime + +# query all loans that happened in the last 7 days +def generate_report(): + '''Generate an excel report for all actions that happened in the last seven days + + Returns: + str: a string represeting the generated table + ''' + db = Database() + path = db.db_path + year = datetime.datetime.now().year + week = datetime.datetime.now().isocalendar()[1] + if not os.path.exists(config.report.path): + os.makedirs(config.report.path) + report_path = os.path.join(config.report.path, f"report_{year}_{week}.tsv") + day = QDate.currentDate().addDays(-7).toString("yyyy-MM-dd") + query = f"""SELECT * FROM loans WHERE loan_date >= '{day}';""" + # print(query) + + colnames = ["UserId", "Title", "Action", "Datum"] + table = PrettyTable(colnames) + table.align[colnames[0]] = "l" + table.align[colnames[1]] = "l" + table.align[colnames[2]] = "l" + + with sql.connect(path) as conn: + cursor = conn.cursor() + cursor.execute(query) + loans = cursor.fetchall() + + for loan in loans: + loan_action = "Ausleihe" if loan[5] == 0 else "Rückgabe" + loan_action_date = stringToDate( + loan[3] if loan[5] == 0 else loan[6] + ).toString("dd.MM.yyyy") + table.add_row( + [ + loan[1], + db.getMedia(loan[2]).title, + loan_action, + loan_action_date, + ] + ) + # # print(table) + # # wruitng the table to a file + # with open("report.txt", "w", encoding="utf-8") as f: + # f.write(str(table)) + + tsv_table = table.get_csv_string().replace(",", "\t") + # write the file + with open(report_path, "w", encoding="utf-8") as f: + f.write(tsv_table) + return table From f913ed27948aeb63c7634562bdd36d0c6750a61c Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Fri, 2 Aug 2024 08:45:29 +0200 Subject: [PATCH 44/83] change url to use one where results are guaranteed, but limited in scope --- src/logic/catalogue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logic/catalogue.py b/src/logic/catalogue.py index 895a772..12a5ba5 100644 --- a/src/logic/catalogue.py +++ b/src/logic/catalogue.py @@ -3,7 +3,7 @@ from bs4 import BeautifulSoup from src import config from src.schemas import Book from src.utils import Log -URL = 'https://rds.ibs-bw.de/phfreiburg/opac/RDSIndex/Search?lookfor="{}"+&type=AllFields&limit=10&sort=py+desc%2C+title' +URL = "https://rds.ibs-bw.de/phfreiburg/opac/RDSIndex/Search?type0%5B%5D=allfields&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=au&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=ti&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=ct&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=isn&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=ta&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=co&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=py&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=pp&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=pu&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=si&lookfor0%5B%5D={}&join=AND&bool0%5B%5D=AND&type0%5B%5D=zr&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=cc&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND" BASE = "https://rds.ibs-bw.de" log = Log("Catalogue") From c10ecdd4d3198df4ccdad223f21641b1386d2c4d Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Fri, 2 Aug 2024 08:46:08 +0200 Subject: [PATCH 45/83] add report location, inactive deletion --- config/settings.yaml | 10 ++++++---- src/utils/log.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/config/settings.yaml b/config/settings.yaml index 2f6c1ff..ffa76b5 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -1,13 +1,15 @@ institution_name: HB Testbibliothek Psychologie default_loan_duration: 7 +inactive_user_deletion: 365 catalogue: True database: - path: C:/testdatabase_12 - name: librr.db - backupLocation: V:/backup + path: C:/testing/source_2 + name: library.db + backupLocation: C:/Databasebackup do_backup: True + report: generate_report: false - email: None + path: C:/report debug: false log_debug: false diff --git a/src/utils/log.py b/src/utils/log.py index fb815fa..6ba3717 100644 --- a/src/utils/log.py +++ b/src/utils/log.py @@ -23,4 +23,4 @@ class Log: self.logger.error(message) def warning(self, message): - self.logger.warning(message) + self.logger.warning(message) \ No newline at end of file From 560b0a529852ba4cb42a4a31fcee018e8b87255b Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:49:16 +0200 Subject: [PATCH 46/83] fix broken excel files --- src/utils/reportThread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/reportThread.py b/src/utils/reportThread.py index b5eff7a..5e3edba 100644 --- a/src/utils/reportThread.py +++ b/src/utils/reportThread.py @@ -58,9 +58,9 @@ class ReportThread(QThread): # # wruitng the table to a file if self.format == "tsv": table = table.get_csv_string() - tsv_table = table.replace(",", "\t") + tsv_table = table.replace(",", "\t")#.replace("Rückgabe", "Rückgabe") # write the file - with open("report.tsv", "w", encoding="utf-8") as f: + with open("report.tsv", "w") as f: f.write(tsv_table) else: with open("report.txt", "w", encoding="utf-8") as f: From 477392e85e3deac49169784cf61bf09a9dd3ff60 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:49:40 +0200 Subject: [PATCH 47/83] add normal id to userui --- src/ui/user.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ui/user.py b/src/ui/user.py index 2d67f81..1f02fad 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -13,15 +13,16 @@ TABLETOFIELDTRANSLATE = { class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): - def __init__(self, u_name, u_no, u_mail): + def __init__(self, user): super(UserUI, self).__init__() self.setupUi(self) self.setWindowTitle("Nutzerdaten") self.setWindowIcon(Icon("user").icon) self.db = Database() - self.username = u_name - self.userno = u_no - self.usermail = u_mail + self.username = user.username + self.userno = user.userid + self.usermail = user.email + self.user_id = user.id self.setFields() self.userMedia = [] self.loadMedia() @@ -57,7 +58,7 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): self.show() def userDelete(self): - self.db.deleteUser(self.userno) + self.db.deleteUser(self.user_id) dialog = QtWidgets.QMessageBox() dialog.setIcon(QtWidgets.QMessageBox.Icon.Information) dialog.setWindowIcon(Icon("delete").icon) From 1b8379e07150c3f84161a6529314eef79a3af05c Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:50:05 +0200 Subject: [PATCH 48/83] change schema --- src/schemas/database.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/schemas/database.py b/src/schemas/database.py index 0242154..0ca4ba2 100644 --- a/src/schemas/database.py +++ b/src/schemas/database.py @@ -1,5 +1,6 @@ USERS = """CREATE TABLE IF NOT EXISTS users ( -id INTEGER PRIMARY KEY, +id INTEGER PRIMARY KEY AUTOINCREMENT, +user_id INTEGER NOT NULL, username TEXT NOT NULL, usermail TEXT NOT NULL, lastActive TEXT); From a9165844d7d48592dc7264f600637e09d42eec90 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:51:41 +0200 Subject: [PATCH 49/83] refactor: add id, set userid to any --- src/schemas/user.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/schemas/user.py b/src/schemas/user.py index 3a9ddd2..35a56dd 100644 --- a/src/schemas/user.py +++ b/src/schemas/user.py @@ -1,11 +1,12 @@ from dataclasses import dataclass - +from typing import Any @dataclass class User: username: str - id: int + userid: Any email: str + id : int = None def __repr__(self): - return f"Name: {self.username}\nMatrikelnr.: {self.id}\neMail: {self.email}" + return f"Name: {self.username}\nMatrikelnr.: {self.userid}\neMail: {self.email}" From 02305a4ad33b253ab6d8cb9b3e59b3432005c5f5 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:47:21 +0200 Subject: [PATCH 50/83] new config class to avoid restarting application upon settings change --- src/utils/config.py | 71 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/utils/config.py diff --git a/src/utils/config.py b/src/utils/config.py new file mode 100644 index 0000000..67d004f --- /dev/null +++ b/src/utils/config.py @@ -0,0 +1,71 @@ +from typing import Optional + +import omegaconf + +class Config: + _config: Optional[omegaconf.DictConfig] = None + + def __init__(self, config_path: str): + """ + Loads the configuration file and stores it for future access. + + Args: + config_path (str): Path to the YAML configuration file. + """ + self._config = omegaconf.OmegaConf.load(config_path) + + @property + def institution_name(self)->str: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.institution_name + @property + def loan_duration(self)->int: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.default_loan_duration + @property + def delete_inactive_user_duration(self)->int: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.inactive_user_deletion + @property + def catalogue(self)->bool: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.catalogue + @property + def database(self)->omegaconf.DictConfig: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.database + + @property + def report(self)->omegaconf.DictConfig: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.report + @property + def debug(self)->bool: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.debug + @property + def log_debug(self)->bool: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.log_debug + + + def updateValue(self, key: str, value: str): + self._config[key] = value + omegaconf.OmegaConf.save(self._config, "config/settings.yaml") + + + + +if __name__ == "__main__": + cfg = Config("config/settings.yaml") + print(cfg.database.path) + + #cfg.updateValue("database.path", "Test") \ No newline at end of file From 9054c46f79316809b5d08466003fc82e0247ec55 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:47:38 +0200 Subject: [PATCH 51/83] ui changes --- src/ui/sources/Ui_dialog_settings.py | 93 ++++++++++++++---- src/ui/sources/Ui_dialog_settings.ui.py | 8 ++ src/ui/sources/Ui_main_UserInterface.py | 5 +- src/ui/sources/Ui_main_userData.py | 4 + src/ui/sources/dialog_settings.ui | 119 ++++++++++++++++++++++-- src/ui/sources/main_UserInterface.ui | 7 +- src/ui/sources/main_userData.ui | 7 ++ 7 files changed, 211 insertions(+), 32 deletions(-) create mode 100644 src/ui/sources/Ui_dialog_settings.ui.py diff --git a/src/ui/sources/Ui_dialog_settings.py b/src/ui/sources/Ui_dialog_settings.py index 1224deb..81171d8 100644 --- a/src/ui/sources/Ui_dialog_settings.py +++ b/src/ui/sources/Ui_dialog_settings.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(422, 184) + Dialog.resize(492, 306) self.formLayout = QtWidgets.QFormLayout(Dialog) self.formLayout.setObjectName("formLayout") self.label = QtWidgets.QLabel(parent=Dialog) @@ -24,54 +24,95 @@ class Ui_Dialog(object): self.label_2 = QtWidgets.QLabel(parent=Dialog) self.label_2.setObjectName("label_2") self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2) - self.default_loan_duration = QtWidgets.QLineEdit(parent=Dialog) - self.default_loan_duration.setObjectName("default_loan_duration") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.default_loan_duration) self.label_3 = QtWidgets.QLabel(parent=Dialog) self.label_3.setObjectName("label_3") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_3) - self.gridLayout = QtWidgets.QGridLayout() - self.gridLayout.setObjectName("gridLayout") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_3) + self.databasesettings = QtWidgets.QGridLayout() + self.databasesettings.setObjectName("databasesettings") self.database_name = QtWidgets.QLineEdit(parent=Dialog) self.database_name.setObjectName("database_name") - self.gridLayout.addWidget(self.database_name, 1, 1, 1, 1) + self.databasesettings.addWidget(self.database_name, 1, 1, 1, 1) self.label_4 = QtWidgets.QLabel(parent=Dialog) self.label_4.setObjectName("label_4") - self.gridLayout.addWidget(self.label_4, 0, 0, 1, 1) + self.databasesettings.addWidget(self.label_4, 0, 0, 1, 1) self.label_6 = QtWidgets.QLabel(parent=Dialog) self.label_6.setObjectName("label_6") - self.gridLayout.addWidget(self.label_6, 2, 0, 1, 1) + self.databasesettings.addWidget(self.label_6, 2, 0, 1, 1) self.database_path = QtWidgets.QLineEdit(parent=Dialog) self.database_path.setObjectName("database_path") - self.gridLayout.addWidget(self.database_path, 0, 1, 1, 1) + self.databasesettings.addWidget(self.database_path, 0, 1, 1, 1) self.database_backupLocation = QtWidgets.QLineEdit(parent=Dialog) self.database_backupLocation.setObjectName("database_backupLocation") - self.gridLayout.addWidget(self.database_backupLocation, 2, 1, 1, 1) + self.databasesettings.addWidget(self.database_backupLocation, 2, 1, 1, 1) self.label_5 = QtWidgets.QLabel(parent=Dialog) self.label_5.setObjectName("label_5") - self.gridLayout.addWidget(self.label_5, 1, 0, 1, 1) + self.databasesettings.addWidget(self.label_5, 1, 0, 1, 1) self.btn_select_database_path = QtWidgets.QToolButton(parent=Dialog) self.btn_select_database_path.setObjectName("btn_select_database_path") - self.gridLayout.addWidget(self.btn_select_database_path, 0, 2, 1, 1) + self.databasesettings.addWidget(self.btn_select_database_path, 0, 2, 1, 1) self.btn_select_database_name = QtWidgets.QToolButton(parent=Dialog) self.btn_select_database_name.setObjectName("btn_select_database_name") - self.gridLayout.addWidget(self.btn_select_database_name, 1, 2, 1, 1) + self.databasesettings.addWidget(self.btn_select_database_name, 1, 2, 1, 1) self.btn_select_database_backupLocation = QtWidgets.QToolButton(parent=Dialog) self.btn_select_database_backupLocation.setObjectName("btn_select_database_backupLocation") - self.gridLayout.addWidget(self.btn_select_database_backupLocation, 2, 2, 1, 1) - self.formLayout.setLayout(2, QtWidgets.QFormLayout.ItemRole.FieldRole, self.gridLayout) + self.databasesettings.addWidget(self.btn_select_database_backupLocation, 2, 2, 1, 1) + self.formLayout.setLayout(3, QtWidgets.QFormLayout.ItemRole.FieldRole, self.databasesettings) + self.label_7 = QtWidgets.QLabel(parent=Dialog) + self.label_7.setObjectName("label_7") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_7) + self.default_loan_duration = QtWidgets.QSpinBox(parent=Dialog) + self.default_loan_duration.setProperty("value", 7) + self.default_loan_duration.setObjectName("default_loan_duration") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.default_loan_duration) + self.delete_inactive_user_duration = QtWidgets.QSpinBox(parent=Dialog) + self.delete_inactive_user_duration.setMaximum(9999) + self.delete_inactive_user_duration.setProperty("value", 365) + self.delete_inactive_user_duration.setObjectName("delete_inactive_user_duration") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.ItemRole.FieldRole, self.delete_inactive_user_duration) self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Discard|QtWidgets.QDialogButtonBox.StandardButton.Ok) self.buttonBox.setObjectName("buttonBox") - self.formLayout.setWidget(3, QtWidgets.QFormLayout.ItemRole.FieldRole, self.buttonBox) + self.formLayout.setWidget(5, QtWidgets.QFormLayout.ItemRole.FieldRole, self.buttonBox) + self.label_9 = QtWidgets.QLabel(parent=Dialog) + self.label_9.setObjectName("label_9") + self.formLayout.setWidget(4, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_9) + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.btn_select_report_path = QtWidgets.QToolButton(parent=Dialog) + self.btn_select_report_path.setObjectName("btn_select_report_path") + self.gridLayout.addWidget(self.btn_select_report_path, 2, 2, 1, 1) + self.label_10 = QtWidgets.QLabel(parent=Dialog) + self.label_10.setText("") + self.label_10.setObjectName("label_10") + self.gridLayout.addWidget(self.label_10, 1, 0, 1, 1) + self.check_generate_report = QtWidgets.QCheckBox(parent=Dialog) + self.check_generate_report.setObjectName("check_generate_report") + self.gridLayout.addWidget(self.check_generate_report, 1, 1, 1, 1) + self.report_path = QtWidgets.QLineEdit(parent=Dialog) + self.report_path.setObjectName("report_path") + self.gridLayout.addWidget(self.report_path, 2, 1, 1, 1) + self.label_8 = QtWidgets.QLabel(parent=Dialog) + self.label_8.setObjectName("label_8") + self.gridLayout.addWidget(self.label_8, 2, 0, 1, 1) + self.label_11 = QtWidgets.QLabel(parent=Dialog) + self.label_11.setObjectName("label_11") + self.gridLayout.addWidget(self.label_11, 0, 0, 1, 1) + self.report_day = QtWidgets.QComboBox(parent=Dialog) + self.report_day.setObjectName("report_day") + self.report_day.addItem("") + self.report_day.addItem("") + self.report_day.addItem("") + self.report_day.addItem("") + self.report_day.addItem("") + self.gridLayout.addWidget(self.report_day, 0, 1, 1, 1) + self.formLayout.setLayout(4, QtWidgets.QFormLayout.ItemRole.FieldRole, self.gridLayout) self.retranslateUi(Dialog) self.buttonBox.accepted.connect(Dialog.accept) # type: ignore self.buttonBox.rejected.connect(Dialog.reject) # type: ignore QtCore.QMetaObject.connectSlotsByName(Dialog) - Dialog.setTabOrder(self.institution_name, self.default_loan_duration) - Dialog.setTabOrder(self.default_loan_duration, self.database_path) + Dialog.setTabOrder(self.institution_name, self.database_path) Dialog.setTabOrder(self.database_path, self.database_name) Dialog.setTabOrder(self.database_name, self.database_backupLocation) Dialog.setTabOrder(self.database_backupLocation, self.btn_select_database_path) @@ -90,3 +131,15 @@ class Ui_Dialog(object): self.btn_select_database_path.setText(_translate("Dialog", "...")) self.btn_select_database_name.setText(_translate("Dialog", "...")) self.btn_select_database_backupLocation.setText(_translate("Dialog", "...")) + self.label_7.setText(_translate("Dialog", "Inaktive Nutzer\n" +"Löschen nach")) + self.label_9.setText(_translate("Dialog", "Bericht")) + self.btn_select_report_path.setText(_translate("Dialog", "...")) + self.check_generate_report.setText(_translate("Dialog", "Bericht erstellen")) + self.label_8.setText(_translate("Dialog", "Speicherpfad")) + self.label_11.setText(_translate("Dialog", "Tag")) + self.report_day.setItemText(0, _translate("Dialog", "Montag")) + self.report_day.setItemText(1, _translate("Dialog", "Dienstag")) + self.report_day.setItemText(2, _translate("Dialog", "Mittwoch")) + self.report_day.setItemText(3, _translate("Dialog", "Donnerstag")) + self.report_day.setItemText(4, _translate("Dialog", "Freitag")) diff --git a/src/ui/sources/Ui_dialog_settings.ui.py b/src/ui/sources/Ui_dialog_settings.ui.py new file mode 100644 index 0000000..4425410 --- /dev/null +++ b/src/ui/sources/Ui_dialog_settings.ui.py @@ -0,0 +1,8 @@ +# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_settings.ui.iRVFlN' +# +# Created by: PyQt6 UI code generator 6.6.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + diff --git a/src/ui/sources/Ui_main_UserInterface.py b/src/ui/sources/Ui_main_UserInterface.py index af55f8d..aeb26e0 100644 --- a/src/ui/sources/Ui_main_UserInterface.py +++ b/src/ui/sources/Ui_main_UserInterface.py @@ -173,10 +173,12 @@ class Ui_MainWindow(object): self.actionAusleihistorie.setObjectName("actionAusleihistorie") self.actionBericht_erstellen = QtGui.QAction(parent=MainWindow) self.actionBericht_erstellen.setObjectName("actionBericht_erstellen") + self.actionNutzer_3 = QtGui.QAction(parent=MainWindow) + self.actionNutzer_3.setObjectName("actionNutzer_3") self.menuDatei.addAction(self.actionEinstellungen) self.menuDatei.addAction(self.actionBeenden) self.menuHotkeys.addAction(self.actionRueckgabemodus) - self.menuHotkeys.addAction(self.actionNutzer) + self.menuFenster.addAction(self.actionNutzer) self.menuFenster.addAction(self.actionAusleihistorie) self.menuFenster.addAction(self.actionBericht_erstellen) self.menubar.addAction(self.menuDatei.menuAction()) @@ -224,3 +226,4 @@ class Ui_MainWindow(object): self.actionAusleihistorie.setShortcut(_translate("MainWindow", "F8")) self.actionBericht_erstellen.setText(_translate("MainWindow", "Bericht erstellen")) self.actionBericht_erstellen.setShortcut(_translate("MainWindow", "F7")) + self.actionNutzer_3.setText(_translate("MainWindow", "Nutzer")) diff --git a/src/ui/sources/Ui_main_userData.py b/src/ui/sources/Ui_main_userData.py index 529080b..c901d3f 100644 --- a/src/ui/sources/Ui_main_userData.py +++ b/src/ui/sources/Ui_main_userData.py @@ -60,6 +60,9 @@ class Ui_MainWindow(object): self.gridLayout.addWidget(self.mail, 2, 1, 1, 1) self.horizontalLayout_4 = QtWidgets.QHBoxLayout() self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.label_5 = QtWidgets.QLabel(parent=self.centralwidget) + self.label_5.setObjectName("label_5") + self.horizontalLayout_4.addWidget(self.label_5) self.deleteUser = QtWidgets.QToolButton(parent=self.centralwidget) self.deleteUser.setMinimumSize(QtCore.QSize(30, 30)) self.deleteUser.setText("") @@ -171,6 +174,7 @@ class Ui_MainWindow(object): self.label.setText(_translate("MainWindow", "Name, Vorname")) self.label_3.setText(_translate("MainWindow", "Mail")) self.label_2.setText(_translate("MainWindow", "Matrikelnummer")) + self.label_5.setText(_translate("MainWindow", "Nutzer Löschen")) self.label_4.setText(_translate("MainWindow", "Medien")) self.radio_allLoanedMedia.setText(_translate("MainWindow", "Alle Ausleihen")) self.radio_currentlyLoaned.setText(_translate("MainWindow", "Aktuell entliehen")) diff --git a/src/ui/sources/dialog_settings.ui b/src/ui/sources/dialog_settings.ui index 201e573..c41caff 100644 --- a/src/ui/sources/dialog_settings.ui +++ b/src/ui/sources/dialog_settings.ui @@ -6,8 +6,8 @@ 0 0 - 422 - 184 + 492 + 306 @@ -31,18 +31,15 @@ - - - - + Datenbank - - + + @@ -96,7 +93,32 @@ - + + + + Inaktive Nutzer +Löschen nach + + + + + + + 7 + + + + + + + 9999 + + + 365 + + + + Qt::Horizontal @@ -106,11 +128,88 @@ + + + + Bericht + + + + + + + + + ... + + + + + + + + + + + + + + Bericht erstellen + + + + + + + + + + Speicherpfad + + + + + + + Tag + + + + + + + + Montag + + + + + Dienstag + + + + + Mittwoch + + + + + Donnerstag + + + + + Freitag + + + + + + institution_name - default_loan_duration database_path database_name database_backupLocation diff --git a/src/ui/sources/main_UserInterface.ui b/src/ui/sources/main_UserInterface.ui index fe32f42..7baf478 100644 --- a/src/ui/sources/main_UserInterface.ui +++ b/src/ui/sources/main_UserInterface.ui @@ -308,12 +308,12 @@ Hotkeys - Fenster + @@ -369,6 +369,11 @@ F7 + + + Nutzer + + btn_createNewUser diff --git a/src/ui/sources/main_userData.ui b/src/ui/sources/main_userData.ui index 45817f1..d91d773 100644 --- a/src/ui/sources/main_userData.ui +++ b/src/ui/sources/main_userData.ui @@ -114,6 +114,13 @@ + + + + Nutzer Löschen + + + From a72d76b94ea2a930036625f0bed840c5ab4eb395 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:48:07 +0200 Subject: [PATCH 52/83] bugfix --- src/ui/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/user.py b/src/ui/user.py index 1f02fad..e28b2be 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -144,7 +144,7 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): ) print(mode) if self.userMedia == []: - books = self.db.getAllMedia(self.userno) + books = self.db.getAllMedia(self.user_id) for book in books: self.userMedia.append(book) # print(self.userMedia) From 95d3de6490c423c4754975f66900f154bea43c89 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:39:06 +0200 Subject: [PATCH 53/83] new settings class --- src/__init__.py | 10 ++++---- src/logic/database.py | 45 ++++++++++++++++++++++-------------- src/ui/main_ui.py | 54 +++++++++++++++++++++++++++++-------------- src/ui/settings.py | 45 ++++++++++++++++++++++++++++++++---- 4 files changed, 110 insertions(+), 44 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 0ae70f2..0459e99 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,10 +1,9 @@ -import omegaconf -import sys # import argparse -__version__ = "0.0.1" -__author__ = "Alexander Kirchner" -config = omegaconf.OmegaConf.load("config/settings.yaml") + +import sys +from config import Config +config = Config("config/settings.yaml") # if programm launched with argument --debug, set debug to True if "--debug" in sys.argv: @@ -12,6 +11,7 @@ if "--debug" in sys.argv: # if programm launched with argument --log, set log_debug if "--log" in sys.argv: config.log_debug = True + # arguments = argparse.ArgumentParser( # prog="Ausleihsystem", diff --git a/src/logic/database.py b/src/logic/database.py index d1e2939..6c8f216 100644 --- a/src/logic/database.py +++ b/src/logic/database.py @@ -180,7 +180,7 @@ class Database: users = cursor.fetchall() for index, user in enumerate(users): - users[index] = User(id=user[0], username=user[1], email=user[2]) + users[index] = User(userid=user[1], username=user[2], email=user[3], id=user[0]) self.close_connection(conn) return users @@ -190,7 +190,7 @@ class Database: cursor = conn.cursor() try: cursor.execute( - f"INSERT INTO users (id,username, usermail) VALUES ('{userno}', '{username}', '{usermail}' )" + f"INSERT INTO users (user_id,username, usermail) VALUES ('{userno}', '{username}', '{usermail}' )" ) conn.commit() except sql.IntegrityError: @@ -198,15 +198,22 @@ class Database: self.close_connection(conn) return True - def getUser(self, userid) -> User: + def getUser(self, user_id) -> User: conn = self.connect() cursor = conn.cursor() - cursor.execute(f"SELECT * FROM users WHERE id = '{userid}'") - result = cursor.fetchone() + cursor.execute(f"SELECT * FROM users") + result = cursor.fetchall() self.close_connection(conn) - user = User(id=result[0], username=result[1], email=result[2]) - log.info(f"Returning User {user}") - return user + print(result) + for res in result: + if res[0] == user_id: + user = User(userid=res[1], username=res[2], email=res[3], id=res[0]) + dbg(f"Returning User {user}") + log.info(f"Returning User {user}") + return user + return User(userid="gelöscht", username="gelöscht", email="gelöscht", id="gelöscht") + # user = User(userid=result[1], username=result[2], email=result[3],id = result[0]) + # return user def getUserId(self, username) -> User: conn = self.connect() @@ -214,22 +221,22 @@ class Database: cursor.execute(f"SELECT * FROM users WHERE username = '{username}'") result = cursor.fetchone() self.close_connection(conn) - user = User(id=result[0], username=result[1], email=result[2]) + user = User(userid=result[1], username=result[2], email=result[3],id = result[0]) log.info(f"Returning User {user}") return user - def updateUser(self, username, userno, usermail): + def updateUser(self, username, user_id, usermail): log.debug(f"Updating User {userno}, {username}, {usermail}") conn = self.connect() cursor = conn.cursor() cursor.execute( - f"UPDATE users SET username = '{username}', usermail = '{usermail}' WHERE id = '{userno}'" + f"UPDATE users SET username = '{username}', usermail = '{usermail}' WHERE user_id = '{user_id}'" ) conn.commit() self.close_connection(conn) def setUserActiveDate(self, userid,date): - query = f"UPDATE users SET lastActive = '{date}' WHERE id = '{userid}'" + query = f"UPDATE users SET lastActive = '{date}' WHERE user_id = '{userid}'" conn = self.connect() cursor = conn.cursor() cursor.execute(query) @@ -237,24 +244,27 @@ class Database: dbg(f"Setting User {userid} to active on {date}") def renameInactiveUsers(self): - lastYear = QtCore.QDate.currentDate().addDays(int(f"-{config.inactive_user_deletion}")).toString("yyyy-MM-dd") - query = f"SELECT * FROM users WHERE lastActive < '{lastYear}'" + lastYear = QtCore.QDate.currentDate().addDays(int(f"-{config.delete_inactive_user_duration}")).toString("yyyy-MM-dd") + query = f"SELECT id FROM users WHERE lastActive < '{lastYear}'" conn = self.connect() cursor = conn.cursor() result = cursor.execute(query).fetchall() self.close_connection(conn) - for user in result: - self.updateUser("gelöscht", user[0], "gelöscht") + if len(result) == 0: + log.info(f"Deleting {len(result)} inactive users") + for user in result: + self.deleteUser(user) def deleteUser(self, userid): log.debug(f"Deleting User {userid}") conn = self.connect() cursor = conn.cursor() - cursor.execute(f"UPDATE users SET username='gelöscht', usermail = 'gelöscht' WHERE id = '{userid}'") + cursor.execute(f"UPDATE users SET username='gelöscht', usermail = 'gelöscht', user_id='gelöscht' WHERE id = '{userid}'") conn.commit() self.close_connection(conn) def getActiveLoans(self, userid): + dbg("id", str(userid)) conn = self.connect() cursor = conn.cursor() try: @@ -442,6 +452,7 @@ class Database: cursor = conn.cursor() cursor.execute(query) result = cursor.fetchone() + print(result) self.close_connection(conn) if result is not None: return result[0] diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index 9d656f5..3df118c 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -48,7 +48,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): # LineEdits self.input_userno.returnPressed.connect( - lambda: self.checkUser("id", self.input_userno.text()) + lambda: self.checkUser("user_id", self.input_userno.text()) ) self.input_username.returnPressed.connect( lambda: self.checkUser("username", self.input_username.text()) @@ -72,7 +72,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): # variables self.db = Database() self.currentDate = QtCore.QDate.currentDate() - loanDate = self.currentDate.addDays(config.default_loan_duration) + loanDate = self.currentDate.addDays(config.loan_duration) self.activeUser = None self.activeState = "Rückgabe" @@ -107,6 +107,9 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): log.info("Showing Settings") settings = Settings() settings.exec() + if settings.settingschanged: + # reload settings + print(config) def changeMode(self): log.info("Changing Mode") @@ -119,8 +122,10 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.label_7.hide() self.nextReturnDate.hide() self.mediaOverview.setRowCount(0) + self.activeUser = None #! remove if last user should be kept if self.activeState == "Rückgabe": self.activateLoanMode() + else: self.activateReturnMode() @@ -135,6 +140,11 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.mode.setStyleSheet("background-color: #1E90FF") self.mode.setText("Ausleihe") self.activeState = "Ausleihe" + if self.input_userno.text() == "" or self.input_username.text() == "": + self.input_file_ident.setEnabled(False) + self.input_file_ident.setPlaceholderText("Bitte zuerst Nutzerdaten eingeben") + else: + self.input_file_ident.setEnabled(True) def activateReturnMode(self): dbg("Activating Return Mode") @@ -145,6 +155,8 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.duedate.setEnabled(False) self.activeState = "Rückgabe" self.mode.setText("Rückgabe") + self.input_file_ident.setEnabled(True) + self.input_file_ident.setPlaceholderText("Buchidentifikation eingeben") def showUser(self): log.info(f"Showing User {self.activeUser}") @@ -160,18 +172,18 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): return self.user_ui = UserUI( - self.activeUser.username, self.activeUser.id, self.activeUser.email + self.activeUser ) # self.user_ui.setFields("John Doe", "123456789", "test@mail.com") self.user_ui.show() def setUserData(self): log.info("Setting User Data") - self.input_username.setText(self.activeUser.username) - self.input_userno.setText(str(self.activeUser.id)) + self.input_username.setText(str(self.activeUser.username)) + self.input_userno.setText(str(self.activeUser.userid)) self.userdata.setText(self.activeUser.__repr__()) today = QtCore.QDate.currentDate().toString("yyyy-MM-dd") - self.db.setUserActiveDate(self.activeUser.id, today) + self.db.setUserActiveDate(self.activeUser.userid, today) # self.mode.setText("Ausleihe") def createUser(self): @@ -222,14 +234,19 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.setUserData() self.input_file_ident.setFocus() self.mode.setText("Ausleihe") - self.btn_show_lentmedia.setText(self.db.getActiveLoans(self.activeUser.id)) + print(self.activeUser.__dict__) + loans = self.db.getActiveLoans(self.activeUser.id) + dbg(loans=loans) + self.btn_show_lentmedia.setText(loans) retdate = self.db.selectClosestReturnDate(self.activeUser.id) if retdate: date = stringToDate(retdate).toString("dd.MM.yyyy") self.nextReturnDate.setText(date) self.nextReturnDate.show() self.label_7.show() - + self.input_file_ident.setEnabled(True) + self.input_file_ident.setPlaceholderText("Buchidentifikation eingeben") + self.input_file_ident.setFocus() def moveToLine(self, line): log.debug("Moving to Line", line) line.setFocus() @@ -253,9 +270,9 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): dialog.exec() return self.mediaAdd(value) - + self.input_file_ident.setFocus() def mediaAdd(self, identifier): - # print("Adding Media", identifier) + dbg("Adding Media", identifier = identifier) self.input_file_ident.clear() self.input_file_ident.setEnabled(False) @@ -282,7 +299,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): elif book_id: if isinstance(book_id, list) and len(book_id) > 1: - # print("Multiple Books found") + print("Multiple Books found") # TODO: implement book selection dialog return else: @@ -324,15 +341,17 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): return else: # print("Book not loaned, loaning now") - self.loanMedia(user_id, book_id, media) + self.loanMedia(user_id, book_id) - def loanMedia(self, user_id, book_id, media): + def loanMedia(self, user_id, book_id): self.db.insertLoan( user_id, book_id[0], self.currentDate.toString("yyyy-MM-dd"), self.duedate.date().toString("yyyy-MM-dd"), ) + media = self.db.getMedia(book_id[0]) + print(media) self.mediaOverview.insertRow(0) self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(media.signature)) self.mediaOverview.setItem(0, 1, QtWidgets.QTableWidgetItem(media.title)) @@ -369,7 +388,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): book_id[0], self.currentDate.toString("yyyy-MM-dd") ) self.mediaOverview.insertRow(0) - self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(book.isbn)) + self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(book.signature)) self.mediaOverview.setItem(0, 1, QtWidgets.QTableWidgetItem(book.title)) self.mediaOverview.setItem( 0, 2, QtWidgets.QTableWidgetItem("Zurückgegeben") @@ -384,6 +403,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_file_ident.clear() else: print("Book not found") + #self.input_file_ident.setPlaceholderText(f"Buch {identifier} nicht gefunden") def setStatusTipMessage(self, message): dialog = QtWidgets.QMessageBox() @@ -397,7 +417,7 @@ def exit_handler(): app = QtWidgets.QApplication(sys.argv) print(backup.backup) # generate report if monday - if datetime.datetime.now().weekday() == 0: + if datetime.datetime.now().weekday() == config.report.report_day: generate_report() dbg("Generated Report") Database().renameInactiveUsers() @@ -427,8 +447,8 @@ def exit_handler(): dialog.setWindowTitle("Backup nicht möglich") dialog.setText("Backup konnte nicht erstellt werden\nGrund: {}".format(reason)) dialog.exec() -def launch(): - app = QtWidgets.QApplication(sys.argv) +def launch(options = None): + app = QtWidgets.QApplication(sys.argv if options is None else [options]) main_ui = MainUI() atexit.register(exit_handler) sys.exit(app.exec()) diff --git a/src/ui/settings.py b/src/ui/settings.py index d3d79c6..5612877 100644 --- a/src/ui/settings.py +++ b/src/ui/settings.py @@ -20,6 +20,11 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.database_path.textChanged.connect(self.enableButtonBox) self.database_name.textChanged.connect(self.enableButtonBox) self.database_name.textChanged.connect(self.enableButtonBox) + self.delete_inactive_user_duration.textChanged.connect(self.enableButtonBox) + self.report_path.textChanged.connect(self.enableButtonBox) + self.report_day.currentIndexChanged.connect(self.enableButtonBox) + self.check_generate_report.stateChanged.connect(self.enableButtonBox) + # buttonbox self.buttonBox.accepted.connect(self.saveSettings) @@ -41,6 +46,10 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): ) self.btn_select_database_path.clicked.connect(self.selectDatabasePath) self.btn_select_database_name.clicked.connect(self.selectDatabaseName) + self.btn_select_report_path.clicked.connect(self.selectReportPath) + #variables + + self.settingschanged = False def enableButtonBox(self): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( @@ -63,6 +72,18 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True ) + + def selectReportPath(self): + reportPath = QtWidgets.QFileDialog.getExistingDirectory( + self, "Select Report Path", self.originalSettings.report.path + ) + self.report_path.setText(reportPath) + self.buttonBox.button( + QtWidgets.QDialogButtonBox.StandardButton.Discard + ).setEnabled(True) + self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( + True + ) def selectDatabasePath(self): databasePath = QtWidgets.QFileDialog.getExistingDirectory( @@ -97,18 +118,26 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): # save settings to config file institution_name = self.institution_name.text() default_loan_duration = int(self.default_loan_duration.text()) + delete_inactive_users = int(self.delete_inactive_user_duration.text()) database_backupLocation = self.database_backupLocation.text() database_path = self.database_path.text() database_name = self.database_name.text() + report_day = self.report_day.currentIndex() + report_generate = self.check_generate_report.isChecked() + report_path = self.report_path.text() # overwrite the original settings self.originalSettings.institution_name = institution_name - self.originalSettings.default_loan_duration = default_loan_duration + self.originalSettings.loan_duration = default_loan_duration self.originalSettings.database.backupLocation = database_backupLocation self.originalSettings.database.path = database_path self.originalSettings.database.name = database_name + self.originalSettings.delete_inactive_user_duration = delete_inactive_users + self.originalSettings.report.report_day = report_day + self.originalSettings.report.path = report_path + self.originalSettings.report.generate_report = report_generate # save the new settings - OmegaConf.save(self.originalSettings, "config/settings.yaml") - + config.save() + self.settingschanged = True self.close() def DiscardSettings(self): @@ -117,14 +146,20 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): def loadSettings(self): self.institution_name.setText(self.originalSettings.institution_name) - self.default_loan_duration.setText( - str(self.originalSettings.default_loan_duration) + self.default_loan_duration.setValue( + int(self.originalSettings.loan_duration) + ) + self.delete_inactive_user_duration.setValue( + int(self.originalSettings.delete_inactive_user_duration) ) self.database_backupLocation.setText( self.originalSettings.database.backupLocation ) self.database_path.setText(self.originalSettings.database.path) self.database_name.setText(self.originalSettings.database.name) + self.report_day.setCurrentIndex(self.originalSettings.report.report_day -1) + self.check_generate_report.setChecked(self.originalSettings.report.generate_report) + self.report_path.setText(self.originalSettings.report.path) pass From 4e117fe84f32567334c6f98e0a3ae871109efa79 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:58:17 +0200 Subject: [PATCH 54/83] filehandler --- filehandle.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 filehandle.py diff --git a/filehandle.py b/filehandle.py new file mode 100644 index 0000000..43c8bdb --- /dev/null +++ b/filehandle.py @@ -0,0 +1,103 @@ +import os +import shutil +from pathlib import Path +ORIGIN = "C:/testing/source_2" +BACKUP = "C:/Databasebackup" +FILE = "database.db" +def handle_file(): + """ + Handles file creation, movement, and backup based on path and backup existence. + + Args: + path (str): The desired path for the file. + filename (str): The name of the file. + backup_location (str): The location for backups. + + Returns: + str: The final path of the file. + """ + full = Path(ORIGIN) / FILE + full_path = str(full) + backup = Path(BACKUP) / FILE + backup_path = str(backup) + origin_reachable = os.path.exists(full_path) + backup_reachable = os.path.exists(backup_path) + print(origin_reachable, backup_reachable) + if not origin_reachable and not backup_reachable: + make_dirs(ORIGIN, BACKUP) + + # if not os.path.exists(full_path): + # print("File does not exist, creating file") + # create_file(full_path) + if os.path.exists(backup_path) and ".backup" in os.listdir(BACKUP): + print("Used backup previously, overwriting file") + #overwrite file at source with backup + shutil.copy(backup_path, full_path) + os.remove(BACKUP + "/.backup") + + +def make_dirs(full_path, backup_path): + """ + Creates directories if they do not exist and moves the file to the desired location. + + Args: + full_path (str): The full path of the file. + backup_path (str): The backup path of the file. + """ + if not os.path.exists(full_path): + os.makedirs(full_path) + if not os.path.exists(backup_path): + os.makedirs(backup_path) + +def create_file(full_path): + """ + Creates a file at the desired location. + + Args: + full_path (str): The full path of the file. + """ + with open(full_path, "w") as f: + f.write("") + +def handle_folder_reachability(original_path, backup_path): + """ + Checks if the original folder is reachable. If not, creates a backup. + If the original folder becomes reachable again, restores the backup. + + Args: + original_path (str): Path to the original folder. + backup_path (str): Path to the backup folder. + + Returns: + str: Path to the current accessible folder. + """ + + backup_file = os.path.join(backup_path, ".backup") + try: + os.makedirs(original_path) + except FileExistsError: + pass + + if not os.path.exists(original_path): + #original folder not reachable, use backup path and create .backup file + if not os.path.exists(backup_path): + os.makedirs(backup_path) + with open(backup_file, "w") as f: + f.write("") + + # Create an empty backup file as a marker + return backup_path +"/" + FILE + + else: + # Original folder is reachable, check for backup + if os.path.exists(backup_file): + # Restore backup + shutil.rmtree(original_path) # Remove original folder to avoid conflicts + shutil.move(backup_path, original_path) + #os.remove(backup_file) + #remove backup file from original path + os.remove(original_path + "/.backup") + os.makedirs(backup_path) + return original_path +"/" + FILE +if __name__=="__main__": + print(handle_folder_reachability(ORIGIN, BACKUP)) # should create a new file at C:/newdatabase/database.db) \ No newline at end of file From 05fe6a1af0a152fe39bda27f479e9e267e22f558 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:58:56 +0200 Subject: [PATCH 55/83] move config to config folder --- config/__init__.py | 1 + config/config.py | 110 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 config/__init__.py create mode 100644 config/config.py diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..3558f42 --- /dev/null +++ b/config/__init__.py @@ -0,0 +1 @@ +from .config import Config \ No newline at end of file diff --git a/config/config.py b/config/config.py new file mode 100644 index 0000000..14ba493 --- /dev/null +++ b/config/config.py @@ -0,0 +1,110 @@ +from typing import Optional + +import omegaconf + +class Config: + _config: Optional[omegaconf.DictConfig] = None + + def __init__(self, config_path: str): + """ + Loads the configuration file and stores it for future access. + + Args: + config_path (str): Path to the YAML configuration file. + """ + self._config = omegaconf.OmegaConf.load(config_path) + + @property + def institution_name(self)->str: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.institution_name + @institution_name.setter + def institution_name(self, value: str): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.institution_name = value + @property + def loan_duration(self)->int: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.default_loan_duration + @loan_duration.setter + def loan_duration(self, value: int): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.default_loan_duration = value + @property + def delete_inactive_user_duration(self)->int: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.inactive_user_deletion + @delete_inactive_user_duration.setter + def delete_inactive_user_duration(self, value: int): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.inactive_user_deletion = value + @property + def catalogue(self)->bool: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.catalogue + @catalogue.setter + def catalogue(self, value: bool): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.catalogue = value + @property + def database(self)->omegaconf.DictConfig: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.database + @database.setter + def database(self, value: omegaconf.DictConfig): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.database = value + @property + def report(self)->omegaconf.DictConfig: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.report + @report.setter + def report(self, value: omegaconf.DictConfig): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.report = value + @property + def debug(self)->bool: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.debug + @debug.setter + def debug(self, value: bool): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.debug = value + @property + def log_debug(self)->bool: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.log_debug + @log_debug.setter + def log_debug(self, value: bool): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.log_debug = value + def save(self): + if self._config is None: + raise RuntimeError("Configuration not loaded") + omegaconf.OmegaConf.save(self._config, "config/settings.yaml") + + + +if __name__ == "__main__": + cfg = Config("config/settings.yaml") + print(cfg.database.path) + cfg.database.path = "nopathhere" + print(cfg.database.path) + cfg.save() + #cfg.updateValue("database.path", "Test") \ No newline at end of file From a8c22778709f5898839b8927efd2769fe10521a5 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:59:28 +0200 Subject: [PATCH 56/83] ui change, bugfix --- src/ui/main_ui.py | 1 - src/ui/sources/Ui_dialog_settings.py | 62 +++++++++------ src/ui/sources/dialog_settings.ui | 108 ++++++++++++++++++--------- src/ui/user.py | 1 - src/utils/config.py | 71 ------------------ 5 files changed, 110 insertions(+), 133 deletions(-) delete mode 100644 src/utils/config.py diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index 3df118c..a438700 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -68,7 +68,6 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): ) self.input_file_ident.setFocus() # self.userdata.textChanged.connect(lambda: self.mode.setText("Ausleihe")) - # self.input_userno. # variables self.db = Database() self.currentDate = QtCore.QDate.currentDate() diff --git a/src/ui/sources/Ui_dialog_settings.py b/src/ui/sources/Ui_dialog_settings.py index 81171d8..e57fe1a 100644 --- a/src/ui/sources/Ui_dialog_settings.py +++ b/src/ui/sources/Ui_dialog_settings.py @@ -21,12 +21,9 @@ class Ui_Dialog(object): self.institution_name = QtWidgets.QLineEdit(parent=Dialog) self.institution_name.setObjectName("institution_name") self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.institution_name) - self.label_2 = QtWidgets.QLabel(parent=Dialog) - self.label_2.setObjectName("label_2") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2) self.label_3 = QtWidgets.QLabel(parent=Dialog) self.label_3.setObjectName("label_3") - self.formLayout.setWidget(3, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_3) + self.formLayout.setWidget(5, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_3) self.databasesettings = QtWidgets.QGridLayout() self.databasesettings.setObjectName("databasesettings") self.database_name = QtWidgets.QLineEdit(parent=Dialog) @@ -56,27 +53,15 @@ class Ui_Dialog(object): self.btn_select_database_backupLocation = QtWidgets.QToolButton(parent=Dialog) self.btn_select_database_backupLocation.setObjectName("btn_select_database_backupLocation") self.databasesettings.addWidget(self.btn_select_database_backupLocation, 2, 2, 1, 1) - self.formLayout.setLayout(3, QtWidgets.QFormLayout.ItemRole.FieldRole, self.databasesettings) - self.label_7 = QtWidgets.QLabel(parent=Dialog) - self.label_7.setObjectName("label_7") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_7) - self.default_loan_duration = QtWidgets.QSpinBox(parent=Dialog) - self.default_loan_duration.setProperty("value", 7) - self.default_loan_duration.setObjectName("default_loan_duration") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.default_loan_duration) - self.delete_inactive_user_duration = QtWidgets.QSpinBox(parent=Dialog) - self.delete_inactive_user_duration.setMaximum(9999) - self.delete_inactive_user_duration.setProperty("value", 365) - self.delete_inactive_user_duration.setObjectName("delete_inactive_user_duration") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.ItemRole.FieldRole, self.delete_inactive_user_duration) + self.formLayout.setLayout(5, QtWidgets.QFormLayout.ItemRole.FieldRole, self.databasesettings) self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Discard|QtWidgets.QDialogButtonBox.StandardButton.Ok) self.buttonBox.setObjectName("buttonBox") - self.formLayout.setWidget(5, QtWidgets.QFormLayout.ItemRole.FieldRole, self.buttonBox) + self.formLayout.setWidget(7, QtWidgets.QFormLayout.ItemRole.FieldRole, self.buttonBox) self.label_9 = QtWidgets.QLabel(parent=Dialog) self.label_9.setObjectName("label_9") - self.formLayout.setWidget(4, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_9) + self.formLayout.setWidget(6, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_9) self.gridLayout = QtWidgets.QGridLayout() self.gridLayout.setObjectName("gridLayout") self.btn_select_report_path = QtWidgets.QToolButton(parent=Dialog) @@ -106,7 +91,36 @@ class Ui_Dialog(object): self.report_day.addItem("") self.report_day.addItem("") self.gridLayout.addWidget(self.report_day, 0, 1, 1, 1) - self.formLayout.setLayout(4, QtWidgets.QFormLayout.ItemRole.FieldRole, self.gridLayout) + self.formLayout.setLayout(6, QtWidgets.QFormLayout.ItemRole.FieldRole, self.gridLayout) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.delete_inactive_user_duration = QtWidgets.QSpinBox(parent=Dialog) + self.delete_inactive_user_duration.setMaximum(9999) + self.delete_inactive_user_duration.setProperty("value", 365) + self.delete_inactive_user_duration.setObjectName("delete_inactive_user_duration") + self.horizontalLayout.addWidget(self.delete_inactive_user_duration) + self.label_12 = QtWidgets.QLabel(parent=Dialog) + self.label_12.setMaximumSize(QtCore.QSize(43, 16777215)) + self.label_12.setObjectName("label_12") + self.horizontalLayout.addWidget(self.label_12) + self.formLayout.setLayout(3, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout) + self.label_7 = QtWidgets.QLabel(parent=Dialog) + self.label_7.setObjectName("label_7") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_7) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.default_loan_duration = QtWidgets.QSpinBox(parent=Dialog) + self.default_loan_duration.setProperty("value", 7) + self.default_loan_duration.setObjectName("default_loan_duration") + self.horizontalLayout_2.addWidget(self.default_loan_duration) + self.label_13 = QtWidgets.QLabel(parent=Dialog) + self.label_13.setMaximumSize(QtCore.QSize(43, 16777215)) + self.label_13.setObjectName("label_13") + self.horizontalLayout_2.addWidget(self.label_13) + self.formLayout.setLayout(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout_2) + self.label_2 = QtWidgets.QLabel(parent=Dialog) + self.label_2.setObjectName("label_2") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2) self.retranslateUi(Dialog) self.buttonBox.accepted.connect(Dialog.accept) # type: ignore @@ -123,7 +137,6 @@ class Ui_Dialog(object): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "Dialog")) self.label.setText(_translate("Dialog", "Name der Einrichtung")) - self.label_2.setText(_translate("Dialog", "Leihdauer in Tagen")) self.label_3.setText(_translate("Dialog", "Datenbank")) self.label_4.setText(_translate("Dialog", "Speicherort")) self.label_6.setText(_translate("Dialog", "Sicherungspfad")) @@ -131,8 +144,6 @@ class Ui_Dialog(object): self.btn_select_database_path.setText(_translate("Dialog", "...")) self.btn_select_database_name.setText(_translate("Dialog", "...")) self.btn_select_database_backupLocation.setText(_translate("Dialog", "...")) - self.label_7.setText(_translate("Dialog", "Inaktive Nutzer\n" -"Löschen nach")) self.label_9.setText(_translate("Dialog", "Bericht")) self.btn_select_report_path.setText(_translate("Dialog", "...")) self.check_generate_report.setText(_translate("Dialog", "Bericht erstellen")) @@ -143,3 +154,8 @@ class Ui_Dialog(object): self.report_day.setItemText(2, _translate("Dialog", "Mittwoch")) self.report_day.setItemText(3, _translate("Dialog", "Donnerstag")) self.report_day.setItemText(4, _translate("Dialog", "Freitag")) + self.label_12.setText(_translate("Dialog", "Tage(n)")) + self.label_7.setText(_translate("Dialog", "Inaktive Nutzer\n" +"Löschen nach")) + self.label_13.setText(_translate("Dialog", "Tage(n)")) + self.label_2.setText(_translate("Dialog", "Leihdauer")) diff --git a/src/ui/sources/dialog_settings.ui b/src/ui/sources/dialog_settings.ui index c41caff..70430d4 100644 --- a/src/ui/sources/dialog_settings.ui +++ b/src/ui/sources/dialog_settings.ui @@ -24,21 +24,14 @@ - - - - Leihdauer in Tagen - - - - + Datenbank - + @@ -93,32 +86,7 @@ - - - - Inaktive Nutzer -Löschen nach - - - - - - - 7 - - - - - - - 9999 - - - 365 - - - - + Qt::Horizontal @@ -128,14 +96,14 @@ Löschen nach - + Bericht - + @@ -206,6 +174,72 @@ Löschen nach + + + + + + 9999 + + + 365 + + + + + + + + 43 + 16777215 + + + + Tage(n) + + + + + + + + + Inaktive Nutzer +Löschen nach + + + + + + + + + 7 + + + + + + + + 43 + 16777215 + + + + Tage(n) + + + + + + + + + Leihdauer + + + diff --git a/src/ui/user.py b/src/ui/user.py index e28b2be..2076cd8 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -79,7 +79,6 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): # print(signature) self.db.extendLoanDuration(signature, extendDate) self.userMedia = [] - break self.UserMediaTable.setRowCount(0) self.loadMedia() return diff --git a/src/utils/config.py b/src/utils/config.py deleted file mode 100644 index 67d004f..0000000 --- a/src/utils/config.py +++ /dev/null @@ -1,71 +0,0 @@ -from typing import Optional - -import omegaconf - -class Config: - _config: Optional[omegaconf.DictConfig] = None - - def __init__(self, config_path: str): - """ - Loads the configuration file and stores it for future access. - - Args: - config_path (str): Path to the YAML configuration file. - """ - self._config = omegaconf.OmegaConf.load(config_path) - - @property - def institution_name(self)->str: - if self._config is None: - raise RuntimeError("Configuration not loaded") - return self._config.institution_name - @property - def loan_duration(self)->int: - if self._config is None: - raise RuntimeError("Configuration not loaded") - return self._config.default_loan_duration - @property - def delete_inactive_user_duration(self)->int: - if self._config is None: - raise RuntimeError("Configuration not loaded") - return self._config.inactive_user_deletion - @property - def catalogue(self)->bool: - if self._config is None: - raise RuntimeError("Configuration not loaded") - return self._config.catalogue - @property - def database(self)->omegaconf.DictConfig: - if self._config is None: - raise RuntimeError("Configuration not loaded") - return self._config.database - - @property - def report(self)->omegaconf.DictConfig: - if self._config is None: - raise RuntimeError("Configuration not loaded") - return self._config.report - @property - def debug(self)->bool: - if self._config is None: - raise RuntimeError("Configuration not loaded") - return self._config.debug - @property - def log_debug(self)->bool: - if self._config is None: - raise RuntimeError("Configuration not loaded") - return self._config.log_debug - - - def updateValue(self, key: str, value: str): - self._config[key] = value - omegaconf.OmegaConf.save(self._config, "config/settings.yaml") - - - - -if __name__ == "__main__": - cfg = Config("config/settings.yaml") - print(cfg.database.path) - - #cfg.updateValue("database.path", "Test") \ No newline at end of file From a071f700594e467e5ef1336b1e949009d244b4e4 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:02:36 +0200 Subject: [PATCH 57/83] updates --- .vscode/tasks.json | 111 +++++++++++++++++++++++++++++++++++++++++ build.app.json | 77 ++++++++++++++++++++++++++++ build.debug.app.json | 77 ++++++++++++++++++++++++++++ build.release.app.json | 77 ++++++++++++++++++++++++++++ config/config.py | 26 ++++++++-- main.py | 1 + main_dev.py | 5 ++ 7 files changed, 369 insertions(+), 5 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 build.app.json create mode 100644 build.debug.app.json create mode 100644 build.release.app.json create mode 100644 main_dev.py diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..512893a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,111 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build LibrarySystem (Debug)", + "type": "shell", + "command": "pyinstaller", + "args": [ + "--noconfirm", + "--onedir", + "--console", + "--icon", + "'${config:ApplicationIconPath}'", + "--name", + "LibrarySystem-debug", + "--contents-directory", + ".", + "--clean", + "--add-data", + "'${config:configPath};config/'", + "--add-data", + "'${config:iconsPath};icons/'", + "'${workspaceFolder}/main_dev.py'" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new", + "focus": false + }, + "dependsOn": "Compress dist Folder Debug", + "problemMatcher": "$pyinstaller" + }, + { + "label": "Build LibrarySystem (Release)", + "type": "shell", + "command": "pyinstaller", + "args": [ + "--noconfirm", + "--onedir", + "--windowed", + "--name", + "LibrarySystem", + "--contents-directory", + ".", + "--clean", + "--add-data", + "'${config:configPath};config/'", + "--add-data", + "'${config:iconsPath};icons/'", + "--icon", + "'${config:ApplicationIconPath}'", + "'${workspaceFolder}/main.py'" + ], + "group": { + "kind": "build", + "isDefault": false + }, + "presentation": { + "reveal": "always", + "panel": "new", + "focus": false + }, + "dependsOn": "Compress dist Folder Release", + "problemMatcher": "$pyinstaller" + }, + { + "label": "Run LibrarySystem (live)", + "type": "shell", + "command": "c:/Users/aky547/GitHub/LibrarySystem/.venv/Scripts/python.exe", + "args": [ + "'c:/Users/aky547/GitHub/LibrarySystem/main_dev.py'", + "--ic-logging" + ], + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": "$python" + }, + { + "label": "Compress dist Folder Debug", + "type": "shell", + "command": "Compress-Archive", + "args": [ + "-Path", + "${workspaceFolder}/dist/LibrarySystem-debug/", + "-DestinationPath", + "${workspaceFolder}/output/LibrarySystem-debug.zip", + ], + "group": "build", + "presentation": "panel" + }, + { + "label": "Compress dist Folder Release", + "type": "shell", + "command": "Compress-Archive", + "args": [ + "-Path", + "${workspaceFolder}/dist/LibrarySystem/", + "-DestinationPath", + "${workspaceFolder}/output/LibrarySystem.zip", + ], + "group": "build", + "presentation": "panel" + } + ] +} \ No newline at end of file diff --git a/build.app.json b/build.app.json new file mode 100644 index 0000000..813c03a --- /dev/null +++ b/build.app.json @@ -0,0 +1,77 @@ +{ + "version": "auto-py-to-exe-configuration_v1", + "pyinstallerOptions": [ + { + "optionDest": "noconfirm", + "value": true + }, + { + "optionDest": "filenames", + "value": "C:/Users/aky547/GitHub/LibrarySystem/main.py" + }, + { + "optionDest": "onefile", + "value": false + }, + { + "optionDest": "console", + "value": false + }, + { + "optionDest": "icon_file", + "value": "C:/Users/aky547/Downloads/1490971308-map-icons-7_82746.ico" + }, + { + "optionDest": "name", + "value": "LibrarySystem" + }, + { + "optionDest": "contents_directory", + "value": "." + }, + { + "optionDest": "clean_build", + "value": true + }, + { + "optionDest": "strip", + "value": false + }, + { + "optionDest": "noupx", + "value": false + }, + { + "optionDest": "disable_windowed_traceback", + "value": false + }, + { + "optionDest": "uac_admin", + "value": false + }, + { + "optionDest": "uac_uiaccess", + "value": false + }, + { + "optionDest": "argv_emulation", + "value": false + }, + { + "optionDest": "bootloader_ignore_signals", + "value": false + }, + { + "optionDest": "datas", + "value": "C:/Users/aky547/GitHub/LibrarySystem/config;config/" + }, + { + "optionDest": "datas", + "value": "C:/Users/aky547/GitHub/LibrarySystem/icons;icons/" + } + ], + "nonPyinstallerOptions": { + "increaseRecursionLimit": true, + "manualArguments": "" + } +} \ No newline at end of file diff --git a/build.debug.app.json b/build.debug.app.json new file mode 100644 index 0000000..0e01b4b --- /dev/null +++ b/build.debug.app.json @@ -0,0 +1,77 @@ +{ + "version": "auto-py-to-exe-configuration_v1", + "pyinstallerOptions": [ + { + "optionDest": "noconfirm", + "value": true + }, + { + "optionDest": "filenames", + "value": "C:/Users/aky547/GitHub/LibrarySystem/main_dev.py" + }, + { + "optionDest": "onefile", + "value": false + }, + { + "optionDest": "console", + "value": true + }, + { + "optionDest": "icon_file", + "value": "C:/Users/aky547/Downloads/1490971308-map-icons-7_82746.ico" + }, + { + "optionDest": "name", + "value": "LibrarySystem-debug" + }, + { + "optionDest": "contents_directory", + "value": "." + }, + { + "optionDest": "clean_build", + "value": true + }, + { + "optionDest": "strip", + "value": false + }, + { + "optionDest": "noupx", + "value": false + }, + { + "optionDest": "disable_windowed_traceback", + "value": false + }, + { + "optionDest": "uac_admin", + "value": false + }, + { + "optionDest": "uac_uiaccess", + "value": false + }, + { + "optionDest": "argv_emulation", + "value": false + }, + { + "optionDest": "bootloader_ignore_signals", + "value": false + }, + { + "optionDest": "datas", + "value": "C:/Users/aky547/GitHub/LibrarySystem/config;config/" + }, + { + "optionDest": "datas", + "value": "C:/Users/aky547/GitHub/LibrarySystem/icons;icons/" + } + ], + "nonPyinstallerOptions": { + "increaseRecursionLimit": true, + "manualArguments": "" + } +} \ No newline at end of file diff --git a/build.release.app.json b/build.release.app.json new file mode 100644 index 0000000..813c03a --- /dev/null +++ b/build.release.app.json @@ -0,0 +1,77 @@ +{ + "version": "auto-py-to-exe-configuration_v1", + "pyinstallerOptions": [ + { + "optionDest": "noconfirm", + "value": true + }, + { + "optionDest": "filenames", + "value": "C:/Users/aky547/GitHub/LibrarySystem/main.py" + }, + { + "optionDest": "onefile", + "value": false + }, + { + "optionDest": "console", + "value": false + }, + { + "optionDest": "icon_file", + "value": "C:/Users/aky547/Downloads/1490971308-map-icons-7_82746.ico" + }, + { + "optionDest": "name", + "value": "LibrarySystem" + }, + { + "optionDest": "contents_directory", + "value": "." + }, + { + "optionDest": "clean_build", + "value": true + }, + { + "optionDest": "strip", + "value": false + }, + { + "optionDest": "noupx", + "value": false + }, + { + "optionDest": "disable_windowed_traceback", + "value": false + }, + { + "optionDest": "uac_admin", + "value": false + }, + { + "optionDest": "uac_uiaccess", + "value": false + }, + { + "optionDest": "argv_emulation", + "value": false + }, + { + "optionDest": "bootloader_ignore_signals", + "value": false + }, + { + "optionDest": "datas", + "value": "C:/Users/aky547/GitHub/LibrarySystem/config;config/" + }, + { + "optionDest": "datas", + "value": "C:/Users/aky547/GitHub/LibrarySystem/icons;icons/" + } + ], + "nonPyinstallerOptions": { + "increaseRecursionLimit": true, + "manualArguments": "" + } +} \ No newline at end of file diff --git a/config/config.py b/config/config.py index 14ba493..dbecbf7 100644 --- a/config/config.py +++ b/config/config.py @@ -4,7 +4,7 @@ import omegaconf class Config: _config: Optional[omegaconf.DictConfig] = None - + def __init__(self, config_path: str): """ Loads the configuration file and stores it for future access. @@ -94,17 +94,33 @@ class Config: if self._config is None: raise RuntimeError("Configuration not loaded") self._config.log_debug = value + @property + def ic_logging(self)->bool: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.ic_logging + @ic_logging.setter + def ic_logging(self, value:bool): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.ic_logging = value def save(self): if self._config is None: raise RuntimeError("Configuration not loaded") omegaconf.OmegaConf.save(self._config, "config/settings.yaml") - - + def apply_options(options:list): + if self._config is None: + raise RuntimeError("Configuration not loaded") + for option in options: + if option in self._config: + self._config[option] = True + else: + raise KeyError(f"Option {option} not found in configuration") if __name__ == "__main__": cfg = Config("config/settings.yaml") - print(cfg.database.path) + #print(cfg.database.path) cfg.database.path = "nopathhere" - print(cfg.database.path) + #print(cfg.database.path) cfg.save() #cfg.updateValue("database.path", "Test") \ No newline at end of file diff --git a/main.py b/main.py index 1bce9d3..c47644c 100644 --- a/main.py +++ b/main.py @@ -2,3 +2,4 @@ from src.ui.main_ui import launch if __name__ == "__main__": launch() + diff --git a/main_dev.py b/main_dev.py new file mode 100644 index 0000000..295af3d --- /dev/null +++ b/main_dev.py @@ -0,0 +1,5 @@ +from src.ui.main_ui import launch + +if __name__ == "__main__": + launch("--debug") + From 2bf382098ffafe5d9503a8ac1901b36db45d5555 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:42:22 +0200 Subject: [PATCH 58/83] add bare settings --- config/settings.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/config/settings.yaml b/config/settings.yaml index ffa76b5..67df6fd 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -1,15 +1,17 @@ -institution_name: HB Testbibliothek Psychologie +institution_name: default_loan_duration: 7 inactive_user_deletion: 365 catalogue: True database: - path: C:/testing/source_2 + path: name: library.db - backupLocation: C:/Databasebackup + backupLocation: do_backup: True report: generate_report: false - path: C:/report + path: + report_day: 1 debug: false log_debug: false +ic_logging: false From f5bb5d3adc08789c3544bc778a59874c826c49bd Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:42:09 +0200 Subject: [PATCH 59/83] add check for new db location, fixes #2 --- src/ui/settings.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ui/settings.py b/src/ui/settings.py index 5612877..9590287 100644 --- a/src/ui/settings.py +++ b/src/ui/settings.py @@ -3,6 +3,7 @@ from PyQt6 import QtWidgets, QtCore from src.utils import Icon from src import config from omegaconf import OmegaConf +import os class Settings(QtWidgets.QDialog, Ui_Dialog): @@ -125,6 +126,9 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): report_day = self.report_day.currentIndex() report_generate = self.check_generate_report.isChecked() report_path = self.report_path.text() + if database_path != self.originalSettings.database.path : + os.makedirs(database_path, exist_ok=True) + self.restart() # overwrite the original settings self.originalSettings.institution_name = institution_name self.originalSettings.loan_duration = default_loan_duration @@ -139,6 +143,18 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): config.save() self.settingschanged = True self.close() + + def restart(self): + dialog = QtWidgets.QMessageBox() + dialog.setIcon(QtWidgets.QMessageBox.Icon.Information) + dialog.setText("Neustart erforderlich") + dialog.setInformativeText( + "Das Programm muss neu gestartet werden, um die neue Datenbank zu verwenden." + ) + dialog.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok) + dialog.setDefaultButton(QtWidgets.QMessageBox.StandardButton.Ok) + dialog.setWindowTitle("Neustart erforderlich") + dialog.exec() def DiscardSettings(self): self.loadSettings() From e5816b40d2f8d1f791bfd41a60eeb711622d1f48 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:51:44 +0200 Subject: [PATCH 60/83] closes #3, fix bug with user creation --- src/logic/database.py | 25 ++++++++++++------- src/ui/main_ui.py | 56 +++++++++++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/src/logic/database.py b/src/logic/database.py index 6c8f216..2593398 100644 --- a/src/logic/database.py +++ b/src/logic/database.py @@ -25,12 +25,12 @@ class Database: try: os.makedirs(config.database.path) except FileNotFoundError: - print(self.db_path) + dbg(self.db_path) if not os.path.exists(config.database.backupLocation): os.makedirs(config.database.backupLocation) #if main path does not exist, try to create it. if that fails, use the backuplocation - print(self.db_path) + dbg(self.db_path) self.checkDatabaseStatus() def handle_folder_reachability(self, original_path, backup_path): @@ -59,7 +59,7 @@ class Database: return backup_path +"/" + FILE else: - print("Original Path Exists, ") + dbg("Original Path Exists, using this path") # Original folder is reachable, check for backup if os.path.exists(backup_file): # Restore backup @@ -125,7 +125,7 @@ class Database: def createDatabase(self): log.info("Creating Database") - # print("Creating Database") + #print("Creating Database") if not os.path.exists(self.db_path): os.makedirs(self.db_path) conn = self.connect() @@ -197,6 +197,15 @@ class Database: return False self.close_connection(conn) return True + + def getUserBy(self, key, value) -> User: + conn = self.connect() + cursor = conn.cursor() + cursor.execute(f"SELECT * FROM users WHERE {key} = '{value}'") + result = cursor.fetchone() + self.close_connection(conn) + user = User(userid=result[1], username=result[2], email=result[3], id=result[0]) + return user def getUser(self, user_id) -> User: conn = self.connect() @@ -204,9 +213,9 @@ class Database: cursor.execute(f"SELECT * FROM users") result = cursor.fetchall() self.close_connection(conn) - print(result) + for res in result: - if res[0] == user_id: + if res[1] == user_id: user = User(userid=res[1], username=res[2], email=res[3], id=res[0]) dbg(f"Returning User {user}") log.info(f"Returning User {user}") @@ -332,7 +341,7 @@ class Database: cursor.execute(query) result = cursor.fetchone() signature = result[1] - # print(signature) + #print(signature) query = f"SELECT * FROM media WHERE signature LIKE '%{signature}%'" cursor.execute(query) result = cursor.fetchall() @@ -452,7 +461,7 @@ class Database: cursor = conn.cursor() cursor.execute(query) result = cursor.fetchone() - print(result) + dbg("Result", response=result) self.close_connection(conn) if result is not None: return result[0] diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index a438700..cbdbffd 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -106,14 +106,16 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): log.info("Showing Settings") settings = Settings() settings.exec() - if settings.settingschanged: # reload settings - print(config) + #print(config) def changeMode(self): log.info("Changing Mode") dbg(f"Current mode: {self.activeState}") self.input_username.clear() + stayReturn = False + if self.userdata.toPlainText() != "": + stayReturn = True self.userdata.clear() self.input_userno.clear() self.btn_show_lentmedia.setText("") @@ -123,7 +125,10 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.mediaOverview.setRowCount(0) self.activeUser = None #! remove if last user should be kept if self.activeState == "Rückgabe": - self.activateLoanMode() + if stayReturn: + self.activateReturnMode() + else: self.activateLoanMode() + else: self.activateReturnMode() @@ -190,9 +195,10 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): user = CreateUser(fieldname="id", data="") user.exec() userid = user.userid + print(userid) if userid: log.info(f"User created {userid}") - data = self.db.getUser(userid) + data = self.db.getUserBy("user_id", userid) self.activeUser = data # set user to active user self.setUserData() @@ -204,7 +210,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def checkUser(self, fieldname, data): log.info(f"Checking User {fieldname}, {data}") - # print("Checking User", fieldname, data) + #print("Checking User", fieldname, data) # set fieldname as key and data as variable user = self.db.checkUserExists(fieldname, data) if not user: @@ -224,16 +230,16 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): multi.exec() self.activeUser = multi.userdata else: - # print("User found", user[0]) + #print("User found", user[0]) self.activeUser = user[0] if self.activeUser is not None: log.info(f"User found {self.activeUser}") - # print("User found", self.activeUser) + #print("User found", self.activeUser) self.setUserData() self.input_file_ident.setFocus() self.mode.setText("Ausleihe") - print(self.activeUser.__dict__) + #print(self.activeUser.__dict__) loans = self.db.getActiveLoans(self.activeUser.id) dbg(loans=loans) self.btn_show_lentmedia.setText(loans) @@ -298,7 +304,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): elif book_id: if isinstance(book_id, list) and len(book_id) > 1: - print("Multiple Books found") + #print("Multiple Books found") # TODO: implement book selection dialog return else: @@ -307,7 +313,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): # check if book is already loaned loaned = self.db.checkLoanState(book_id[0]) if loaned: - # print("Book already loaned") + #print("Book already loaned") self.setStatusTipMessage("Buch bereits entliehen") # dialog with yes no to create new entry dialog = QtWidgets.QMessageBox() @@ -329,7 +335,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): newentry = NewEntry([book_id[0]]) newentry.exec() self.setStatusTipMessage("Neues Exemplar hinzugefügt") - # print(created_ids) + #print(created_ids) self.input_file_ident.setEnabled(True) newentries = newentry.newIds if newentries: @@ -339,7 +345,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): dbg("inserted duplicated book into database") return else: - # print("Book not loaned, loaning now") + #print("Book not loaned, loaning now") self.loanMedia(user_id, book_id) def loanMedia(self, user_id, book_id): @@ -350,7 +356,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.duedate.date().toString("yyyy-MM-dd"), ) media = self.db.getMedia(book_id[0]) - print(media) + #print(media) self.mediaOverview.insertRow(0) self.mediaOverview.setItem(0, 0, QtWidgets.QTableWidgetItem(media.signature)) self.mediaOverview.setItem(0, 1, QtWidgets.QTableWidgetItem(media.title)) @@ -366,19 +372,19 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_file_ident.setEnabled(True) def returnMedia(self, identifier): - # print("Returning Media", identifier) + #print("Returning Media", identifier) # get book id from database # self. identifier = Book( isbn=identifier, title=identifier, signature=identifier, ppn=identifier ) book_id = self.db.checkMediaExists(identifier) - # print(book_id) + #print(book_id) if book_id: # check if book is already loaned loaned = self.db.checkLoanState(book_id[0]) if loaned: - # print("Book already loaned, returning now") + #print("Book already loaned, returning now") user = self.db.getUserByLoan(book_id[0]) # set userdata in lineedits self.activeUser = user @@ -397,11 +403,12 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.db.getActiveLoans(self.activeUser.id) ) else: - # print("Book not loaned") + #print("Book not loaned") self.setStatusTipMessage("Buch nicht entliehen") self.input_file_ident.clear() else: - print("Book not found") + dbg("Book not found") + #print("Book not found") #self.input_file_ident.setPlaceholderText(f"Buch {identifier} nicht gefunden") def setStatusTipMessage(self, message): @@ -414,7 +421,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def exit_handler(): dbg("Exiting, creating backup") app = QtWidgets.QApplication(sys.argv) - print(backup.backup) + #print(backup.backup) # generate report if monday if datetime.datetime.now().weekday() == config.report.report_day: generate_report() @@ -446,8 +453,15 @@ def exit_handler(): dialog.setWindowTitle("Backup nicht möglich") dialog.setText("Backup konnte nicht erstellt werden\nGrund: {}".format(reason)) dialog.exec() -def launch(options = None): - app = QtWidgets.QApplication(sys.argv if options is None else [options]) +def launch(*argv): + options = sys.argv + if argv: + options += [arg for arg in argv] + options = [arg for arg in options if arg.startswith("--")] + #print("Launching Main UI") + #print(options) + + app = QtWidgets.QApplication([]) main_ui = MainUI() atexit.register(exit_handler) sys.exit(app.exec()) From e7efdc8481690cd960d87d0570d9512ac0ad4e3d Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:48:02 +0200 Subject: [PATCH 61/83] changes --- icons/library_shelf.png | Bin 0 -> 2653 bytes src/ui/extendLoan.py | 4 ++-- src/ui/multiUserInfo.py | 2 +- src/ui/newentry.py | 4 ++-- src/ui/reportUi.py | 2 +- src/ui/user.py | 12 ++++++------ src/utils/createReport.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 icons/library_shelf.png diff --git a/icons/library_shelf.png b/icons/library_shelf.png new file mode 100644 index 0000000000000000000000000000000000000000..cb1961f452b2b64434711c434af685c7b03f5baf GIT binary patch literal 2653 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 literal 0 HcmV?d00001 diff --git a/src/ui/extendLoan.py b/src/ui/extendLoan.py index 39e18e4..82e43ef 100644 --- a/src/ui/extendLoan.py +++ b/src/ui/extendLoan.py @@ -17,9 +17,9 @@ class ExtendLoan(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.accepted.connect(self.extendLoan) def extendLoan(self): - # print("Extend Loan") + #print("Extend Loan") selectedDate = self.extenduntil.selectedDate() - # print(selectedDate) + #print(selectedDate) self.extendDate = selectedDate self.close() diff --git a/src/ui/multiUserInfo.py b/src/ui/multiUserInfo.py index ff03a85..6a4ede0 100644 --- a/src/ui/multiUserInfo.py +++ b/src/ui/multiUserInfo.py @@ -27,7 +27,7 @@ class MultiUserFound(QtWidgets.QDialog, Ui_Dialog): self.tableWidget.cellClicked.connect(self.selectUser) def selectUser(self, row, column): - # print(row, column) + #print(row, column) user = User( id=self.tableWidget.item(row, 0).text(), username=self.tableWidget.item(row, 1).text(), diff --git a/src/ui/newentry.py b/src/ui/newentry.py index 3a7b437..163e370 100644 --- a/src/ui/newentry.py +++ b/src/ui/newentry.py @@ -40,7 +40,7 @@ class NewEntry(QtWidgets.QDialog, Ui_Dialog): ) def populateTable(self): for title in self.titles: - # print(title) + #print(title) entries = self.db.getMediaSimilarSignatureByID(title) # sort by signature entries.sort(key=lambda x: x.signature, reverse=True) @@ -66,7 +66,7 @@ class NewEntry(QtWidgets.QDialog, Ui_Dialog): signature=signature, ppn=eval(ppn), ) - # print(book) + #print(book) if not self.db.checkMediaExists(book): newBookId = self.db.insertMedia(book) self.newIds.append(newBookId) diff --git a/src/ui/reportUi.py b/src/ui/reportUi.py index 9a1695c..2ca07b3 100644 --- a/src/ui/reportUi.py +++ b/src/ui/reportUi.py @@ -90,7 +90,7 @@ class ReportUi(QtWidgets.QDialog, Ui_Dialog): def report_generated(self): self.reportlink.setOpenExternalLinks(True) fileformat = self.rthread.format - print(fileformat) + #print(fileformat) self.reportlink.setText( f'Report' ) diff --git a/src/ui/user.py b/src/ui/user.py index 2076cd8..f74aca7 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -72,11 +72,11 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): extend.exec() if extend.result() == 1: extendDate = extend.extendDate.toString("yyyy-MM-dd") - # # print columns of selected rows + # #print columns of selected rows for item in self.UserMediaTable.selectedItems(): if item.column() == 1: signature = item.text() - # print(signature) + #print(signature) self.db.extendLoanDuration(signature, extendDate) self.userMedia = [] self.UserMediaTable.setRowCount(0) @@ -91,7 +91,7 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): self.UserMediaTable.setRowCount(0) for loan in self.userMedia: - # print("looping loans") + #print("looping loans") fielddata = eval(f"loan.{searchfield}") if isinstance(fielddata, str): fielddata = fielddata.lower() @@ -141,12 +141,12 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): if self.radio_currentlyLoaned.isChecked() else "overdue" ) - print(mode) + #print(mode) if self.userMedia == []: books = self.db.getAllMedia(self.user_id) for book in books: self.userMedia.append(book) - # print(self.userMedia) + #print(self.userMedia) self.UserMediaTable.setRowCount(0) for book in self.userMedia: @@ -166,7 +166,7 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): continue self.addBookToTable(book) - print(book.title) + #print(book.title) def addBookToTable(self, book): self.UserMediaTable.insertRow(0) # item0 = isbn diff --git a/src/utils/createReport.py b/src/utils/createReport.py index 0c94f16..8c914d8 100644 --- a/src/utils/createReport.py +++ b/src/utils/createReport.py @@ -23,7 +23,7 @@ def generate_report(): report_path = os.path.join(config.report.path, f"report_{year}_{week}.tsv") day = QDate.currentDate().addDays(-7).toString("yyyy-MM-dd") query = f"""SELECT * FROM loans WHERE loan_date >= '{day}';""" - # print(query) + #print(query) colnames = ["UserId", "Title", "Action", "Datum"] table = PrettyTable(colnames) @@ -49,7 +49,7 @@ def generate_report(): loan_action_date, ] ) - # # print(table) + # #print(table) # # wruitng the table to a file # with open("report.txt", "w", encoding="utf-8") as f: # f.write(str(table)) From 114d6fb1ca5bb48bb21c6b3502253705404e28c4 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:55:27 +0200 Subject: [PATCH 62/83] fix return_date bug, should have been returned_date --- src/ui/loans.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ui/loans.py b/src/ui/loans.py index f50488b..ce86e52 100644 --- a/src/ui/loans.py +++ b/src/ui/loans.py @@ -28,7 +28,7 @@ class LoanWindow(QtWidgets.QMainWindow, Ui_MainWindow): # lineedits self.searchbar.textChanged.connect(self.limitResults) - self.searchbar.returnPressed.connect(self.passThis) + self.searchbar.returnPressed.connect(self.selfpass) # radio buttons self.radio_all.clicked.connect(self.filterResults) @@ -39,16 +39,17 @@ class LoanWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.loanTable.doubleClicked.connect(self.showUser) self.show() - def passThis(self): + def selfpass(self): pass + def insertRow(self, data): dbg(contents=data) - retdate = ( - stringToDate(data.return_date).toString("dd.MM.yyyy") - if data.return_date != "" - else "" - ) + + retdate = "" + if data.returned_date != "": + retdate = stringToDate(data.returned_date).toString("dd.MM.yyyy") + ic(retdate) self.loanTable.insertRow(0) self.loanTable.setItem(0, 0, QtWidgets.QTableWidgetItem(data.book.isbn)) self.loanTable.setItem(0, 1, QtWidgets.QTableWidgetItem(data.book.signature)) From 4164da4f557b06953fdbd7d7d128bd5bf794a84a Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:21:26 +0200 Subject: [PATCH 63/83] feat: add option to change shortcuts, set return modes --- config/config.py | 24 ++++ src/ui/settings.py | 94 +++++++++--- src/ui/sources/Ui_dialog_settings.py | 115 +++++++++------ src/ui/sources/Ui_dialog_settings.ui.py | 2 +- src/ui/sources/Ui_main_UserInterface.py | 31 +--- src/ui/sources/dialog_settings.ui | 183 +++++++++++++++--------- src/ui/sources/main_UserInterface.ui | 45 +----- 7 files changed, 303 insertions(+), 191 deletions(-) diff --git a/config/config.py b/config/config.py index dbecbf7..a3c62bb 100644 --- a/config/config.py +++ b/config/config.py @@ -54,6 +54,30 @@ class Config: if self._config is None: raise RuntimeError("Configuration not loaded") self._config.catalogue = value + + @property + def shortcuts(self)->omegaconf.DictConfig: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.shortcuts + @shortcuts.setter + def shortcuts(self, value: omegaconf.DictConfig): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.shortcuts = value + + @property + def advanced_refresh(self)->omegaconf.DictConfig: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.advanced_refresh + + @advanced_refresh.setter + def advanced_refresh(self, value: omegaconf.DictConfig): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.advanced_refresh = value + @property def database(self)->omegaconf.DictConfig: if self._config is None: diff --git a/src/ui/settings.py b/src/ui/settings.py index 9590287..c3b77a6 100644 --- a/src/ui/settings.py +++ b/src/ui/settings.py @@ -6,26 +6,23 @@ from omegaconf import OmegaConf import os + + + + + class Settings(QtWidgets.QDialog, Ui_Dialog): def __init__(self): super(Settings, self).__init__() self.setupUi(self) self.setWindowTitle("Einstellungen") self.setWindowIcon(Icon("settings").icon) + #variables self.originalSettings = config + self.shortcuts = config.shortcuts + self.settingschanged = False - # lineedits - self.institution_name.textChanged.connect(self.enableButtonBox) - self.default_loan_duration.textChanged.connect(self.enableButtonBox) - self.database_backupLocation.textChanged.connect(self.enableButtonBox) - self.database_path.textChanged.connect(self.enableButtonBox) - self.database_name.textChanged.connect(self.enableButtonBox) - self.database_name.textChanged.connect(self.enableButtonBox) - self.delete_inactive_user_duration.textChanged.connect(self.enableButtonBox) - self.report_path.textChanged.connect(self.enableButtonBox) - self.report_day.currentIndexChanged.connect(self.enableButtonBox) - self.check_generate_report.stateChanged.connect(self.enableButtonBox) - + self.populateShortcuts() # buttonbox self.buttonBox.accepted.connect(self.saveSettings) @@ -48,10 +45,42 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.btn_select_database_path.clicked.connect(self.selectDatabasePath) self.btn_select_database_name.clicked.connect(self.selectDatabaseName) self.btn_select_report_path.clicked.connect(self.selectReportPath) - #variables + self.returnMode.clicked.connect(self.returnModeSetting) + + + #other + #stretch columns + self.shortcutchanger.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch) + def returnModeSetting(self): + currentstate = self.returnMode.isChecked() + if self.originalSettings.advanced_refresh != currentstate: + self.enableButtonBox() + + def populateShortcuts(self): + for shortcut in self.shortcuts: + name = shortcut["name"] + default = shortcut["default"] + current = shortcut["current"] + self.addShortcut(name, default, current) + #assume the shortcuts will be changed + self.settingschanged = True + self.enableButtonBox() + + def addShortcut(self, name, default, current): + #remove all pages from shortcutchanger + #add new page with name, default and current + + self.shortcutchanger.insertRow(0) + self.shortcutchanger.setItem(0, 0, QtWidgets.QTableWidgetItem(name)) + self.shortcutchanger.setItem(0, 1, QtWidgets.QTableWidgetItem(default)) + #add keysequenceedit + keysequenceedit = QtWidgets.QKeySequenceEdit() + keysequenceedit.setKeySequence(current) + self.shortcutchanger.setCellWidget(0, 2, keysequenceedit) + + + - self.settingschanged = False - def enableButtonBox(self): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True @@ -73,7 +102,8 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True ) - + self.settingschanged = False + def selectReportPath(self): reportPath = QtWidgets.QFileDialog.getExistingDirectory( self, "Select Report Path", self.originalSettings.report.path @@ -85,6 +115,9 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True ) + self.settingschanged = False + + def selectDatabasePath(self): databasePath = QtWidgets.QFileDialog.getExistingDirectory( @@ -97,6 +130,8 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True ) + self.settingschanged = False + def selectDatabaseName(self): # filepicker with filter to select only .db files if a file is selected, set name to the lineedit and set database_path @@ -114,6 +149,22 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True ) + self.settingschanged = False + + def getShortcuts(self): + shortcuts = [] + for row in range(self.shortcutchanger.rowCount()): + name = self.shortcutchanger.item(row, 0).text() + default = self.shortcutchanger.item(row, 1).text() + current = self.shortcutchanger.cellWidget(row, 2).keySequence().toString() + shortcuts.append( + { + "name": name, + "default": default, + "current": current, + } + ) + return shortcuts def saveSettings(self): # save settings to config file @@ -126,6 +177,12 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): report_day = self.report_day.currentIndex() report_generate = self.check_generate_report.isChecked() report_path = self.report_path.text() + refresh_state = self.returnMode.isChecked() + shortcuts = self.getShortcuts() + #shortcuts to omegaconf.DictConfig + shortcuts = OmegaConf.create(shortcuts) + + if database_path != self.originalSettings.database.path : os.makedirs(database_path, exist_ok=True) self.restart() @@ -136,9 +193,11 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.originalSettings.database.path = database_path self.originalSettings.database.name = database_name self.originalSettings.delete_inactive_user_duration = delete_inactive_users - self.originalSettings.report.report_day = report_day + self.originalSettings.report.report_day = report_day -1 self.originalSettings.report.path = report_path self.originalSettings.report.generate_report = report_generate + self.originalSettings.advanced_refresh = refresh_state + self.originalSettings.shortcuts = shortcuts # save the new settings config.save() self.settingschanged = True @@ -176,6 +235,7 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.report_day.setCurrentIndex(self.originalSettings.report.report_day -1) self.check_generate_report.setChecked(self.originalSettings.report.generate_report) self.report_path.setText(self.originalSettings.report.path) + self.returnMode.setChecked(self.originalSettings.advanced_refresh) pass diff --git a/src/ui/sources/Ui_dialog_settings.py b/src/ui/sources/Ui_dialog_settings.py index e57fe1a..ff5f87e 100644 --- a/src/ui/sources/Ui_dialog_settings.py +++ b/src/ui/sources/Ui_dialog_settings.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(492, 306) + Dialog.resize(492, 445) self.formLayout = QtWidgets.QFormLayout(Dialog) self.formLayout.setObjectName("formLayout") self.label = QtWidgets.QLabel(parent=Dialog) @@ -21,6 +21,39 @@ class Ui_Dialog(object): self.institution_name = QtWidgets.QLineEdit(parent=Dialog) self.institution_name.setObjectName("institution_name") self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.institution_name) + self.label_2 = QtWidgets.QLabel(parent=Dialog) + self.label_2.setObjectName("label_2") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.default_loan_duration = QtWidgets.QSpinBox(parent=Dialog) + self.default_loan_duration.setProperty("value", 7) + self.default_loan_duration.setObjectName("default_loan_duration") + self.horizontalLayout_2.addWidget(self.default_loan_duration) + self.label_13 = QtWidgets.QLabel(parent=Dialog) + self.label_13.setMaximumSize(QtCore.QSize(43, 16777215)) + self.label_13.setObjectName("label_13") + self.horizontalLayout_2.addWidget(self.label_13) + self.formLayout.setLayout(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout_2) + self.label_7 = QtWidgets.QLabel(parent=Dialog) + self.label_7.setObjectName("label_7") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_7) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.delete_inactive_user_duration = QtWidgets.QSpinBox(parent=Dialog) + self.delete_inactive_user_duration.setMaximum(9999) + self.delete_inactive_user_duration.setProperty("value", 365) + self.delete_inactive_user_duration.setObjectName("delete_inactive_user_duration") + self.horizontalLayout.addWidget(self.delete_inactive_user_duration) + self.label_12 = QtWidgets.QLabel(parent=Dialog) + self.label_12.setMaximumSize(QtCore.QSize(43, 16777215)) + self.label_12.setObjectName("label_12") + self.horizontalLayout.addWidget(self.label_12) + self.formLayout.setLayout(3, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout) + self.returnMode = QtWidgets.QCheckBox(parent=Dialog) + self.returnMode.setTristate(False) + self.returnMode.setObjectName("returnMode") + self.formLayout.setWidget(4, QtWidgets.QFormLayout.ItemRole.FieldRole, self.returnMode) self.label_3 = QtWidgets.QLabel(parent=Dialog) self.label_3.setObjectName("label_3") self.formLayout.setWidget(5, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_3) @@ -54,11 +87,6 @@ class Ui_Dialog(object): self.btn_select_database_backupLocation.setObjectName("btn_select_database_backupLocation") self.databasesettings.addWidget(self.btn_select_database_backupLocation, 2, 2, 1, 1) self.formLayout.setLayout(5, QtWidgets.QFormLayout.ItemRole.FieldRole, self.databasesettings) - self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) - self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Discard|QtWidgets.QDialogButtonBox.StandardButton.Ok) - self.buttonBox.setObjectName("buttonBox") - self.formLayout.setWidget(7, QtWidgets.QFormLayout.ItemRole.FieldRole, self.buttonBox) self.label_9 = QtWidgets.QLabel(parent=Dialog) self.label_9.setObjectName("label_9") self.formLayout.setWidget(6, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_9) @@ -92,35 +120,31 @@ class Ui_Dialog(object): self.report_day.addItem("") self.gridLayout.addWidget(self.report_day, 0, 1, 1, 1) self.formLayout.setLayout(6, QtWidgets.QFormLayout.ItemRole.FieldRole, self.gridLayout) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.delete_inactive_user_duration = QtWidgets.QSpinBox(parent=Dialog) - self.delete_inactive_user_duration.setMaximum(9999) - self.delete_inactive_user_duration.setProperty("value", 365) - self.delete_inactive_user_duration.setObjectName("delete_inactive_user_duration") - self.horizontalLayout.addWidget(self.delete_inactive_user_duration) - self.label_12 = QtWidgets.QLabel(parent=Dialog) - self.label_12.setMaximumSize(QtCore.QSize(43, 16777215)) - self.label_12.setObjectName("label_12") - self.horizontalLayout.addWidget(self.label_12) - self.formLayout.setLayout(3, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout) - self.label_7 = QtWidgets.QLabel(parent=Dialog) - self.label_7.setObjectName("label_7") - self.formLayout.setWidget(3, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_7) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.default_loan_duration = QtWidgets.QSpinBox(parent=Dialog) - self.default_loan_duration.setProperty("value", 7) - self.default_loan_duration.setObjectName("default_loan_duration") - self.horizontalLayout_2.addWidget(self.default_loan_duration) - self.label_13 = QtWidgets.QLabel(parent=Dialog) - self.label_13.setMaximumSize(QtCore.QSize(43, 16777215)) - self.label_13.setObjectName("label_13") - self.horizontalLayout_2.addWidget(self.label_13) - self.formLayout.setLayout(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.horizontalLayout_2) - self.label_2 = QtWidgets.QLabel(parent=Dialog) - self.label_2.setObjectName("label_2") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.label_2) + self.buttonBox = QtWidgets.QDialogButtonBox(parent=Dialog) + self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Discard|QtWidgets.QDialogButtonBox.StandardButton.Ok) + self.buttonBox.setObjectName("buttonBox") + self.formLayout.setWidget(9, QtWidgets.QFormLayout.ItemRole.FieldRole, self.buttonBox) + self.shortcutchanger = QtWidgets.QTableWidget(parent=Dialog) + self.shortcutchanger.setObjectName("shortcutchanger") + self.shortcutchanger.setColumnCount(3) + self.shortcutchanger.setRowCount(0) + item = QtWidgets.QTableWidgetItem() + self.shortcutchanger.setHorizontalHeaderItem(0, item) + item = QtWidgets.QTableWidgetItem() + self.shortcutchanger.setHorizontalHeaderItem(1, item) + item = QtWidgets.QTableWidgetItem() + self.shortcutchanger.setHorizontalHeaderItem(2, item) + self.formLayout.setWidget(8, QtWidgets.QFormLayout.ItemRole.FieldRole, self.shortcutchanger) + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.label_14 = QtWidgets.QLabel(parent=Dialog) + self.label_14.setObjectName("label_14") + self.verticalLayout.addWidget(self.label_14) + self.label_15 = QtWidgets.QLabel(parent=Dialog) + self.label_15.setObjectName("label_15") + self.verticalLayout.addWidget(self.label_15) + self.formLayout.setLayout(8, QtWidgets.QFormLayout.ItemRole.LabelRole, self.verticalLayout) self.retranslateUi(Dialog) self.buttonBox.accepted.connect(Dialog.accept) # type: ignore @@ -137,6 +161,13 @@ class Ui_Dialog(object): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "Dialog")) self.label.setText(_translate("Dialog", "Name der Einrichtung")) + self.label_2.setText(_translate("Dialog", "Leihdauer")) + self.label_13.setText(_translate("Dialog", "Tage(n)")) + self.label_7.setText(_translate("Dialog", "Inaktive Nutzer\n" +"Löschen nach")) + self.label_12.setText(_translate("Dialog", "Tage(n)")) + self.returnMode.setToolTip(_translate("Dialog", "Wenn aktiv: Wenn ein Medium zurückgegeben wird, wird die nächste Aktion des Moduswechsels zum normalen Rückgabemodus führen")) + self.returnMode.setText(_translate("Dialog", "Erweiterter Rückgabemodus")) self.label_3.setText(_translate("Dialog", "Datenbank")) self.label_4.setText(_translate("Dialog", "Speicherort")) self.label_6.setText(_translate("Dialog", "Sicherungspfad")) @@ -154,8 +185,12 @@ class Ui_Dialog(object): self.report_day.setItemText(2, _translate("Dialog", "Mittwoch")) self.report_day.setItemText(3, _translate("Dialog", "Donnerstag")) self.report_day.setItemText(4, _translate("Dialog", "Freitag")) - self.label_12.setText(_translate("Dialog", "Tage(n)")) - self.label_7.setText(_translate("Dialog", "Inaktive Nutzer\n" -"Löschen nach")) - self.label_13.setText(_translate("Dialog", "Tage(n)")) - self.label_2.setText(_translate("Dialog", "Leihdauer")) + item = self.shortcutchanger.horizontalHeaderItem(0) + item.setText(_translate("Dialog", "Name")) + item = self.shortcutchanger.horizontalHeaderItem(1) + item.setText(_translate("Dialog", "Standard")) + item = self.shortcutchanger.horizontalHeaderItem(2) + item.setText(_translate("Dialog", "Aktuell")) + self.label_14.setText(_translate("Dialog", "Shortcuts")) + self.label_15.setText(_translate("Dialog", "(Erst nach Neustart\n" +"wirksam)")) diff --git a/src/ui/sources/Ui_dialog_settings.ui.py b/src/ui/sources/Ui_dialog_settings.ui.py index 4425410..42e0582 100644 --- a/src/ui/sources/Ui_dialog_settings.ui.py +++ b/src/ui/sources/Ui_dialog_settings.ui.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_settings.ui.iRVFlN' +# Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\LibrarySystem\src\ui\sources\dialog_settings.ui.vqAAbY' # # Created by: PyQt6 UI code generator 6.6.1 # diff --git a/src/ui/sources/Ui_main_UserInterface.py b/src/ui/sources/Ui_main_UserInterface.py index aeb26e0..c5577ac 100644 --- a/src/ui/sources/Ui_main_UserInterface.py +++ b/src/ui/sources/Ui_main_UserInterface.py @@ -51,20 +51,7 @@ class Ui_MainWindow(object): self.label_5.setFont(font) self.label_5.setObjectName("label_5") self.horizontalLayout.addWidget(self.label_5) - self.mode = QtWidgets.QLabel(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.mode.sizePolicy().hasHeightForWidth()) - self.mode.setSizePolicy(sizePolicy) - self.mode.setMinimumSize(QtCore.QSize(62, 0)) - self.mode.setMaximumSize(QtCore.QSize(62, 16777215)) - self.mode.setBaseSize(QtCore.QSize(62, 0)) - self.mode.setAutoFillBackground(False) - self.mode.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.mode.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.mode.setLineWidth(2) - self.mode.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.mode = QtWidgets.QPushButton(parent=self.centralwidget) self.mode.setObjectName("mode") self.horizontalLayout.addWidget(self.mode) self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) @@ -167,19 +154,15 @@ class Ui_MainWindow(object): self.actionRueckgabemodus.setObjectName("actionRueckgabemodus") self.actionNutzer = QtGui.QAction(parent=MainWindow) self.actionNutzer.setObjectName("actionNutzer") - self.actionNutzer_2 = QtGui.QAction(parent=MainWindow) - self.actionNutzer_2.setObjectName("actionNutzer_2") - self.actionAusleihistorie = QtGui.QAction(parent=MainWindow) - self.actionAusleihistorie.setObjectName("actionAusleihistorie") + self.actionAusleihhistorie = QtGui.QAction(parent=MainWindow) + self.actionAusleihhistorie.setObjectName("actionAusleihhistorie") self.actionBericht_erstellen = QtGui.QAction(parent=MainWindow) self.actionBericht_erstellen.setObjectName("actionBericht_erstellen") - self.actionNutzer_3 = QtGui.QAction(parent=MainWindow) - self.actionNutzer_3.setObjectName("actionNutzer_3") self.menuDatei.addAction(self.actionEinstellungen) self.menuDatei.addAction(self.actionBeenden) self.menuHotkeys.addAction(self.actionRueckgabemodus) self.menuFenster.addAction(self.actionNutzer) - self.menuFenster.addAction(self.actionAusleihistorie) + self.menuFenster.addAction(self.actionAusleihhistorie) self.menuFenster.addAction(self.actionBericht_erstellen) self.menubar.addAction(self.menuDatei.menuAction()) self.menubar.addAction(self.menuHotkeys.menuAction()) @@ -221,9 +204,7 @@ class Ui_MainWindow(object): self.actionRueckgabemodus.setShortcut(_translate("MainWindow", "F5")) self.actionNutzer.setText(_translate("MainWindow", "Nutzer")) self.actionNutzer.setShortcut(_translate("MainWindow", "F6")) - self.actionNutzer_2.setText(_translate("MainWindow", "Nutzer")) - self.actionAusleihistorie.setText(_translate("MainWindow", "Ausleihhistorie")) - self.actionAusleihistorie.setShortcut(_translate("MainWindow", "F8")) + self.actionAusleihhistorie.setText(_translate("MainWindow", "Ausleihhistorie")) + self.actionAusleihhistorie.setShortcut(_translate("MainWindow", "F8")) self.actionBericht_erstellen.setText(_translate("MainWindow", "Bericht erstellen")) self.actionBericht_erstellen.setShortcut(_translate("MainWindow", "F7")) - self.actionNutzer_3.setText(_translate("MainWindow", "Nutzer")) diff --git a/src/ui/sources/dialog_settings.ui b/src/ui/sources/dialog_settings.ui index 70430d4..d94cce4 100644 --- a/src/ui/sources/dialog_settings.ui +++ b/src/ui/sources/dialog_settings.ui @@ -7,7 +7,7 @@ 0 0 492 - 306 + 445 @@ -24,6 +24,85 @@ + + + + Leihdauer + + + + + + + + + 7 + + + + + + + + 43 + 16777215 + + + + Tage(n) + + + + + + + + + Inaktive Nutzer +Löschen nach + + + + + + + + + 9999 + + + 365 + + + + + + + + 43 + 16777215 + + + + Tage(n) + + + + + + + + + Wenn aktiv: Wenn ein Medium zurückgegeben wird, wird die nächste Aktion des Moduswechsels zum normalen Rückgabemodus führen + + + Erweiterter Rückgabemodus + + + false + + + @@ -86,16 +165,6 @@ - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Discard|QDialogButtonBox::Ok - - - @@ -174,72 +243,54 @@ - - + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Discard|QDialogButtonBox::Ok + + + + + + + + Name + + + + + Standard + + + + + Aktuell + + + + + + - - - 9999 - - - 365 + + + Shortcuts - - - - 43 - 16777215 - - + - Tage(n) + (Erst nach Neustart +wirksam) - - - - Inaktive Nutzer -Löschen nach - - - - - - - - - 7 - - - - - - - - 43 - 16777215 - - - - Tage(n) - - - - - - - - - Leihdauer - - - diff --git a/src/ui/sources/main_UserInterface.ui b/src/ui/sources/main_UserInterface.ui index 7baf478..e7e7621 100644 --- a/src/ui/sources/main_UserInterface.ui +++ b/src/ui/sources/main_UserInterface.ui @@ -95,49 +95,10 @@ - - - - 0 - 0 - - - - - 62 - 0 - - - - - 62 - 16777215 - - - - - 62 - 0 - - - - false - - - QFrame::StyledPanel - - - QFrame::Sunken - - - 2 - + Rückgabe - - Qt::AlignCenter - @@ -314,7 +275,7 @@ Fenster - + @@ -353,7 +314,7 @@ Nutzer - + Ausleihhistorie From 6eecbab684b840e49709c0447821bc1c49c0254a Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:21:59 +0200 Subject: [PATCH 64/83] debug fix --- src/utils/debug.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/utils/debug.py b/src/utils/debug.py index 306de51..8616e92 100644 --- a/src/utils/debug.py +++ b/src/utils/debug.py @@ -1,11 +1,8 @@ +from src import config from icecream import ic from src.utils import Log -from src import __version__, config - - log = Log("debugMessage") - def debugMessage(*args, **kwargs): startmessage = "Logging debug message" # join args and kwargs to a string @@ -15,10 +12,7 @@ def debugMessage(*args, **kwargs): if config.debug: if config.log_debug: log.info(f"{startmessage}: {message}") - - ic(message) - return message - - -if __name__ == "__main__": - debugMessage("This is a debug message ", test="test", url="https://www.google.com") + if config.ic_logging == True: + ic(message) + else: print(message) + return message \ No newline at end of file From 83636f65c23a084d438c13d87894e28c70db816c Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:54:36 +0200 Subject: [PATCH 65/83] rework args to fix argparse bug --- src/__init__.py | 104 +++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 55 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 0459e99..89fbd17 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -2,62 +2,56 @@ import sys -from config import Config +from config import Config# +import argparse + config = Config("config/settings.yaml") - +__version__ = "0.1.0" # if programm launched with argument --debug, set debug to True -if "--debug" in sys.argv: - config.debug = True -# if programm launched with argument --log, set log_debug -if "--log" in sys.argv: - config.log_debug = True - - -# arguments = argparse.ArgumentParser( -# prog="Ausleihsystem", -# description="Ein Ausleihsystem für Handbibliotheken", -# epilog="Version: {}".format(__version__), -# ) -# arguments.add_argument( -# "-d", -# "--debug", -# action="store_true", -# help="Display debug messages in terminal", -# default=False, -# required=False, -# ) -# arguments.add_argument( -# "-v", -# "--version", -# action="store_true", -# help="Display version number", -# default=False, -# required=False, -# ) -# arguments.add_argument( -# "-l", -# "--log", -# action="store_true", -# help="Log debug messages to logfile", -# default=False, -# required=False, -# ) -# arguments.add_argument( -# "--no-backup", -# action="store_true", -# help="Disable backup", -# default=False, -# required=False, -# ) - -# args = arguments.parse_args() -# # based on the arguments, set the config values -# if args.debug: +# if "--debug" in sys.argv: # config.debug = True -# if args.version: -# print(f"Version: {__version__}") -# sys.exit() -# if args.log: +# # if programm launched with argument --log, set log_debug +# if "--log" in sys.argv: # config.log_debug = True -# if args.no_backup: -# config.database.do_backup = False +valid_args = ["--debug", "--log", "--no-backup", "--ic-logging", "--version", "-h"] + +args_description = { + "--debug": "Enable debug mode", + "--log": "Enable logging", + "--no-backup": "Disable database backup", + "--ic-logging": "Enable icecream logging (not available in production)", + "--version": "Show version", + "-h": "Show help message and exit" +} + +args = sys.argv[1:] +if any(arg not in valid_args for arg in args): + print("Invalid argument present") + sys.exit() +def help(): + print("Ausleihsystem") + print("Ein Ausleihsystem für Handbibliotheken") + print("Version: {}".format(__version__)) + print("Valide Argumente:") + print("args") + print("--------") + print("usage: main.py [-h] [--debug] [--log] [--no-backup] [--ic-logging] [--version]") + print("options:") + for arg in valid_args: + print(f"{arg} : {args_description[arg]}") + +# based on the arguments, set the config values +if"-h" in args: + help() + sys.exit() +if "--debug" in args: + config.debug = True +if "--log" in args: + config.log_debug = True +if "--no-backup" in args: + config.no_backup = True +if "--ic-logging" in args: + config.ic_logging = True +if "--version" in args: + print(__version__) + sys.exit() From c7309e047be158844a5ba846a14f90d411654b62 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:08:45 +0200 Subject: [PATCH 66/83] feat: rework settings, conf to detect settings changes better --- config/config.py | 10 +++- src/ui/settings.py | 141 +++++++++++++++++++++++++-------------------- 2 files changed, 86 insertions(+), 65 deletions(-) diff --git a/config/config.py b/config/config.py index a3c62bb..8741d83 100644 --- a/config/config.py +++ b/config/config.py @@ -140,7 +140,15 @@ class Config: self._config[option] = True else: raise KeyError(f"Option {option} not found in configuration") - + + + def to_Omegaconf(self): + return omegaconf.OmegaConf.create(self._config) + + def updateValue(self, key:str, value): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config[key] = value if __name__ == "__main__": cfg = Config("config/settings.yaml") #print(cfg.database.path) diff --git a/src/ui/settings.py b/src/ui/settings.py index c3b77a6..8f74940 100644 --- a/src/ui/settings.py +++ b/src/ui/settings.py @@ -5,12 +5,6 @@ from src import config from omegaconf import OmegaConf import os - - - - - - class Settings(QtWidgets.QDialog, Ui_Dialog): def __init__(self): super(Settings, self).__init__() @@ -18,26 +12,20 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.setWindowTitle("Einstellungen") self.setWindowIcon(Icon("settings").icon) #variables - self.originalSettings = config + self.originalSettings = config.to_Omegaconf() + self.changedSettings = config.to_Omegaconf() self.shortcuts = config.shortcuts self.settingschanged = False + self.restart_required = False - self.populateShortcuts() # buttonbox self.buttonBox.accepted.connect(self.saveSettings) self.buttonBox.rejected.connect(self.close) self.loadSettings() - self.buttonBox.button( - QtWidgets.QDialogButtonBox.StandardButton.Discard - ).clicked.connect(self.DiscardSettings) - self.buttonBox.button( - QtWidgets.QDialogButtonBox.StandardButton.Discard - ).setEnabled(False) - self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( - False - ) + + self.populateShortcuts() # buttons self.btn_select_database_backupLocation.clicked.connect( self.selectBackupLocation @@ -63,9 +51,7 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): current = shortcut["current"] self.addShortcut(name, default, current) #assume the shortcuts will be changed - self.settingschanged = True - self.enableButtonBox() - + self.settingschanged = True def addShortcut(self, name, default, current): #remove all pages from shortcutchanger #add new page with name, default and current @@ -78,17 +64,6 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): keysequenceedit.setKeySequence(current) self.shortcutchanger.setCellWidget(0, 2, keysequenceedit) - - - - def enableButtonBox(self): - self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( - True - ) - self.buttonBox.button( - QtWidgets.QDialogButtonBox.StandardButton.Discard - ).setEnabled(True) - def selectBackupLocation(self): backupLocation = QtWidgets.QFileDialog.getExistingDirectory( self, @@ -102,7 +77,7 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True ) - self.settingschanged = False + self.settingschanged = True def selectReportPath(self): reportPath = QtWidgets.QFileDialog.getExistingDirectory( @@ -115,7 +90,7 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True ) - self.settingschanged = False + self.settingschanged = True @@ -130,7 +105,8 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True ) - self.settingschanged = False + self.settingschanged = True + self.restart_required = True def selectDatabaseName(self): @@ -141,7 +117,6 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.originalSettings.database.path, "Database Files (*.db)", ) - self.database_name.setText(databaseName[0]) self.database_path.setText(databaseName[0].rsplit("/", 1)[0]) self.buttonBox.button( QtWidgets.QDialogButtonBox.StandardButton.Discard @@ -149,7 +124,8 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled( True ) - self.settingschanged = False + self.settingschanged = True + self.restart_required = True def getShortcuts(self): shortcuts = [] @@ -185,22 +161,58 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): if database_path != self.originalSettings.database.path : os.makedirs(database_path, exist_ok=True) - self.restart() - # overwrite the original settings - self.originalSettings.institution_name = institution_name - self.originalSettings.loan_duration = default_loan_duration - self.originalSettings.database.backupLocation = database_backupLocation - self.originalSettings.database.path = database_path - self.originalSettings.database.name = database_name - self.originalSettings.delete_inactive_user_duration = delete_inactive_users - self.originalSettings.report.report_day = report_day -1 - self.originalSettings.report.path = report_path - self.originalSettings.report.generate_report = report_generate - self.originalSettings.advanced_refresh = refresh_state - self.originalSettings.shortcuts = shortcuts + self.restart_required = True + if shortcuts != self.originalSettings.shortcuts: + self.restart_required = True + + + + # create new Settings + self.changedSettings.institution_name = institution_name + self.changedSettings.loan_duration = default_loan_duration + self.changedSettings.database.backupLocation = database_backupLocation + self.changedSettings.database.path = database_path + self.changedSettings.database.name = database_name + self.changedSettings.delete_inactive_user_duration = delete_inactive_users + self.changedSettings.report.report_day = report_day + self.changedSettings.report.path = report_path + self.changedSettings.report.generate_report = report_generate + self.changedSettings.advanced_refresh = refresh_state + self.changedSettings.shortcuts = shortcuts + + changed = self.changedSettings + original = self.originalSettings + if changed == original: + print("No changes") + self.settingschanged = False + self.restart_required = False + else: + print("Changes detected") + self.settingschanged = True + if original.database.path != changed.database.path or original.shortcuts != changed.shortcuts: + self.restart_required = True + + + # save the new settings - config.save() - self.settingschanged = True + if self.settingschanged: + # save the settings + config.updateValue("institution_name", self.changedSettings.institution_name) + config.updateValue("default_loan_duration", self.changedSettings.loan_duration) + config.updateValue("database.backupLocation", self.changedSettings.database.backupLocation) + config.updateValue("database.path", self.changedSettings.database.path) + config.updateValue("database.name", self.changedSettings.database.name) + config.updateValue("delete_inactive_user_duration", self.changedSettings.delete_inactive_user_duration) + config.updateValue("report.report_day", self.changedSettings.report.report_day) + config.updateValue("report.generate_report", self.changedSettings.report.generate_report) + config.updateValue("report.path", self.changedSettings.report.path) + config.updateValue("advanced_refresh", self.changedSettings.advanced_refresh) + config.updateValue("shortcuts", self.changedSettings.shortcuts) + self.originalSettings = self.changedSettings + config.save() + + if self.restart_required: + self.restart() self.close() def restart(self): @@ -208,34 +220,35 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): dialog.setIcon(QtWidgets.QMessageBox.Icon.Information) dialog.setText("Neustart erforderlich") dialog.setInformativeText( - "Das Programm muss neu gestartet werden, um die neue Datenbank zu verwenden." + "Das Programm muss neu gestartet werden, um die Änderungen zu übernehmen." ) dialog.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok) dialog.setDefaultButton(QtWidgets.QMessageBox.StandardButton.Ok) dialog.setWindowTitle("Neustart erforderlich") + dialog.setWindowIcon(Icon("restart").icon) dialog.exec() def DiscardSettings(self): self.loadSettings() - pass - + self.restart_required = False + self.settingschanged = False def loadSettings(self): - self.institution_name.setText(self.originalSettings.institution_name) + self.institution_name.setText(config.institution_name) self.default_loan_duration.setValue( - int(self.originalSettings.loan_duration) + int(config.loan_duration) ) self.delete_inactive_user_duration.setValue( - int(self.originalSettings.delete_inactive_user_duration) + int(config.delete_inactive_user_duration) ) self.database_backupLocation.setText( - self.originalSettings.database.backupLocation + config.database.backupLocation ) - self.database_path.setText(self.originalSettings.database.path) - self.database_name.setText(self.originalSettings.database.name) - self.report_day.setCurrentIndex(self.originalSettings.report.report_day -1) - self.check_generate_report.setChecked(self.originalSettings.report.generate_report) - self.report_path.setText(self.originalSettings.report.path) - self.returnMode.setChecked(self.originalSettings.advanced_refresh) + self.database_path.setText(config.database.path) + self.database_name.setText(config.database.name) + self.report_day.setCurrentIndex(config.report.report_day) + self.check_generate_report.setChecked(config.report.generate_report) + self.report_path.setText(config.report.path) + self.returnMode.setChecked(config.advanced_refresh) pass From e7bcce328b633e635c18b9daef709a2c0d478f39 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:19:50 +0200 Subject: [PATCH 67/83] feat: add documentation, changeable shortcuts --- docs/Ausleihhistorie.md | 15 +++++++ docs/Ausleihsystem.md | 47 +++++++++++++++++++++ docs/Bericht erstellen.md | 27 ++++++++++++ docs/Einstellungen.md | 21 +++++++++ docs/Nutzer anlegen.md | 18 ++++++++ docs/Nutzeroberfläche.md | 31 ++++++++++++++ docs/ausleihe verlängern.md | 9 ++++ docs/images/add_user.png | Bin 0 -> 8103 bytes docs/images/createUser.png | Bin 0 -> 757 bytes docs/images/err_nouser.png | Bin 0 -> 5442 bytes docs/images/extend.png | Bin 0 -> 16891 bytes docs/images/generateReport.png | Bin 0 -> 9327 bytes docs/images/generatedReport.png | Bin 0 -> 9903 bytes docs/images/loanhistory.png | Bin 0 -> 12259 bytes docs/images/main_ktoarea.png | Bin 0 -> 7585 bytes docs/images/main_marked areas.png | Bin 0 -> 20359 bytes docs/images/main_no_user.png | Bin 0 -> 19885 bytes docs/images/main_user_active.png | Bin 0 -> 19335 bytes docs/images/main_userdata.png | Bin 0 -> 3913 bytes docs/images/restart.png | Bin 0 -> 7352 bytes docs/images/settings.png | Bin 0 -> 30876 bytes docs/images/settings_changed_restart.png | Bin 0 -> 6304 bytes docs/images/user_main.png | Bin 0 -> 19351 bytes docs/index.md | 20 +++++++++ docs/shortcuts.md | 16 +++++++ docs/stylesheets/extra.css | 5 +++ icons/icons.yaml | 1 + icons/restart.svg | 1 + mkdocs.yml | 30 +++++++++++++ src/logic/documentation_thread.py | 12 ++++++ src/ui/sources/Ui_dialog_generateReport.py | 2 +- src/ui/sources/Ui_main_UserInterface.py | 14 ++++++ src/ui/sources/dialog_generateReport.ui | 2 +- src/ui/sources/main_UserInterface.ui | 33 ++++++++++++--- src/utils/__init__.py | 3 +- src/utils/documentation.py | 21 +++++++++ 36 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 docs/Ausleihhistorie.md create mode 100644 docs/Ausleihsystem.md create mode 100644 docs/Bericht erstellen.md create mode 100644 docs/Einstellungen.md create mode 100644 docs/Nutzer anlegen.md create mode 100644 docs/Nutzeroberfläche.md create mode 100644 docs/ausleihe verlängern.md create mode 100644 docs/images/add_user.png create mode 100644 docs/images/createUser.png create mode 100644 docs/images/err_nouser.png create mode 100644 docs/images/extend.png create mode 100644 docs/images/generateReport.png create mode 100644 docs/images/generatedReport.png create mode 100644 docs/images/loanhistory.png create mode 100644 docs/images/main_ktoarea.png create mode 100644 docs/images/main_marked areas.png create mode 100644 docs/images/main_no_user.png create mode 100644 docs/images/main_user_active.png create mode 100644 docs/images/main_userdata.png create mode 100644 docs/images/restart.png create mode 100644 docs/images/settings.png create mode 100644 docs/images/settings_changed_restart.png create mode 100644 docs/images/user_main.png create mode 100644 docs/index.md create mode 100644 docs/shortcuts.md create mode 100644 docs/stylesheets/extra.css create mode 100644 icons/restart.svg create mode 100644 mkdocs.yml create mode 100644 src/logic/documentation_thread.py create mode 100644 src/utils/documentation.py diff --git a/docs/Ausleihhistorie.md b/docs/Ausleihhistorie.md new file mode 100644 index 0000000..8a9fd77 --- /dev/null +++ b/docs/Ausleihhistorie.md @@ -0,0 +1,15 @@ +# Ausleihhistorie +![Oberfläche](images/loanhistory.png) + +!!! info + + Die Ausleihhistorie kann vom Ausleihsystem immer aufgerufen werden. Dazu kann man entweder das Menu öffnen (Fenster -> Ausleihhistorie) oder den festgelegten Shortcut verwenden. + +# Bedienung +Hier werden alle Medien angezeigt. +Über die Filter kann man gezielt + - alle Ausleihen + - aktuell entliehene Medien + - überzogene Medien +anzeigen. +Zudem kann man mithilfe der Eingabezeile unter den Filteroptionen gezielt nach einem Titel, Nutzer oder einer Signatur gesucht werden. \ No newline at end of file diff --git a/docs/Ausleihsystem.md b/docs/Ausleihsystem.md new file mode 100644 index 0000000..08246b8 --- /dev/null +++ b/docs/Ausleihsystem.md @@ -0,0 +1,47 @@ +# Ausleihsystem + + + +## Oberfläche + +![Nutzeroberfläche ohne Bewerkungen](images/main_marked%20areas.png) + +Die Oberfläche kann generell in drei Bereiche unterteilt werden: + - Konto und Ausleihe + - Nutzerdaten + - Historie + +### Konto und Ausleihe + +Hier werden die Kontodaten und die Ausleihen angezeigt. +Der Bereich beschränkt sich auf folgende Inhalte: +![Kontobereich](images/main_ktoarea.png) + +Hier werden folgende Daten angezeigt + +- Modus: entweder "Ausleihe" oder "Rückgabe" (1) +- Matrikelnummer: die Matrikelnummer des Nutzers, um das Konto zu öffnen (3) +- Benutzername: der Benutzername des Nutzers (3) +- Signatur: die Signatur des Mediums, welches entliehen oder zurückgegeben wird (4) +- Ausleihe bis: bis wann das Medium ausgeliehen wird, Zeitraum wird in den Einstellungen angepasst +- Nutzer anlegen: ein Knopf, um einen neuen Nutzer anzulegen (2) + +### Nutzerdaten +Hier werden die Nutzerdaten angezeigt. Vorraussetzung ist, dass ein Nutzer angelegt und geöffnet wurde, oder dass ein Medium zurückgegeben wurde. +Dieser Bereich beschränkt sich auf folgende Inhalte: + +![Nutzerdaten](images/main_userdata.png) + +- Das Feld Nutzerdaten (6)beinhaltet + - Namen + - Matrikelnummer + - E-Mail +- Das Feld Ausleihdaten beinhaltet: + - Anzahl Ausleihen (5) : ein klickbares Feld, welches die Anzahl der Ausleihen anzeigt. Beim Klick wird die Übersicht des aktiven Nutzers angezeigt. + - Nächstes Rückgabedatum (wird angezeigt wenn ein Nutzer (mehr als) eine Ausleihe hat und ein Medium zurückgegeben wurde oder ein Nutzer geöffnet wird) + +### Historie (7) +Das Feld der Historie listet alle Medien auf, die im aktiven Prozess ausgeliehen oder zurückgegeben wurden. + + + diff --git a/docs/Bericht erstellen.md b/docs/Bericht erstellen.md new file mode 100644 index 0000000..76522fe --- /dev/null +++ b/docs/Bericht erstellen.md @@ -0,0 +1,27 @@ +# Bericht erstellen + +![Bericht erstellen](images/generateReport.png) + +## Information +Diese Oberfläche kann immer von der [Hauptoberfläche](Ausleihsystem.md) geöffnet werden. Hierzu entweder +`Fenster -> Bericht erstellen` oder den Shortcut F7 verwenden + + +## Bericht generieren + +Um einen Bericht zu erstellen, müssen folgende Kriterien erfüllt sein: + +- Zeitspanne festgelegt (Entweder über den Slider, oder über die Woche / Monat / Jahr Knöpfe) +- Datenformat ausgewählt + +Nachdem diese Kriterien erfüllt sind, kann der Bericht über den Knopf `Bericht erstellen` erstellt werden. Der Bericht wird erstellt, bei größeren Datensätzen kann es länger dauern, eine Fortschrittsanzeige gibt an, wie weit der Prozess ist. + +Wurde der Bericht erfolgreich erstellt, sieht die Oberfläche wie folgt aus: + +![Bericht erstellt](images/generatedReport.png) + +Über einen Klick auf `Report` wird die entsprechende Datei geöffnet. + +!!! info + + Text öffnet das Notepad, Excel öffnet Excel \ No newline at end of file diff --git a/docs/Einstellungen.md b/docs/Einstellungen.md new file mode 100644 index 0000000..19d3963 --- /dev/null +++ b/docs/Einstellungen.md @@ -0,0 +1,21 @@ +# Einstellungen +![Einstellungen](images/settings.png) + +## Bedienung +Hier werden die Einstellungen geändert. Sobald ein Wert geändert wird, ist es möglich die Einstellungen rückgängig zu machen, oder die Änderungen zu speichern. + +## Besonderheiten +Der Knopf neben Speicherort, Datenbankname, Sicherungspfad und Speicherpfad kann verwendet werden, um den Pfad gezielt zu setzen. + +Beim Datenbanknamen wird allerdings nur der Name der Datenbank übernommen + +## Hinweis + +Einige Aktionen (bspw. Änderungen der Shortcuts) erfordern einen Neustart der Anwendung. Dies wird beim Speichern der Einstellungen mit folgendem Dialog dargestelt: + +![restart](images/restart.png) + +Im Anschluss an diesen Dialog erscheint ein neuer Dialog: + +![restart Application](images/settings_changed_restart.png) + diff --git a/docs/Nutzer anlegen.md b/docs/Nutzer anlegen.md new file mode 100644 index 0000000..56eb6cd --- /dev/null +++ b/docs/Nutzer anlegen.md @@ -0,0 +1,18 @@ +# Nutzer anlegen + +![Nutzer erstellen](images/add_user.png) + +## Information +Diese Oberfläche kann nur geöffnet werden, wenn die [Hauptoberfläche](Ausleihsystem.md) geöffnet ist. Hierzu muss bei dieser auf folgenden Knopf gedrückt werden: +![create user](images/createUser.png) + +## Bedienung +Um einen Nutzer anzulegen, müssen alle Angaben korrekt ausgefüllt sein. +### Limitierungen +Folgende Regeln sind zwingend einzuhalten: +- Nachname, Vorname muss mit `, ` getrennt sein +- Matrikelnummer darf nicht länger als 20 Zeichen sein und nur Nummern enthalten +- Mail muss einem validen Schema entsprechen (s. Bild) + +Nachdem alle Kriterien erfüllt sind, kann der Knopf `Save` angeklickt werden. Der Nutzer wird gespeichert und in der [Hauptoberfläche](Ausleihsystem.md) geöffnet. + diff --git a/docs/Nutzeroberfläche.md b/docs/Nutzeroberfläche.md new file mode 100644 index 0000000..a3f1564 --- /dev/null +++ b/docs/Nutzeroberfläche.md @@ -0,0 +1,31 @@ +# Nutzerdatenfenster +![alt text](images/user_main.png) + +!!! info + Die Nutzeroberfläche kann nur geöffnet werden, wenn ein Nutzer offen ist. Ansonsten wird ein Fehler angezeigt. (s. unten) + +![Error](images/err_nouser.png) + +# Bedeutung der Felder +- (1) Nutzerdaten: + + Name des Nutzers, Matrikelnummer, E-Mail + Wird eine Angabe geändert, erscheint Feld (3) um diese Angaben entweder zu speichern oder um die Änderungen rückgängig zu machen. + +- (2) Nutzer Löschen: + + Mit dem Klick auf den Mülleimer wird der Nutzer gelöscht. Alle zugewiesenen Ausleihen werden in der [Ausleihhistorie](Ausleihhistorie.md) mit den Nutzerkonto `gelöscht` angezeigt. + +- (4) Medien: + + Umfasst unter anderem Feld (5), welches ein neues Fenster zum [verlängern](ausleihe verlängern.md) der Ausleihe öffnet. + Mit einem Klick auf + - Alle Ausleihen + - Aktuell entliehen + - Überzogen +werden die Einträge der Tabelle gefiltert. Zusätzlich kann mithilfe der Eingabezeile unter den Filteroptionen gezielt nach einem Titel oder einer Signatur gesucht werden. + +- (5) Verlängern: + + Um Medien zu verlängern, müssen diese in der Tabelle angeklickt werden. Mit Strg können mehrere Medien gleichzeitig ausgewählt und verlängert werden. Hierzu wird ein neues Fenster geöffnet, siehe [Ausleihe verlängern](ausleihe verlängern.md). + diff --git a/docs/ausleihe verlängern.md b/docs/ausleihe verlängern.md new file mode 100644 index 0000000..6f79498 --- /dev/null +++ b/docs/ausleihe verlängern.md @@ -0,0 +1,9 @@ +# Medien verlängern +![Oberfläche](images/extend.png) + +## Information +Die Ausleihe verlängern kann nur geöffnet werden, wenn ein Nutzer offen ist. + +Diese Oberfläche erlaubt es, eine oder mehrere Medien zu verlängern. Hierzu muss ein neues, in der Zukunft liegendes Datum ausgewählt und mit OK bestätigt werden. + +In der Datenbank wird nun das neue Datum gespeichert und die Einträge in der Tabelle werden aktualisiert. \ No newline at end of file diff --git a/docs/images/add_user.png b/docs/images/add_user.png new file mode 100644 index 0000000000000000000000000000000000000000..a322dafdccbe19976b907743e4ddf2b2be6770e3 GIT binary patch literal 8103 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 Pu45?5se-Cx%tHSQsK~s+ literal 0 HcmV?d00001 diff --git a/docs/images/createUser.png b/docs/images/createUser.png new file mode 100644 index 0000000000000000000000000000000000000000..4fdec9f8d3dd58f6b6b2f8a88d3fa5d234b58f2f GIT binary patch literal 757 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 literal 0 HcmV?d00001 diff --git a/docs/images/extend.png b/docs/images/extend.png new file mode 100644 index 0000000000000000000000000000000000000000..3103f64f218a57432339711857e7d06d54300556 GIT binary patch literal 16891 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 V_KR%9A2vLa7FPh2h#G$WzW`YDy3zmu literal 0 HcmV?d00001 diff --git a/docs/images/generateReport.png b/docs/images/generateReport.png new file mode 100644 index 0000000000000000000000000000000000000000..2ee367a4383b3978348969122e5e227849d352b2 GIT binary patch literal 9327 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 literal 0 HcmV?d00001 diff --git a/docs/images/generatedReport.png b/docs/images/generatedReport.png new file mode 100644 index 0000000000000000000000000000000000000000..6122367a4f25b780034447850cac8607a42b8203 GIT binary patch literal 9903 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 r9@#Q6TyA$b=*(hII)=g1)z4*}Q$iB}hU~9p literal 0 HcmV?d00001 diff --git a/docs/images/main_no_user.png b/docs/images/main_no_user.png new file mode 100644 index 0000000000000000000000000000000000000000..e283cd389cb2ff7250176cb0c94089aa464582b2 GIT binary patch literal 19885 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 WE)&i#0rd+6(pG=;pyCgkH~$wuNBz(M literal 0 HcmV?d00001 diff --git a/docs/images/main_userdata.png b/docs/images/main_userdata.png new file mode 100644 index 0000000000000000000000000000000000000000..41a6bf805d007adf2b8f89f9da921fb081f8bc45 GIT binary patch literal 3913 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 WLParn;y&o2fvZLqSIREAMgJYS(ND$z literal 0 HcmV?d00001 diff --git a/docs/images/restart.png b/docs/images/restart.png new file mode 100644 index 0000000000000000000000000000000000000000..249f44da1faeba213bac1d39a164dcd9b821ec88 GIT binary patch literal 7352 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 literal 0 HcmV?d00001 diff --git a/docs/images/settings.png b/docs/images/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..4e69c82937f97ca75550028814a6200c8aeabb38 GIT binary patch literal 30876 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%0A^#<=kz zyHcWi>2NW)R@Zc*_(iLt9QSfcuZ%6>E5byHb|Rs zuVUxFv6sm(M|#<8{*w3$DOg*H($P?P_8?`yhUGKs-goAD9MjVykyw`yhRIH*RYw%> zL+7WU-!J(W`?9Gj=G?xCTh{o+3Y^9NV-s@uNz&6k%$J&9JagE?t?H2%GRflqW2f?l z$l)vePua>jznV>^h;wK@tI{2)aOuCwCfD!j&Yf0q+)pF0dMFjwi^+6YW;e~~skFl- zrG&#Gu z!bQ4Y`Qha=-Siq2tDO01XE>%%(Ga9uGgKk0e3cR^k(@nrEDa&+P4K%vBT;fc8$0r53+3Pe^`Q_Y>G{tdS2>&mUUrgA$jIvs- zy~I`s-_cog>3m6QEuxFOFFG(9 z=4@y>8!Y&?RJ-1lg2x7C_3Hji14Iab#_#*n=%d?vfk%cp;$uX=tIc#e!fHyrN%3~8 z*LAXliUbkf9k?DSrTCxQe+u6AHUeOiv$I!veJD%-$uS@j9`N(=J}|S|(bQ%jmX=Dm zAxgzfJ#K7k6GgrDV@o7tg=#DXyo>7xg}r_MIFjIVmCdzbTe7b1Y4+ zpY=~4`W!9~VQUM}Djhj?pO%!;3AujWOIHKrP=ia|`t#FemE(qzx&xSrq)CJX&5hy) zXQ9^)AUNPz4}T9>4#X`_+0qMo+%W4ulBQK$F?g(YCqpxd^ZKf?={!x{oQzYCkrZZC!7SiRX3`{!t>dHhM+ZZGpUu4+T`V*pS5k;L}T1ZO;~Mzq>>cUA?7sSN08V!t=718moKg$} zlR?BGZ%I4(iM65m$Tn|@zdvY6b(qBjjVYGjR9J%izY-WFWNh}>;%Ywo0BdJCaIaMx zR#$%gb36wUPmT`yOBL3qpT1P^E%Bt7_fRRa{#kLNtD-^pzH97>82 zdQjP$n?I740Je-fDkfn(%cVP>M?=(|hc{;_^MvlMrEhjywl3z^+)m@%^6s=R!`hz@ zC7)O1e%8SyZ0}8Af&vK>#^r3lz?T)S`W+i0;r$an?&tpIIwEo}b`>3ed1yHa)x1?% zvppR}$fd5D?%v=q*>9PX$Hpo2HvCqJm^{WWnV|^?WxI#9+q*9$Pz8kwanMXhlEv zGEbO-o@4_ozG7ERCvpOVS6F_E;|g$Lsq@2AukE83zqaU{qn7KJ(WL2p(22Ifk$h*I z;{rF3_%|kc%VuoM41TOa4Q!ax7ZTDohu!&L3(ISF0tZxuCL0W@j$w0A)heG0L2{8- zjlrEeGPh5x%^MC5x1|4!?YumE-C+$9XBU`!1bkxAi8X}Nra9M1&2}%eHwsDjInze1 zxeMJ6a`kz`Q-o`Kd!~U3=^v29v6FW#ukby^T9lZTmVIs`Z2iT#lytxnY}w2kLs22x~6>7^Z9%(f#wu8gRqd% z)EHN5F(mMHzYA6!#lB~k1f8Zkavj(gww(3V>BVKStIu9OTk4uQ-6z9uVytVFsT>8j zys9Ua!it=sXYaDJ@pv3Retl4s?canfADsTPmr&(4u*xz9+56>>p`sBoBBgJn6OK}) z0KEe!3)NEc1XW@ZVlT8ZKjM2UF5kSaUr{4d7?f7#D6GuWfVH*ccFvePrkbQsqc_63 zMpQ0Wu4>g?AAy!#W8G-z|SzH&s?wB?KjlA`~lUGjQUnt zhZfTnhIr&coB7Woj9$51L!5W7iN(xH8v9R`NB^$*WInCh{j|Lr4C$|}o`QG)u=Ga#B@#V*IPl_o; z`Ta5BkoswA;_HK}l!Y*=L9+2ScwT+NKvb!WWbaQk-7(hy5qG{`SzD+5fOY93CNj^V{N0zM4gQs$`{)U@8%^g?n!t0D z*^hQCi!T@N^~7@0PjH;!r7@H`;*7*$(Zz4v1w-`!8a@@^)Uw{Ees*mo5 zZF$}oUFw01*{#N+WnkpXdi;T9A7wEoY4a1j`9I9`9Btz#u>#u7x?(~2rrd@p0%K^qP%!{X^cqD z8h#R!yU?3;KZTeWAtvTBt$|xNql*%~G2(8F(I`)iqm&}s5O+Fz+op!D3(xMS1APjH zG!$d@vo{M1j-pW-VE?{LvDCy4TA$Q^w7v*x4-;s13Up!pr?8o!%*t!$VMatVq8GY|FLuSPO@LpI@O$!$V0Sb7HMev3~bJ91NBF% z`B=;A2xE2ULy%F=V|Y?ZYtL!aAm~7U2vT8EWJDvF0_Lh$O#{h)>>0HeB>Z7_U##L$ zDK+~9OAdz~Xm5RBZ(a>|*R2uyV8Z+EHmMXd1H_FJdGZ)LJA28)Cpz91*P(((s4-u( zFcV+)j-b8iXMx@|A*-ZfhVCaz-kjcahsI`D@x=Eg*bH}Dq>z!&5oS5egxy`&-9{mX z5j0NaYFsC`x`!_$%6M!;ppaVm-LjdbD9BEdKvJdIC;XeS zPU#GVPT1en&Q~pbT+A+@`@oE@epCRPNR+2=v}EtbbkDpdS|B^4bQM^=TW7n3R^#R{ z?yas50^Mq9v6D^2G3^4AUy+gypKzloRc=3mxmcB@X&2^k+6#L^9bh=s~hMYs2=|u^$u^ zo8jM4iwU--r$SC>SEIB`aHmDr;_YJ|$AMG}5Dl7G2YYzn&YBO;^4N8hkBQPgV)G7>6i%g>+)-?9W zP~a^Ee((5??si`GyK)jacx^@br`V>oC(q}jMfu+Ex480+s^Z|wz*=E`8K(P)etWI$ zk;|Lvuzt&F(~i`nhIK)4~1EK&y2pw^C%9GP$Mu{qA%3^=g6q$ajcTf9hCA z?>?H>ZYM&1?)E|8cb0ntW2pYlYXvG5a+>tpW}JB`cejG~7wm4Bb-_Pv9?CrzH~A!n zaHur9WJc1XKN4x|1wChfxhXD=2VVKos!E%LxtjgXx{P*o13u1h_~kYi|6a89QjiD( z#OSCmAN4@|xG%p~q%)c2mthE9Jwy$$B^X9Ez=os^SHmo(s{#31F$KV$V9Ps{Y`Ga8 zy*J(DS3pZLpX2KA|?Sa>g%2mdx- z#7sTp0O_hl>YE(w`)5@^Fm!eX_xi5o&q-e`QTs29uZ9tZZVqptZ0E(D9!fBks| zauuk7vm{V1fj2I-e0-j_9-A46>C5k5nYL*Kk5=55+IH(dUz@;C^ry-`eUcRO{bJ}* z=X!YkUvpFN(k~*vs2()-G5zQ^lJouZ#<=&wrd5QRwJg8% z;fFn!SewNx@fk6f}T%^14T}l;39wKm|$SsTyZY*deb|L!xSBv zPqH*XlXHPqgyz(p>-PVOa_EzNeqXPgQsQzv;mCn{esXdqu|c2pt_y`Nz1*43Tm~;4 z2fXwM!mQr>Ml?oTjEibXz==W}CtXu~!!)MV$CzHg*YRwjQyHmu}7A-raBxk);~%9}mpzmj;Mx@v3qWTu zHaq27B>*(R6B`2OH9Kl39U;jShuknzh__^+c;tX$ClTrNi}wY*&7*bo9$5rXY=qKk z%~mBGStXdTGZ*{S-IyDlP9kto5jG~#DUdrLlX zu|_*oyS!rUP~P$Gy^2%WDK*}))Tp1wwv8Xos7Er3HS|-{Ui}OHCI3vnfdTN3WhGs0 zL1ts=y^_7J;J=_BcqA`iwxdr$0np0KpDe44d_1~Y076do=j$9sHQZCbBnQP=@sDTj zL~=8vT>)xh6B~uy+^Z8UXI5c3PrOj=tc&D@Ou11P%~3Hf0$TyW2z}8vqY@R6j!R*n zwE~y%#*bk(llK?B)oX@GuMr$~P(W1hr%>u(HNV&pn9xka=K<#cT5tUcsuO$I!mfZ0 z7S5qM*i3EbLF5j$_x{J(5QanGjE0>cov|g_g$G|e4mM4k{IkZx4^I!K_`H;R=8H5i zb@u`tngO#t58KBcBjUAUKdIQW3BUn|Gzfh0J*AeN3@)wv_lhSl!NNNpXAf2d%HT|e zi=5KVttbjm^wii7C@4I9K(w2h&v~avNpic~ONGr^&SYGSZnPyeN(D$Cv&(_(24t8J zg{+5Lk5$ZoBxi6aPb8I0r<~0|t!a0BB0UJK=>iE}5N-#)YGP{MSP@ME;AI{>zOVNe zY_PHT-%mdN`%dl9xEp*fb!+$!W(V{Ewnd92XREC`i=$E}FhH+ymFm0CY1|R>a*a10 zV5OoQdXdCoGKMQxQF%md_jb>MksW{x;<6ff9-cxPP^rLwp%HTS)a+19_5J$%xEM_$ zta`Vppp-b0kF`(L{HW|pY3LIY=&km)Bl5z#G_~Gje72es8Kc*tts`f=SBmKK#kxy+ zjl$#nR0$7MQX?&yz*}lbgy!aVZ9RKRYP>6^TlM(z6HmHi+e<7(Q98jg=CzH`H#y<6TN=iLLJ zL=1(UQO{K_a!(4}R%41H{VT+;xM(4H`D$cRY#K=H$R$Ju-6Li{+JDt#54RYB-UN`Q zRfqH2Bo_BG?-^>t1ks4NaSTZG`>lokbD>Qg+ivOCp)A zEvo$eUZj}}D8AcX(~N^n1MLD{+min8E=XXd81NvQUkMKu!mfl71gr+plLvVe;$Sf< zdZu|7^Cpv;$BdL&3tjeBT`!n-8ond3&V3(LF2f z6ce(PT<_qmv$MndGL##1jvKr0@qoN<$vmB8aDLi$iO9sgQ0zYF0eGMX;CpNr5UQ^r zsNrZEZV1jKxybBP=8^sW?CdB0n1+;f7Wo+rf1K|IwkFa}aL6w?!hgHZ8&XX^zxmk@ ziI_=efEWV+2{=ZS=~CDel7V8~A0zQbRh!o}`HJV%yEU{2n@Ru$E%}0NAOX%>J*!$& ziU^TQWpmNbj(4x_7$O*c9_v&x8Cm+sH`0tY&V!-zgpLsIG$G+UZ1oETu968{C5-lK zf9;#i)t0gx0{Eo$Yu{qU_8Rd=_E50G0(?y?fLjDM zW`I$S<|7rl*fgbIKkTIh!3AlLO_J}XM@RRbf-xTOxAUGyATdIgzVO2s*dS>aM^V!b zYWUt~fQbHs`C_*HRo7#X|C`w@WhHgI*@Yc620J;CpGZXxpC%8OR6d?yE3Px#f8qUq zKK!6EG3?SV%saSOtB1KnrHf(K)};1P>;-QFoBGDS;Mba_hiYk|K`hwoRqYG5e8@0z&$G$ z*D5a-br|9d8eE`Lhr6VL#6uhmGpqB6JJ_PL+6BszsVO{k4;MO6_qmQDo7@AwLKZi$;r zPvv6sjJMBGait&&V)A^YwRakc+`Dw{y`MxCA5Z%Ugh(j=Zj$@yq0`c^-}Is$6b z9B5T(00FxJlj-v#l!xH+?7;ox3Ew`X`|xfi%YtD$^dVNt-qk6>6UPIow<%d5`1dqZYSPizgME|* z*nY&VBQq&#NrOYdkt&sD%{g~VwFzCufWlt!O!}$ldYhsiu;aWHp{beS=k-aJW>i&?x>(abLO7mn)9`)6tC={1LJm`ZD zdX@2bKrn1J;&R@VLO)3mzo-&M#=$@Ou9-hHs5Hd%8~+R}INlLXVNE9U;?&D&_q(Iu zha97)POkXq;OilY-@lHoty;Wlt}Vd9N6csr@O+y5r9_Sp_t7J1uV`C- zGhhDd?ZSg5UKgvfZD)3U@R3FNT*OJGci#ij#=PPBdPw@#8x=Ppi8ycC`lNxG{!*td zMzOZUtiZcP4rgS}R7X$Zdx-^m_Ig0hL{32R)55Df#nCez<$hJ~98L4mBDc3XBdk1j z@&0<5ShNLX%B@WjbQ&)oN41;lKWciI@eDf-V))3O@;giBim4oqRpCi0(EFd;ap@IA zllE8}Dyyb0g47|Rm4fTv7$?nxwIo|G#!+8(lot|iMhA4bT zax%8>^IO=y8$De;P3TZevt}EUCAnJ1q9p1m&mp)Hp+*DB--4R5^oW>`z5CZ@OufcP zaL@qVUZ%#ONjLLN+k|UgeTl{cw1TnX9b5KNZim=ph}er{h?gp9NKUQEO35X`JC-31t!+~kse6Uah44@&^zGI7rp~%W%qmJ*8 zSh?8brZ{`SoFNeC@M=sx!0GC=bZp%3*&pU?cRK25`Crv7xL}S^?YY!z^~=Ca$~N|R zV`C;te=J__K@NgKL%zSXy+ex!20IQKPYY8pXN2hBpPjnce>16FVH`^;NvPUJdUxzH zyZkF3`^&~gIP5FBgKdaB~n7FbsJkI$oY8=_?+yO|YO z3*(6;_-X7?aR7d|a>g*urde4Pjdv`bxkW!D6p-xhF6?c6l5mL9T0oS%3^>bQLIx#| zo!#+XXdCdko@19s>?OgAf2uW9w=OU*590NeX+sLjzlbY;n8kHU`X7#gD~KW6=UuYI zb{TEwt{9??j7Siw+ew7?7I2`mMs0Qk>%iM}P@o%neBd_x5d7mamJ-b(_e!HG+CR-8 z_a_MiB(2dmV-=4x5$e`cK|Lhv&&*bxS;bB#T&8%^vHHgw(giW#h_7Xo;OgoDI2A`* z^r~)B#&fJtO46|WhegmxN!nv4=>@Em)N?Oik0%|B5;>oxK24UIgn(mY?m*LgYu;jQ z29lTc&QS2U60Gsz8jN=eh+6UY{v2|s<_6L?9Moz*VdWczWAkcj@^f+;u^AD5r$*tK z=i-11X6Wb!WX~?fvHB78&h=G&DB|=jAHm|j*OZeTJO$dM?OSuz5mwcS#KuD&Zy}30 z5%2&+lc;K)LA$Y_S4~Wwj@#5rgWTUPx$HyE>X4QSuUxx7alJAf#GAOKQT7ZX5S~;rW(O}DT}VQrSKwxK zytmpjHlD>g=T2&|5T;IK#`BWB9I{~{G@y;?c2ERJM%fm)Hb;DM?vIupiz`nElKq_;HH^J% zLH4124wJZbBFrrf^SFZM*M*#weiBz+)?B=RQBNy;D(D73%p>-+n08RY^)7O%nA-_+ zuM^qRL&tJHTD-^UJSkY>%;Lq(b=b!T-MNuZDw1ftuvVX)F zrw9#T#;C~{57Hs95IuSbg3&_MsKG?bZI zdcXJQ_xG;*V?X=swVrkMS!?f8)^lPs)q&3lX$dhfFrF!Wkk@|n$B!0*kNb$b94ADN z4%0&$D2GuyPQU*cVB5;7$zou@lZfujaUNp=w-1IM7#PF@|1r!V*Gel444St}^0K<0 zOpf!2+;sJ_hk8z4cx;TVXRU7dCXrsOSbk))Y#@qc z#=au2oG|f)prRfHw)x-vlaq3C4s}f=BX?{K<7dJPnGyw4Btz!m0i1ZEVUPEVRZbM^ZRaI5Yom-5#`6J({ zP=|3mg~`$YUJp&amZ_*H2ok@B%qWp!yJB?RKr#*fI{+IQSq7P1gU1dJN|;+zvxQ=~ zg5BG0oRmnzXP@gMBuoSIlt+eDo!pM12Jm{sl+jrc%^9xyL)0H-w!SCK5I~tr!&{33 zgC~HVKL5BKLl zb~a&1;CwYeI`De#IqLW9(#7xK`%q!2b1G-Qe`@o0Lh+&kVkuH-3NGd7cO!f(Qu-ReHS~E1hMB^9m@&G!9IbzFy6WCPr{3= zriFmJKSSR#Od=fXxbLdu8d6`w>zC?9x7^C$YOV3(f#77@879BIqRD@ujl@WvXrP1$ z$M~DdQzs^XGb2=ONI+f%*ejgOxX9idU?pDfMS29cuuuBgjM+QwTMPZpxom|)v>A(3 zr#&$Hyz}cyQ{9Bbde>Wr7!L=ABBUD47EK@L6`T2Hffc$))W`XR!?owWxq-IB`?$E{ zwA-j`1Nvz*hC{Blsp<8nFSNW@M0}lv;pkZ%jfml8ulbJ$P_+zw)4+7zt_$s^8vm2F zC}F3C!s%n--}F5t%SysoAq>%zV5A-0I468Z(>~3>AiFp0CE8SuqeX$E4)C&nL9@+oQdcicAB@71sCz z>2Tzu^BJiK*82y-%`qJDxieqaQ`Iv!V}1HFNImz4v2xc_dg`-*+0R@^i)z*z>t3}sVt-wXPYBET5rDM=E*=mK2hjU%X$Nt!#U})@#N>- z5P^EHB+Tm88RsVF=)}=-wu5%@mL|82e#{dAabQJ`^g;sH$a0&99$mYyH1N%D`9JIO zrDi;!Rv2i-N_&(1DyA|sxFwEV^^2JWN0@Eg^P8KScv!XA-I#g%UjO;ryj48=q=1a^ ze*1H*sa{FSPApG>wA19+SG9H$;^LKMW!53gT4G*VpXUo)}u@^!7EuT%niH zGWGcdD@?28O3c(zaC4p;!*2g(u$%6Ca3T05hI zykrT~cq|l%prom3(Nr0ipQT%-$%DmYzsX%fH0idfFUMd95elOtljHTZXV+yNIRTO@ z%B>=%qT>{#*yj9^0906wr04yE(A50OyLaQndj;J>7T}sdPP%FGJ^nNox|3D zZkzeL>$|&8c(ZexQgkou2FPCj%2cE?&$yCwPVo_3f8MA~P~|+E}Y=Hhx7qc+XSoc!7fxac`S%-D;B* z75lGAPQ_mFfbr4o3_yeWcf?~BuuANz2m(0Q9G7H+s9ruK={Yl!Imup699ml;2MVE; z@Yt)EV5Gs_08M7+HR&3`2D;L7b-@Zt4lP=G+C8hHD4abl2c-7(rh|hf0~#De9ToPg zF1=L2Uy=yz$hL5lyk4!j@$_oo-YZs%BYd+x*E?Zuj5FQ#Y|()`?sgROtqD~)Lfe;b z&IMMiJ!ihAvezM)RVv@{M`LVup^u6}B)57Y4Mz05!3y4=68@=)sJdABM@GGY81#vQ zJyqqv))toY{l#hUyV%Wv=texD!?|U0I%+>irK3~f!Jzs)t+oENHwt*T>ydrdr1i9` zg@?M1I_W5oT$1WLykKx{4y#3QPvF~PevwVh3*px+i6l#US$~rJk5Pjgfh3~HXr>v) zemIwL3zlo%#FUH3Mfc#6ZOU%p(uQAqP`x9YtZ^Kgxucq-(T=m%(jfXNm!%vi1*c-D z!61rgSwknTVQ;`xy(~$TV%JW@5Qfy6z~*)jH!BQbT=R&h%^9m3ub`i?!=e=-<82Ji z_bw9QD-Jwt*>ituWOqM0Mm*o1a#FLR8dp716W|K|Gp5q+)JdJ}Tqn3fxi&yBk>tH4N`Qnw|=8X-|R!Av;rGgXQ$A;v7 zIb}bxZ&`F>57rV^;~6iHC1^7y*>A5UtNRLzJ#=={1kw5wGhMP%KI{vUp2i0u8 zdp~L!-5|*!I??Wj=KkxxtOq0l@q{>tt;$RuLAl9Z-06ppl${%-t_XieeBFw?^hmiu zt5KG8*LDe(iWOt|7kklwC3A~k+xG5@Uf$-BG?SMYT* zr@Rmf#D<1C8ay8E`&mgzIx4o23C?qW@w$#`4#r|mHUPzAUv>EVtn0FABi){s=}$gs zepUB&cCxk|$5VlA^Y(f(FhNI5>cWv9>e)QcycEgZk)nE|4j#z=HmP*#f1-@&I9U2b zA6JnbkXm6Py=tykPu>RyRk^4=a6CtEa2Av_ah=*~D{%b=c{2ayE5(0M>ms z^STD5C~=)vD^ay%t2|=Iezg+Q@y%MO#DXb8+{PP-hrZM54MNvc@RV=aRjyLh?%vR_ zaF%hK8N05?dlA*H(``C1K3grbz*q*gF{`?+NYbsf8H*$~!rT3Z>R%5V=YVLa}5w+^F zPRzP3x=ceX@NwmX9Iu|(n5lFssA?jb%YK!mQO(-VH@Cy%8APtq=_rS|Ot&d^D4APr z@#9`2dFa)EC9lybs5xZA&n0QYO1TK!{iVh+>eGt1clvu9qi;HF>Go%Ysq$~W;0aF#p<|6rz%R3TEC~F6`jvL6R7@FA zogmaq-D2UzKYGcCW_6z57dKmnKgBFCT9EC?oRBXkC20*fznbA92?+QMQnZt3Z9w3G z+pE^}r50dfN^-UjfdLXsmq7nEBg??@PR2Cw@4Ce0bZ2PD0^-s^>XuE;X_ z&N>+~d0#6Eur@Im$27Nf@d3El0Iw+sqUm0;e0>Ag)nX8-1OV%|b?-GJIpYdVJ{jlO zixj_!4J~+4x7-~qyk(A=D&a@NaJ)V&DRsVaPXXZ4I{ZpjT@)B;Q1(VSz>h#=fw1{{ z-WFy(b^lPIhG5ko01Vy6zx76^F(duRP zT^6bigOz7bk;ijMZ@&iLL%l3pDZf6=@GBU^)Q{rAXQk3t7R5@PO}A?*lXOPce&|ZdV${k@9H!A9obH3hC8-N>kV0ac&pvL+Om({V-H$IbN?=-jeJ{~N z@25pB zC$FU5SvrzopK2RY-jIbgyx{cxB3k0c2ChGCD|7yS}{B!W5631B_|h1wMUZJgb)%wm~Cm)n6YzeAVc!%>g}r+ zagI~@0>UzS)bXrL;V}8Ce_~%Ge&XAF+_nGX67V2_`n&35@GP?rSB*j5vt4b=8c4T- zPE``}GUnBuiS^s-=ofQjyP=;1qstCKs5l?9)!w%B-Zlo|C-5B?7ZyJZ-<>w3k+0)d zz!2Uzla~&XU_MSI{TOwKdE27>Tp;QVa(N7I^xxdF8n7#MC#jwDF#J#{*Mec2gusfM;ki)E4^Or8cMyGECRIk`jo6Alpbey0)Se;tSC-Z%wavT?i zx}J;cUI|_L_rA0i>)Wu_!}zBkhv(;HXRtnAtZ~~^eY)Y7Y(Kb<+9-0r_{Uk$k;N6s zk0X0O}{S-8I<}7_MFMCGG(3xkL2l;hdl`=kJkc{Pf0T|e9sJ|_? zUXqsxqH?0{Lby|nC+zv#_To6XDv-iI0=TpcpaK?iXr%Ru#pK>0Gbf$trmW7lR%48r@L z71#LrZ`QcI4QpOlvZ6LS*lqn2bIyo9+=7SC6{Xr|SJ z9k?S$!ZYms-QA>$;O-;DC?qNhLkekN*kU8U+C=dtEG-r}J-w(RAtk2n8a$<3z<5#^ zS@Lmq)c$XB#cRHt@`RBh4-LLTm2r9%ef_k0Yqirb@_n2Ft?`P>A^}>L?C5mLX&_jh z8(;2*W;)@u?4u&~yoUMH)Bg*&CY;0meH}16KM!14S#fxAlis6+jbi5Jj(ZXs0&aK4 znteE4_4W(UAfnv9*-yCfP8IZP?W_&~cR&%FK+FAhkMq>-M0o_H9;)zTVxr>6i7yy_ zGr4zsT!uz_&*8d+#|bZ=nM(xvmBwY58p!Qxj0=jDBuNLZC?$3J=Z+VosEmwre^72^ zL5X6A$+9gJ7qg_YAP|Vx8#nhc3AOomtzl^TqvEj#w``^78O{%pym|RRY3ZRrJa$Wz zXfIo21Z6)31x2N|QrcR*%2Oila6_?*aK_uh#c#_mixaS_kL8cN_8B!F zV<#BXu#NEh>4E6W4qHi@Za0oUKN zZVyA)j&8w+Gxra&aUJZ|>(dc$K^dQ5>RWC-pQfqg)CweM-ZC(LdH74rAg0&C14)%OCFrf_deGi1s1GSTy&E(N;Mt;D#GXH4V zZA->XJCJp>(qV$lDvw4Iu&83E&Bt6kCck4cm^tf|4JnV|&X@#QYUza-ZpR(7nXJjc z1|<`}FVpQOKKIs9mOsQ99xrxxUoMO-bh=j2^%!#cdT$Ik`I$C#_RK9f)18P`)&Jir zVb)L!UA~9G>QkWnA;H4f7KHkCsECf^z~e(eq~m7#;jn$vvJE3W`h&1r1(jlp?XmVu z#z9cueeS_Ct9}`J6bfZ`_xBGwyFvFo+UQY97J@Gr-+PjGf#1%3!yZYW!`s^q&b70M zVa*UG{sN8Uw3nkn;Gx*G_@gLvro;J}guJ!=H75VtKhZeOB2+&qnp4HisY zB;=_z#wrT(QkT;-Ud*tEoFCdq-5xb+iJjv)^E)B|*5)q_TAV{}G)@mSJw6z%`bQol zvP&`fl=6GXg?u-;5PT#oSkmigwtmkZTJ7fUkQnAB}&=fgp;tIHnFx-6Us-C zJk&%10bPHYW_NgaIHyV_OZtXxkHYUVGc9eBqaeT{vBSZ-h0)#*Ugd}(4%=w+O?z(5Kd(Zv%w^kr4u(S96+WS7w^FH5>GB(uZJT7n? z1Ojp1xqZt71Uf7N0{!y%x1+#c?!tIVfww~eCb~C46$3)cz=vPmZW!DEfvS)v_8%Mp zJ|BB@+d2RQI@NygbEq-FiwpwkN!_`1!wl*`o;~r_ej6^jmwn*^f2@@ELaQt-R>%YT z`R<6gbx@7X+MlmC&RUYLU8`*}6#b2(IKD{#p|N%xEZz9Zaf>H5v^Pbk4KnA%?|--8 zm41G`@1lK0qBy*JK`!q@ticB#=a0_Nm=e%LxfH_;%$PeiB=u)n(a z(>OE)f zy@ZOEHHpPrTQpkN%2+AkOO94&hP-pNH_$nzy2p7CTe^t1j@Rn5QXC^xBG!lzR~=yF zI;Vl}ub*{J)pG*|$jqU8oEe*QD=XoMany zaj~dtI}wWclmLWOe@tqMbinxj3retmXQ@sEi-^$h^9`cUhVTX)%}4!ZmS=3PW?xRe z&7KRV_U{_|s96e^coNOMEQd)2=lb;q%LhuY($`tD*k0nr<)BjPi1F-rt+cwjIwi^| z{Cq{zA}ZX$20^YiU#9p)P}7;z(#m5XP@=Za&SlBK3u3`nHA6Qa7Kq&zKN6#7Id}F1 zVueUO7greC-Mxw(vNKlNrZh5X)4F31iz_QD+4!yfgPyhX?>tLsTZhXaATORPIKa#5 zgDlP_#*zsA6`BdW}9>FMdQfy0Rxi`lwfUS1fMADm>nODo*AOi_FE?Zdk# z6hSOSkcli1cg+jMfHeOOil)%l2CRAx1_I^#O?ICX(UFzFneD)e%lP2>d|cTjopvm@88FNSHtfbhA-)o;=S0YIT#WA#j8nz2UrX3Uo?cZ7~%T)z~q@t*l#@%an2zeSZR|3GM(9ob^NDiqU>AY zt3>t#ji1w;Haw3XOzB^D(m38Q7e;!AHoo%b=(zDHt>v~!QISu@+bmm{$DHSf+x1$M|%vj&TTq(>HvI%;!EfS#mz(N-sSF9t+XhxKF* zRz^qA1sTb3g}K8uu0KL9AlQ*SI5jvzL^J4~uHE~s6atQyy}tXp3sFKw?N`yWu@@Zc zhtwe}yWcNqriCa}CZV*<)9;P}cQjnRTxBM9kGy2>v-tMq84!&;%{$?UfU{dYFKy&# zuy+TEY>O1V8~H2EqzaOwuIC$bQg!du-gL8qZS=cDRP2}{xg_`&2k8DDW6Y5`wyz;P zR{ibE{6dV(KuVgviRN0m@1AbqU-2cLJ73?=qn!qJz?i4^&&?*Z{*0fkJJ|~vFGS;- z)*1JI#?Pn*2gZ%?jc1O1S~$i4vpZY^1d?s~!J8o9QqT@nI170^`LZDWFq#o)CX<$~ z`le7rIFwp4(l`XH^3mb&4It?A<#kW=0?8{i_*WqEz6*SI5bjmm4}(CLGrt|gd&7gH z?p}fdzYDk_AkdTHV+YA$`M){b*J97n(GlMr9?hS({zfla4mc6+Sz%VsAG#tczasgU zzjrUS>?}9|A*`qB+j60ptEGUohbM3c|6HlCcue1{u-M#zGdUE9C4@HmTudl+v2!)o z<|GK@nW^l5K3+s!Ab>a!w@W4R2GrR3FBgPb*Ob&3Cw-IS=HwKqaqWEOmes9=>t3yT zae~*iMk4bG(6lTw-|+1*OKRfS*hYfmhs!4}n(sl4ElZYvR|SFGbtL4oLgqq4`K$S# zaWGA&5;=!KPh1PCoh7sswKm>mYY2ltvEvyk0nt@mZxs&xs~dEF6UnS9D3_k959Ap! z_h6yEj@asFxn|lj>bvaG&I|vPN%{5@Kx{I4MM9RIKJKm71 zJW}h;iP(ftSyFwN&}3_u88PyZLe96U{~KQnzSA6t*i4XK6W;!)=s1}c`fcR zQzqz7T5#DbNXSVWqaR>NiRl^_y)b4i%ZWkrUUsYxh8Z$osOsTKRQqO5 z?2qKJq2b4Rib-3a+DdC&Mm8s3YZk2RjzWsBA%YNNH>xl&zh%V02j{fbJElSKv56vj zvsQtN?<)?1vA54)mhJ58W|ClYzg9Nmhbl>;zQtAZBm$raTS|L5YvJbktjzmK#A>pwrIcNZdN)7R$h5yhQ1V8uueDl(R0XDb zY3U2q3KG6F_0_6@panFP*!5Y|I>OxI)p|5>FT|Yol^D%i1TP~td6yNr+y)<0g78Jg zLmaIuXhG@VnLQrqco(iY*#VpA2Xlu(A5LD2{+8h3$eSJ+tYFk1HLknbT*1ERV0=e56CxB%t)Gs8OIs0uy0=vWJ@xgq|CD`}ne8^4J;L;9p(@Os@EO(U(To z4N7V-t$9$t8E(NCv_)qwuvmMRp6S+pni4MFH8GIBKA51~8tDJSJIIgcms7+5-NFKJ z@O#@ zfxSnoj4IbfK&BS9$}6|K;6tI))k`Mkhd{F8c_&5ElIyDS>W}|9_UseQDh>7hDi>iG|UH=@sJFp0E&!GZKmy2E>VvvFlA z*TbCE`d~aI%Td3<&qop!ow+yD4j}uJU`Zwbfz}^tfV=Z0^@Od4&W@D47=HL@^Wm0# zxW8ON8d#DCLe&>i3ziyCr_VSoZavyaPKJIjnTlUMAPP$OaQVf)v_vG`{_&kpQQW)G z>c2zximHwy!GEezNmm}jm|gFBcI3nY%B$kaqU^W(kj-y({z+dBfgW`ktO#%kl)jAX zz1kv`6~5WMGv0tnGD5ni3Lx`zkzE`b8&93YfgxMno^V70!Ttxqw6^PcLjJp-=0*GCul^BU|Am8PhxrYBpvfRA(P+p-9b#x8GSUcw7 zSx44v<*{XNj2)a8V2pl1}-(-*rm0R8ujuRz-96U+okLpWz&-oU?3G$nEC1}WgA{9EuGgg zl|;SqRQ>A|2!u-Li6v%+I5_;>#?jWhOTe+##jB}3uls+8EEO67xj7H)Hjv>|CU?=` zU)j3(^fO&IZx64Sdg;v0t@~}T@msdj2YINEFJ?}?3XKq6-qxwX3|$?(wcHdER9jnn zrbF8Q(KF&@*iIAujg!Q#RvWTJ~I>Q^`)KEAzWzkqc9d zu)GE(&XBInV_96F4_m`#rllA4&eJsIE~o^GylN8`9=fN0ZA2=jGbVcIhPWpHL`LqI zwmVarIjbHr*7)2XjS0engK>*NlF`f21JXW2$tc%trNO&(!X#%up?I>S7qL7_+SWwA zTzYc!5%|1Egn@iz>m7?A=5nakS<+-p0YY$mt1@uOYt)dJka?J7BxCKNqu3XQvQab* zNtYz?LD{DFj35(1m+R{-QMb!64Q@YMIIA$()T(X~xEgnO%;fPpca^3^=MOEEZk-<~ zx}rj1H9EenIMf2>tq1gjq9&>zKkgtEs@%-G{yBL%xx6V$>T8*`XjSQ6M%jDJHCShD zd@alVw3^q69cJ~DfQQ;u`np0QXOacFvl@WxLuqXzO*@(9lS>=2TvL2%`^DPW-4$Tp z8A~6LAjevtS-zw`*?ZJOpj2PfNfGNJs|J=NBP&B86(tq_TgLYu@|);5f=kq~xqRO(xqWY#F={0YeHkf}^| z#(CIW`~vg)VCf{rbbRD{HD^N;g^_KOj5D7QiH;N%+zZto1r`nZIr_QoL6UbuK!%zw z1qb`Zd+uT!JuTi^n&ivxBfAz0I`{AC36H0YAL9}3Y>vs%OwxBX^-ss4$%OLGulKpZ zqM#?m<-6ZbOIHPVy!ncWfPE_IH7X}Bsr(U{ay7p7YbweK9ME3c475U+tFyvN^NV9H z5OY7xJwRE4^T0eZ>OxFw@t{~49X~yPLKceh9!U;)y#K)Y(~C3_ZQ6v*;T@n%=0nuoEtlcvO<%0qtSMG zww4za%=etX530XjIp3o==abcUs!Mtou2h`^_o|(3kn!!~Cf@1?uy}J?5^G{&BIH`O zldE#*wY{sD#?;S!sIw=sLdD~S)p&?mFkd-t5J-o%nC{mFAizxuM(=7AfI~%%cp%Kc zPpfzBaZ8>+fBSBM)Waru6fVlC$1uD|x=96Ik zjoUe5v%k5{e|#}nwqFXw#h`-6&ds}ls@C?LqE`#`Oic_TfoOhR-Xx9EN*(C!{TY-} zP?pC4XsjTaBy9ROcHRP{vHP_^x-g9eqw@jA{tvKMy1B6wEA8VmR9{e&DpD3Qh;sL` z<%WgD@NaLeMS+>U`P*^tA*LVx1(wH$0@hdDJv{jNEbxMyJfi7YFy|n4aBM3O`Z81u zMxj!11+!|Tzg`S{ZDT%?o4ejS1uY`wSWTPg%lB^s(9<@T`MGO}Ocv%-6p5lG+g3wm zpW67v!rK@NUE@vypeJ`qi_sQ($+uP!PPaFgAiu-ZRCDuE|H>!M#ZflKGKXNqT*#CB zsra!{OWX=kgSU;YJ@csdFC8E)pkJY%;TD1fow{kWa$mlcDA86|zI~5$Pa#e< z6{TnX%Uv67AMWxoFd2Pn&Lk~`i#cB~xoDnble$7>WM?UjmB=IBP6IsuGiudtPDE0b z*7ejgc?o`=hAU|dzfCQs`PdJQCB3vCs%mhcN}*HC-u(->Dl)@`qB^TC#|cL@?)BUi zEYgli`N7ty?ZQnrFEp{PF>=0!8GTYOzA_G<=z6YH`>Ct3a9BYyU88o}OZ#^Bwcbqa z#PT`D!W73kzqVt1Z5)|-?lR{t8vSSp1D+bLFWk*+(4OF|JBQx&ISji0(BLRi%B*Z! zXjYAWtLKl*mImA#PA|e+U%^b?;|89JA zy;a8W=PT5dP9OSAs&qC0Gwsq1EIaPHG>hj8wH6flG@cWYas?LO!Ua{!|}y z@xm%rl=#9(aaEKlE&uPkPGJfMJ@pE8N@VD9XjwFqa*LStb+S!mypESzIy_eT6LdoU z2A%q*k<|~Zg#<1aTS8J%CS&8?La~)T@b@O~jLy9ymlXA1F}P!pyBVEirZ&XiU@mD4 zaOP_$E|H->yp*J($fO3{vgZBKsrW20f9Uc;5C6jGnbN2e4suD-F{SI1y!1{^FjM#- z4r}nyACB~Dh6F)ovOF(~M3rv3>Q6!$315|q!t(D7ibiSmN*FXsfcbW3j(YAz{6w9l z-XnjOiz4fxvLeOH>;8k!!QejuU{-YRSEN3p`D<&fnBr)QqHmZ~J+5bT)N>@kD!0p7 z$11|j&5rp9#)hjV<3oY;7@iFB$3JJ9&Az)2wTp z+T{m7RXO7pW&#GKUE__KkB@OaS9P`%q{o4QU?uC(w!4@v2SAtbRzMXv4~Unz#3M$U z!g4Jp6X&3Pw+hc493`(H2+^4s^*p}2sD$M|!6UUKI;;?vS+x5GP#omrMKv8_!HPc5 zaR5xr_I$iK52%)dg(K>Gd~-v2x`gj{k{p3Zbm{L%G@=O2|8Q@6*$AP^rblY-KVZph z_h+gpIl#Bg=K*eiy5Jf?vaj951(x zNdtktd@XO76KKFkz~l-7LhS-N9EYn;B~@geGDZHy|l| zJBb=^7kd$JwjHqaIGWvTAXIy{~{U< z7e#uyZRS)oE<_)IWOM!g``ZWTR9`maYW7df#qc@*Sd$P4q)Z)!_L*_|`e=HMmnr)% z+!7jmxm786{1?#c{KpHclQo30gH(08H>$W^%=YSO2w(o*k-HtOp;V`MLm~(?dXS)6 zf%^>wc}RqxQK)lz?a7G)iUrbvKvs!1(}55(0(54tp-l*sl~PZLYc)#Zy)!QUnDC>c zOl>bOxuf#8XbkE+FtId*XkB^5#8* z^4j(6Im44pNpJ(x(!dwb07++&@L^f0wsUL^Bp?x z44(<2#dAJXVYMR~P0#suXe1h7B(aw>3yh8|j?M&#^QImNy&uq3;}2|8Wg#2kAV3-&JFF7}p^S4oWfC1kOPNyOQhc z-KU4G^(_l_y_^R5TPn>bD-mcs{z;c@xyWf z&=Pwi@r*WnjI?e2Eh8f%%F>y{j~};B)3F;jTVKDtl=3OS*+3*_ypzRC!!@H$2pGU7 zSuLbV(n7P_y2)1#*H$#jrhMxCPt+d0WqL!e3|=HzU#D9Ou!+d=Qy7IGW{M;_LUYkB zBD8Oh)<8WcI=FZ!QSZ2rIfCKGChy}@9Kx=!LwZ6nYNw=a|6p35JHbV&xZK4*L`9Ng zte9RDuTn9Zx_Br^-ZlUZv6<+4d2!m=bav`_PMyHm^49v^SHVbUM$g@V@;P-c0B6jW zoEr0d`;+IflD#?axTxFK=M&9{;0IrtCBUL*lTfc0iEd#>$H7I5`R%l|114q9Z9o0E zT3yeyyj+e!%^r+Y$QnE>UOw9AIh3nS(Zg=(b!J>yj5g{Fid=h@Zm44Ks4y7{0cV#e zbl8};=+BjTx>L(?`opu3v!x}yc6`rm#Ks`l$!*`M+LBbu)`7LB;!e-U{ih(Jp=*tg z?@2XZGZLB1IQJ5keP5os?`fTnpvvhk-|OzLh)ygyL#hOaGG0VIs!`2A6!%v?f0KDoS{-@|^| zxx2#29_bsuwyk}6Wkz^9WmUofYa(WHO!y~-itID0{!k0hG!(W}?35o~{dZXb?{ylW ztL@eerv0f&;r3tHBRP+gDv4SGM${+WKMtxe2zLBvpM)?sx={hn=izEbm*xT$Sn+~)0A7E$e<6@>U?UwFpMG-s#!Yr|f5&$Y?-d06oR z*EYH%Z{*s}%JoCguiGNgZYkjx)3IXH3q4!Hv)K~KrS>?Ft$pVNAD@R!V%6Uh;>a=W zNg^F&Y(B;L@On@q{2GNpk6kyq-{YkU{eHt~0w7+neeLA|X_AN<@@#7blrlxse{GFtIP|fGQx?7tvb+!KHe9K8CS;vDq;5zE zU18}y=JGgs@OXyxp`A&RR|l?oXTfsZ9Q8ojhF8fkGVX4RHZfF>vFEvPEAPq_a?rmYivwWQekwyKk5$;F~m z&qXu%tbYr=Y)g;-`j+FRe_P~8RonLdlCJ+|k0fDdo#p#?V_I!&ZJlSEgNCNnp1PeI zcv}YAPm;MB!-271x1&=jGc}#=Yt2tjEqm}O&uSRDl!xT1i#WCsjD?!>OibUt?5$q* zCUTmZ$_BE>Jh!Z_B`_YAtUBJ85U}64E!+^2zo=Kdu4C>cDm$$6WrNOL{i-6`&-w!F7{ z0U2NG%ZFPfz%4yp+mt8W7r`|)b)M#O=2bBIn!86@E^jk5oc>OOA4yg_)0=EIm@6u< zBx|4XfNdsGcy$kIcZO@!!@*n(tZt+D@tPL6X0CgI;)grji{UdG=UH%sUc;oe5ueT| z_OW`wVP##97!RA*9q->2aM}0_OdetDcKCH39mT*;bKBY@d54QN-jx8-%E`d1Mh0ax zS9ZBkZ@~kDW)nb8fdvLUJYl|AkW;+!3bklBY3y;++}biE$aWLMr=3_a@YE15*&uTT zWmzD3zW^h7xKe#n4;ci*J(z2Ks+KCMEc>Wo(+Prg4z)R|Oeo3xTndK_zXt%>4p8=g zJ(&HtVh0*%nLcL+wVv@GVpd_Xy93pYMGSP0bIR;#-dRVoqrdj*XU4^o8D!0E4W8oN zai;&h{%eI&kE313)l?uXGKZ5jLcX+Qbf%=f%e|4Aq7YU#4zxE6Xs;(KiA5jQ)p&f( zFssk)!hsS1yMTE0rfq+-*T4M;?-|qW{;3wF#hFK1p9KBR1iVzH&nRQuj51mmQBFGW zgKVFHVC@ctEEk-{A%PmzAfveO{9<1>4iBf#S~Wg;pm8cA0wI_0`n}_#;!qeMUoPMx zR&mkALw7AznC16tDgSg!kEBmBtQ_(dI0AYSg)SbukWg`iy;53f z2qk_R?Uu3EV*;XuMy!=XlI2Vw+=|g<;(*G`oFEc{#~k_*m&GL z2*BP9pq$xx1`w?8>mo(sknmOK&e08lqey_nsPSC2v(g77o|DPXbmeER|NehGb@L54 zWbbO5!b1L<2xwbMIb7MPo(cxZ@;rZzyO?q-Lped`5T6r(?zI7}2RK{a{3xIV{Zl-) z??79G);zP`Jr9yM#19t~ig1JLet9D2Aq2^au+hDHzpF>4(I1e=E0sys(zjbx4(_6SbM< z6n6diAxU~=v~Ejtt|Vtm@+Vr7?Ot}ah$FT^EZAHWf4V9FMzBdt7*7+L!(3DBjPD`X zw*WU!v$B4SPdqTf>|(Qb``k7Wz&va}kT$9n6%W+yI+vY+SwJNP)NQo~fTETOOX*q^ z1~cV<%BW++!bR)T!Y%dMh=<+dSm%J}5$DA!YQ9uSirCoN=c=Cxdc1rwis z&f=FeEWgY@cbg<7Wj|vRYhGIm=%nG_-@4jyUj$d}9avl3Jycv|0IBrFy(7rhm*=7l z7mJVP!U9G;T+E9+bj;;Xj_Sl}^OJ;HMnA0pL^@^=!J>8oboX7>&=I72RGlEuVY8V- z>8c@R&8@5gUt3<>FyHX6B^E4b$f%jw_po?{C4wBl(4MS^6V--BMiL$SB{++_&4cz1 zOjK3Cz{vI-6dq3t!$P{yOg*QLM_|rS#OqgWBrmlHdRLaTwtRVVuk+Y&M-|+(|7@XZ z!>7HrPN=$AIrZdirYazQ>_sxxv6=vApi+z?HWNDmr{IUB=elzb)EI7K%}?X!gYwha za~@H3TUws_u|0v#U`cEz9c3u;p z#Q@^oQ}0cHCwFFs%<<38&Qd;}koitMU}aX-5AOhrz)P7-S38rxo!R zfq#XlC-+inQlz4e5VH~o*I|V6o%O*$9JzrN7EXIvN0G#CO74aaWN7XtYDOWp;}G0_ zh}G(yO22SfhYaR?IP1tI*7*qEc1bz*?!eCKHbRp%<;d{b4M&S0kcE+ed4;>M_}c+6 zf^L_G5KajCMXS_M3o{mWq7o_7SgOoI2eD04gE2YC6QDB7A|UpW6k{SH-cM%r6|aMu zMW68M#}{s$J(d{$342NmrQ0Z?I!u0ZYp zi4VMLCxl=raXX@j-7s&=F51c0c^VTQVz-m1xu{R#2ULOx_6Jd18wFwJ+Pa(fs^a&@BH+&Zy_I-gT>O~kw1ycm`ELep9t4naa8+T*($3PTBi$WL*#m%O|F7Qb|F{kt zcpXgBC|!=9v}KrBievB`U!~9v@9u^p=CN}va~C@(()-Ju#RTg3C+rNhvbs#=cSS+| z-5QiTH&va_y1$tNp4_1#pY0!tKyJ?sE>F=xAM}T0p{rl3#Qd~0>;mG$hO4nn%n-mN zLhj5sT77eFU@d-WX--Ba#NR2)Bs^p_OZJL(?9@Mqh8$26)tnA3zOzCC9 zeg$2=*YRy=Y&J8*PPiQV*uyq3DCn$d2MO+k_*4>99YC8G&I z!BCc!jo%9|5T5 zksMd1kRC@yCK%H^Drs1NFi>GFq=&7< z3*}BSZ3f-x9*oQNK7R!n6%Imgf!FCn{immu&YpWmVUQhK#nQ3T5us$QBpekEaQ{ta zR#uC8a{ju0)!f?l*dw`}D&ffxUq4i4dIf_Igl;%69L7q`^4_oAn+ul&tcl38rrYL# zxFC{*-jYr*%m?`X&C@vzmzqu6&u11Jx+zSo$93q2MRRb(dd%MWn-Oog-ANB;-hMG9 z>?K)=$DEgR4bnBTap@qs4O&~CdFGWplD}nLzzF=Vu`{%^>sV>oMOxa;o^%#13D@Aw z*w5ZII3nobide(fUZYip)f!rP-5g=m#s5KHenrwaob7LoK#G-yV*hsBe8!Lewyrex zW^85V%|{Raw7LBz;i}xdO#bG%g7m!R7MpS3&&g768H<5v=TckLcZEnxHp-3sUcj$w}o8qWGI};;j}*7PjUBY z&mj$cZ4LAja({cu#-=9s%N)w}Y*1|Dn%QTGff8|#U8mX-7qS-RfEBB6nR&=p;y**P z@gK}B?axwSs??t*$3XbQp#6_{hKbPbyrlo?PE@$^=5Q(H1cFXmYTjVSDl?u_hP)>O zd8u1755U{j(sAZ$B0bS>IXz+|G$#~XkWULj0UV@>MKi)oeKnS{G5MC_?l=1av z^+m{4(teH_k7#gr;h_q&Z@ z$U|0jnlL{vlSB0YnLpG2V3Yp!J{IYPjs4N2)!?J-Y#UEl`Q2SY-6&-W3>zq9!6jVN!dajgAYueRX7$5b7G# zV#jFgcAsBoA(44UN`q#X5wIT@ew&b;#V({MWzX>-{KoKqsG^I~cIc@}Oq*t9n-~v} zxbfbhJD03azKjxDaWCo0Tlre$OtHmo+YSLB3gP%9ur6cNA6$1%v{;3sa4>2$KtJSy z5q+s>u23xKyph(*$WT!zkfXL(F#Qxa2mWQ%qi2idcb5U@{3sM8e=|6v7a1b^^q`WF ztZ+0%N%t(E=l{`uIQ8_eTz8FS{^3r*p(nNCIAq06mQ!xK6(_Oz%2D{fR+HS@5q~P z?Z3KSx44VT+5(A3S5EO z1y8zAb_Wrd)R7N-&vaF<77+Xe1~eF}`&ONOj{raeC6za94z?usQCfISTU;2utI!3&QTiWvZfXMk(k%N72rPVZ8;pXjQyr|ZuV{3Nz2F-l?8KK8SZ z^=Mvl0(op7>xi*B|E15=PP5K-U6EL?N*f3LxZ%cn#ABeu1wtnTR>nW);G%^{IaWD! zZqPSU|5Qh+3;Ayo7XmDGA^Tr17>M+)$Iii(bi4qMKqY;E&zXV^jp4ynbBGon`;CK; zz)=}jwHQBwVO|6ljmNt%aS0|&3$U>|dBF^l5D~#eQwpS$Sr=8IdS#d&b4BdL8E*Pq zwJO?9^2mJ?)xafV1aVX0Ex+n8MRHKsvR;Rj6uMcYGQe4Wr6_YC9#&e4TTFgRaO{_Z zZMV@+MMPv0LOq8S9<2&(hI#BRwBP6tiWKqAu7vkLEwAJw`00_7{I{!t`&v27AsWR| z*;eug--<>|QC4vz{hpkI9k(^VkduzR>4G;d>M!xxWNMEd%Ffiq66@u=7 z1%L5IC|6uXxwvrQ)CtP&@x7aB&UkZOC&HC^pgJAr|GfmB;CJnV*)Y}Wq3-18$GQdY zGY!Dz%l;c)Rdr-c?lB)f|9w5&YWYyGqsyH7WV1*gui=X5plKqSqqKlg_6T3+{cikX zWt4U6Wl_;lJ9_(+_yg;AjhAdKF#|ts&@Po`US(M6a{nJ=y}6{Op8cn;7I9gy%gsFa z#D2{>oxFLPXj6%mC7FhdaI`o%`*MC-fbAczYWxXDZx0s z5v;T}=NC>}j(QgQ>UQ0rHVG#@Fh zy)}H{*0U){=n~h$Ezu5rSWeiJmLmDNnwu|A8!v|p2`KrtDvGq`D6=%)4Ak)MrJpLW zc~k#2)sb6W%@wcoe!r_R?Edai92M9eV8K_Zc>3;$Jy$TN0>2HbsZqLK2NpKaB{rnE zBJjM5U7zn`tyr`>%omZ;Rm*gq2I3Q<8<1#L=F$%Gu>V>Uk;fFoz22{c7ot>rU$zPa zHbf47ZAlT;jDXBVFv!ZJvYKcW5cg%ZF2DxhA(zk{Uw0xatR%HaM!4mm&hJ!mt2pZ3 zfj>uEMpUi%?RyzVAiY4@416}=hYt5|p7znml7cR#1&)`cmKYNb>4z16U;?f?9#eUv zuytUgiZmKD&>DFDa^@L{>Gnz=VRxrbjrv;{{cN_o(Qk?;MS#70m%Bhc2?LAB3t)Ma zIxM(#d)(GQTGKwCj+zSyTL&d}1HO$-!pJAVa6hD=NUP$So>IT0h>JqcY|1h zH3<4$^-o_bW1AK;vLL&Y%XPjQfK8^}Rls=B3>I5VcI9Qf41c7*g*u&12i( z3v_|__`fVPM?UZaIsH&wmoN~wQx^eX@TAlR(6kbt32*@$xV+utaI2G5-(Yx2@pmfE zFG8LpI=DjJFQC+J!;DuLqww6+CsFs-d))uFjhudtW6aSp zw?nSzhlrQWWq!%L;+8z&0F(Zye9M{cioD+W=hdfwh1ZN-?)n#?mtV?OdINkFaKX%q zDh!tomCAJW&40ML0K5JlFWG#VpQ`w(Ac~S(4 zL!VG`Vaq`N@j?_081G=w*ADBQMq=a3fus_e)D+{gIpp;u58DVOru@$t|esqq~Q~b{ah!Y z`I?I)u{Ff}3+OpcqpMNGJ#1s|LX43G5h0h=h7m7&h?IuW_WUEduk=64CglcIv0pH& zeDAqbMg6!gpJZQ^w^Pyj2w`VQZNRd#_Y+a;v4c=g&yA*}F#Y?RX}S0#_>l*rZ%5X! zS4H^D3}G6*SU6 z8rjDaYpbRG`6H{u;~;&wWw=CzpqZiV84?ZqyIM7#1)K;ws_4v(#(ojzo}XYTU|20fOz%_(PCYGU^F~3Jh|}BEZia7uyDWs6t!u) zGy>E4FmEv63&rUhRJNI{GFFUB4EO!9i{vU(Q{UQO3+!`m#qhhJ;JlB6ChS{$lH_;C zf-*N1T}yyQ)T>tX^?GP&^qQh|ry@OeSF2VIKi5c{s=itOa2AXIScJLs+<-{GgU;g+8)kCvsxF5~Xm-Y)l0+xh;c z(aYGTqUY2@y5pIU@E})F)}_LEjfs!c!Oj|>bd9wTdc;%SqQbaQk_2asaYS0o(?aZw z+y=Wmt;;GhP!CG(;&Y}%F(lDfTC;_$C$dik)~H7jHT`@ZvyNAegX>6HugLr~=(R1d zS3JLSC}N~PaR2IPg<4%nv`hGPBX4dX0V;ARy z&mk==K0rB;TZ;d0cZNl&AQoBosh^jor(Y08@USdKyil5y4G=?w#fopaI#Gq$}=xj|_uE$Xx zW}t^c=Fbe`0tR{V>Z&8;HCYO#7kA+%~!Y(tqaLtwZH7f)aM*m-)ivb3_CzS(r z%_K{+@Nmn3Y)2p5_C73Ou-y5cR5531G1Eh?M$`0QcKqq-Wn2Sqre?DaBT6 zt|3tZ1zSz0EG^u3=xTj#lYR7uC{bQlC~*^Tm((?rKK5<~Cmy!cC@MbQJ52yLzP0za z$h?y~Pqaxwb(>G=qJQ{CkI6L_kI9;FTF#tKDG0)k3iECwRxLjjR!Cyxcdryly`F=- zXPv-i4*1K~aWZ3T){WZtW@1!%lIkk_itf1E&wn2HQ2i{;|ZYH!V45|rO^T-xWm_wi0$}xIt!>b=ak7t zJmZib{YHX1vOfg*BTQ2k4uf2ey1W3Y-)F^43Jm`vWgX+*td&oo3FcgIxW8 zvl;lm)|~z?_74C1@c(XC^}l=O-#zpHx)cB3bLQW3=HGMXKU_sWY2p9Rb1go714<@k zPA7rQZ6DmLKfY>r1|BowzYlmEfZcw7ZpOZ@zFDJVw|QW-3&F!1QUPbuD~MQMrdmU) zfVHVXxyhSQKubQ@eEH#I%C#FkNArrww>NdH;c)mRYVm{o4gUjgSn#S<#=*`)fJwS0 zdluX}@P0MDD?WG7h5v<+`n+`Rwne*uUCmW==a literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3f3d567 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,20 @@ +# Übersicht + +!!! info + + Einige Knöpfe sind auf Englisch. Diese Einstellung kann ich leider nicht ändern, da diese Übersetzung von der Anwendung automatisch ausgeführt werden + + +## [Hauptoberfläche](Ausleihsystem.md) +Die Hauptoberfläche wird angezeigt, wenn die Anwendung gestartet wird. +Von hier aus können Nutzer angelegt und bearbeitet werden, Medien ausgeliehen und zurückgegeben werden, sowie verschiedene Unterbereiche anzeigen. + +Unterbereiche umfassen: + +- [Nutzerdaten](Nutzeroberfläche.md) - Daten für den aktuellen Nutzer anzeigen und bearbeiten +- [Nutzer anlegen](Nutzer anlegen.md) - Neuen Nutzer anlegen +- [Ausleihhistorie](Ausleihhistorie.md) - Historie der Ausleihen +- [Bericht erstellen](Bericht erstellen.md) - Bericht für einen festgelegten Zeitrahmen erstellen +- [Einstellungen](Einstellungen.md) + +## Navigation diff --git a/docs/shortcuts.md b/docs/shortcuts.md new file mode 100644 index 0000000..9368ebe --- /dev/null +++ b/docs/shortcuts.md @@ -0,0 +1,16 @@ +# Shortcuts + +!!! info + + Die Shortcuts können für die Anwendung manuell festgelegt werden. + Dafür in den [Einstellungen](Einstellungen.md) unter Shortcuts den neuen Shortcut eingeben und speichern + +Standardmäßig sind die Shortcuts wie folgt festgelegt: + +| Shortcut | Standard | +| ------- | -------- | +| Ausleihhistorie | F7 | +| Bericht erstellen | F6 | +| Hilfe | F1 | +| Nutzer | F5 | +| Rückgabemodus | F8 | diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..dd4026f --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,5 @@ +.md-typeset .admonition, +.md-typeset details { + border-width: 0; + border-left-width: 4px; +} \ No newline at end of file diff --git a/icons/icons.yaml b/icons/icons.yaml index 1dc2e7c..2c9a3d1 100644 --- a/icons/icons.yaml +++ b/icons/icons.yaml @@ -15,3 +15,4 @@ icons: user: user.svg warning: warning.svg delete: delete.svg + restart: restart.svg diff --git a/icons/restart.svg b/icons/restart.svg new file mode 100644 index 0000000..8fa96d4 --- /dev/null +++ b/icons/restart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..52fbb20 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,30 @@ +site_name: LibrarySystem +theme: + features: + - search.suggest + - search.highlight + name: material + icon: + admonition: + note: fontawesome/solid/note-sticky + abstract: fontawesome/solid/book + info: fontawesome/solid/circle-info + tip: fontawesome/solid/bullhorn + success: fontawesome/solid/check + question: fontawesome/solid/circle-question + warning: fontawesome/solid/triangle-exclamation + failure: fontawesome/solid/bomb + danger: fontawesome/solid/skull + bug: fontawesome/solid/robot + example: fontawesome/solid/flask + quote: fontawesome/solid/quote-left + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - tables +extra_css: + - stylesheets/extra.css +plugins: + - search \ No newline at end of file diff --git a/src/logic/documentation_thread.py b/src/logic/documentation_thread.py new file mode 100644 index 0000000..ed2c7cc --- /dev/null +++ b/src/logic/documentation_thread.py @@ -0,0 +1,12 @@ +from PyQt6.QtCore import QThread, pyqtSignal +from src.utils import launch_documentation + +class DocumentationThread(QThread): + def __init__(self): + super().__init__() + + def run(self): + launch_documentation() + + + diff --git a/src/ui/sources/Ui_dialog_generateReport.py b/src/ui/sources/Ui_dialog_generateReport.py index 76022fe..ecc84da 100644 --- a/src/ui/sources/Ui_dialog_generateReport.py +++ b/src/ui/sources/Ui_dialog_generateReport.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(375, 247) + Dialog.resize(375, 245) Dialog.setMinimumSize(QtCore.QSize(40, 0)) self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) self.verticalLayout.setObjectName("verticalLayout") diff --git a/src/ui/sources/Ui_main_UserInterface.py b/src/ui/sources/Ui_main_UserInterface.py index c5577ac..e5f8621 100644 --- a/src/ui/sources/Ui_main_UserInterface.py +++ b/src/ui/sources/Ui_main_UserInterface.py @@ -142,6 +142,9 @@ class Ui_MainWindow(object): self.menuHotkeys.setObjectName("menuHotkeys") self.menuFenster = QtWidgets.QMenu(parent=self.menubar) self.menuFenster.setObjectName("menuFenster") + self.menuHilfe = QtWidgets.QMenu(parent=self.menubar) + self.menuHilfe.setGeometry(QtCore.QRect(2484, 209, 181, 94)) + self.menuHilfe.setObjectName("menuHilfe") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(parent=MainWindow) self.statusbar.setObjectName("statusbar") @@ -158,15 +161,22 @@ class Ui_MainWindow(object): self.actionAusleihhistorie.setObjectName("actionAusleihhistorie") self.actionBericht_erstellen = QtGui.QAction(parent=MainWindow) self.actionBericht_erstellen.setObjectName("actionBericht_erstellen") + self.actionDokumentation_ffnen = QtGui.QAction(parent=MainWindow) + self.actionDokumentation_ffnen.setObjectName("actionDokumentation_ffnen") + self.actionProblem_melden = QtGui.QAction(parent=MainWindow) + self.actionProblem_melden.setObjectName("actionProblem_melden") self.menuDatei.addAction(self.actionEinstellungen) self.menuDatei.addAction(self.actionBeenden) self.menuHotkeys.addAction(self.actionRueckgabemodus) self.menuFenster.addAction(self.actionNutzer) self.menuFenster.addAction(self.actionAusleihhistorie) self.menuFenster.addAction(self.actionBericht_erstellen) + self.menuHilfe.addAction(self.actionDokumentation_ffnen) + self.menuHilfe.addAction(self.actionProblem_melden) self.menubar.addAction(self.menuDatei.menuAction()) self.menubar.addAction(self.menuHotkeys.menuAction()) self.menubar.addAction(self.menuFenster.menuAction()) + self.menubar.addAction(self.menuHilfe.menuAction()) self.retranslateUi(MainWindow) self.actionBeenden.triggered.connect(MainWindow.close) # type: ignore @@ -198,6 +208,7 @@ class Ui_MainWindow(object): self.menuDatei.setTitle(_translate("MainWindow", "Datei")) self.menuHotkeys.setTitle(_translate("MainWindow", "Hotkeys")) self.menuFenster.setTitle(_translate("MainWindow", "Fenster")) + self.menuHilfe.setTitle(_translate("MainWindow", "Hilfe")) self.actionEinstellungen.setText(_translate("MainWindow", "Einstellungen")) self.actionBeenden.setText(_translate("MainWindow", "Beenden")) self.actionRueckgabemodus.setText(_translate("MainWindow", "Rückgabemodus")) @@ -208,3 +219,6 @@ class Ui_MainWindow(object): self.actionAusleihhistorie.setShortcut(_translate("MainWindow", "F8")) self.actionBericht_erstellen.setText(_translate("MainWindow", "Bericht erstellen")) self.actionBericht_erstellen.setShortcut(_translate("MainWindow", "F7")) + self.actionDokumentation_ffnen.setText(_translate("MainWindow", "Dokumentation öffnen")) + self.actionDokumentation_ffnen.setShortcut(_translate("MainWindow", "F1")) + self.actionProblem_melden.setText(_translate("MainWindow", "Problem melden")) diff --git a/src/ui/sources/dialog_generateReport.ui b/src/ui/sources/dialog_generateReport.ui index 1700beb..e478a02 100644 --- a/src/ui/sources/dialog_generateReport.ui +++ b/src/ui/sources/dialog_generateReport.ui @@ -7,7 +7,7 @@ 0 0 375 - 247 + 245 diff --git a/src/ui/sources/main_UserInterface.ui b/src/ui/sources/main_UserInterface.ui index e7e7621..46614e8 100644 --- a/src/ui/sources/main_UserInterface.ui +++ b/src/ui/sources/main_UserInterface.ui @@ -278,9 +278,25 @@ + + + + 2484 + 209 + 181 + 94 + + + + Hilfe + + + + + @@ -309,11 +325,6 @@ F6 - - - Nutzer - - Ausleihhistorie @@ -330,9 +341,17 @@ F7 - + - Nutzer + Dokumentation öffnen + + + F1 + + + + + Problem melden diff --git a/src/utils/__init__.py b/src/utils/__init__.py index ea00422..d004bfe 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,4 +1,5 @@ from .log import Log from .icon import Icon from .debug import debugMessage -from .stringtodate import stringToDate \ No newline at end of file +from .stringtodate import stringToDate +from .documentation import launch_documentation \ No newline at end of file diff --git a/src/utils/documentation.py b/src/utils/documentation.py new file mode 100644 index 0000000..0b5bf8e --- /dev/null +++ b/src/utils/documentation.py @@ -0,0 +1,21 @@ +from pyramid.config import Configurator +from wsgiref.simple_server import make_server +import os +def website(): + config = Configurator() + + # Set up static file serving from the 'site/' directory + config.add_static_view(name='/', path=os.path.join(os.getcwd(), 'site'), cache_max_age=3600) + + app = config.make_wsgi_app() + return app + +def launch_documentation(): + app = website() + server = make_server('localhost', 6543, app) + print("Serving MkDocs documentation on http://0.0.0.0:6543") + server.serve_forever() + +if __name__ == '__main__': + pass + \ No newline at end of file From f4bc3de3572e6d1d05d0d0c8c3bde8acb7d11375 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:11:13 +0200 Subject: [PATCH 68/83] add requirements, update documentation --- config/config.py | 12 +++++++++-- docs/Ausleihsystem.md | 34 ++++++++++++++++++++++++++++++- docs/MultiUser.md | 5 +++++ docs/Nutzeroberfläche.md | 4 ++-- docs/images/activeLoan.png | Bin 0 -> 15756 bytes docs/images/err_noBook.png | Bin 0 -> 4485 bytes docs/images/err_return.png | Bin 0 -> 5269 bytes docs/images/main_loan_active.png | Bin 0 -> 7547 bytes docs/images/multiUser.png | Bin 0 -> 9960 bytes docs/index.md | 5 +++-- requirements.txt | Bin 1834 -> 3222 bytes 11 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 docs/MultiUser.md create mode 100644 docs/images/activeLoan.png create mode 100644 docs/images/err_noBook.png create mode 100644 docs/images/err_return.png create mode 100644 docs/images/main_loan_active.png create mode 100644 docs/images/multiUser.png diff --git a/config/config.py b/config/config.py index 8741d83..59133e3 100644 --- a/config/config.py +++ b/config/config.py @@ -132,7 +132,8 @@ class Config: if self._config is None: raise RuntimeError("Configuration not loaded") omegaconf.OmegaConf.save(self._config, "config/settings.yaml") - def apply_options(options:list): + + def apply_options(self, options:list): if self._config is None: raise RuntimeError("Configuration not loaded") for option in options: @@ -148,7 +149,14 @@ class Config: def updateValue(self, key:str, value): if self._config is None: raise RuntimeError("Configuration not loaded") - self._config[key] = value + if "." in key: + keys = key.split(".") + if keys[0] in self._config: + self._config[keys[0]][keys[1]] = value + else: + raise KeyError(f"Option {keys[0]} not found in configuration") + else: + self._config[key] = value if __name__ == "__main__": cfg = Config("config/settings.yaml") #print(cfg.database.path) diff --git a/docs/Ausleihsystem.md b/docs/Ausleihsystem.md index 08246b8..d42b07b 100644 --- a/docs/Ausleihsystem.md +++ b/docs/Ausleihsystem.md @@ -19,7 +19,7 @@ Der Bereich beschränkt sich auf folgende Inhalte: Hier werden folgende Daten angezeigt -- Modus: entweder "Ausleihe" oder "Rückgabe" (1) +- Modus: entweder "Ausleihe" oder "Rückgabe" (1) Kann entweder durch anklicken des Knopfes oder mit dem Shortcut (Standard: `F5`) geändert werden - Matrikelnummer: die Matrikelnummer des Nutzers, um das Konto zu öffnen (3) - Benutzername: der Benutzername des Nutzers (3) - Signatur: die Signatur des Mediums, welches entliehen oder zurückgegeben wird (4) @@ -43,5 +43,37 @@ Dieser Bereich beschränkt sich auf folgende Inhalte: ### Historie (7) Das Feld der Historie listet alle Medien auf, die im aktiven Prozess ausgeliehen oder zurückgegeben wurden. +## Ausleihen + +Um ein Medium auszuleihen, muss zuerst ein Nutzer geöffnet sein. Dazu entweder den Knopf neben Modus drücken, oder den Shortcut (Standard: `F5`) verwenden. +Die Oberfläche sieht dann wie folgt aus: + +![Ausleihe](images/main_loan_active.png) +Der Cursor wird automatisch auf die Matrikelnummer gesetzt. Hier kann entweder die vollständige Nummer, oder ein Teil eingegeben werden. Sollten mehrere Nummern dem Teilfilter ensprechen, wird eine Auswahl angezeigt. (s. [MultiUser](MultiUser.md)) +Nach der Auswahl wird das entsprechende Konto geöffnet und die Ausleihe kann durchgeführt werden. + +![Ausleihe](images/activeLoan.png) + + +Sollte der aktive Nutzer aktive Ausleihen haben, so wird die Anzahl der Medien neben "Anzahl Ausleihen" angezeigt. Das nächste Rückgabedatum wird ebenfalls angezeigt. Über einen Klick auf die Zahl der Ausleihen gelangen Sie zur [Übersicht der Ausleihen des Nutzers](Nutzeroberfläche.md). + +Um ein Medium auszuleihen, muss die Signatur in die entsprechende Zeile eingegeben werden. Sollte die Signatur nicht in der Datenbank existieren, wird der definierte Katalog geprüft. Wird das Medium gefunden, wird es in die Datenbank übernommen und die Ausleihe wird durchgeführt. Sollte das Medium nicht gefunden werden, wird eine Fehlermeldung angezeigt. + +![FehlerAusleihe](images/err_noBook.png) + +!!! info + + Die Signatur muss mindestens ein Leerzeichen enthalten, ansonsten wird eine Fehlermeldung angezeigt. + +## Rückgaben + +!!! info + Rückgaben funktionieren nur, wenn kein Nutzer offen ist. Vor einer Rückgabe bitte sicherstellen, dass der Modus auf "Rückgabe" gesetzt ist. + +Um ein Medium zurückzugeben, muss die Signatur in die entsprechende Zeile eingegeben werden. Sollte die Signatur nicht existieren, wird eine Fehlermeldung angezeigt. + +![FehlerRückgabe](images/err_return.png) + +Existiert das Medium in der Datenbank und ist entliehen, wird die Rückgabe durchgeführt. Die Rückgabe wird in der Historie gespeichert und das Medium wird wieder als verfügbar markiert. Die Daten des Nutzers werden in der Oberfläche angezeigt. diff --git a/docs/MultiUser.md b/docs/MultiUser.md new file mode 100644 index 0000000..99be7a8 --- /dev/null +++ b/docs/MultiUser.md @@ -0,0 +1,5 @@ +# Mehrere Nutzer gefunden +![Mehrere Nutzer gefunden](images/multiUser.png) + +!!! info + Es wurden mehrere Nutzer gefunden, die auf die Suchanfrage passen. Bitte wählen Sie einen Nutzer aus der Liste aus, und bestätigen Sie mit `OK`. \ No newline at end of file diff --git a/docs/Nutzeroberfläche.md b/docs/Nutzeroberfläche.md index a3f1564..dc13a15 100644 --- a/docs/Nutzeroberfläche.md +++ b/docs/Nutzeroberfläche.md @@ -2,7 +2,7 @@ ![alt text](images/user_main.png) !!! info - Die Nutzeroberfläche kann nur geöffnet werden, wenn ein Nutzer offen ist. Ansonsten wird ein Fehler angezeigt. (s. unten) + Die Nutzeroberfläche kann nur geöffnet werden, wenn ein Nutzer offen ist. Ansonsten wird ein Fehler angezeigt. (s. unten) ![Error](images/err_nouser.png) @@ -14,7 +14,7 @@ - (2) Nutzer Löschen: - Mit dem Klick auf den Mülleimer wird der Nutzer gelöscht. Alle zugewiesenen Ausleihen werden in der [Ausleihhistorie](Ausleihhistorie.md) mit den Nutzerkonto `gelöscht` angezeigt. + Mit dem Klick auf den Mülleimer wird der Nutzer gelöscht. Alle zugewiesenen Ausleihen werden in der [Ausleihhistorie](Ausleihhistorie.md) mit den Nutzerkonto `gelöscht` angezeigt. Diese Option ist nur vorhanden, wenn keine Ausleihen aktiv sind. - (4) Medien: diff --git a/docs/images/activeLoan.png b/docs/images/activeLoan.png new file mode 100644 index 0000000000000000000000000000000000000000..83a6e1e8acf805dffab1ce3bc084a23ff0602909 GIT binary patch literal 15756 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 literal 0 HcmV?d00001 diff --git a/docs/images/err_noBook.png b/docs/images/err_noBook.png new file mode 100644 index 0000000000000000000000000000000000000000..4a656f5cf88a04107a986cbce9a3b3fdae5164d8 GIT binary patch literal 4485 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 literal 0 HcmV?d00001 diff --git a/docs/images/err_return.png b/docs/images/err_return.png new file mode 100644 index 0000000000000000000000000000000000000000..05201bd186bf9229db0c6a351f9ad8918c5c7d67 GIT binary patch literal 5269 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 E7iA!(Bme*a literal 0 HcmV?d00001 diff --git a/docs/images/main_loan_active.png b/docs/images/main_loan_active.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd0009a53b34370ad02f368d50023088fe20b58 GIT binary patch literal 7547 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 literal 0 HcmV?d00001 diff --git a/docs/images/multiUser.png b/docs/images/multiUser.png new file mode 100644 index 0000000000000000000000000000000000000000..612d82247281f32708e6af43db28f835f28da550 GIT binary patch literal 9960 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 Zdhg>q+G`$cFrVqy!s9*+7Tc=N|6gQ>*kb?y delta 260 zcmbOxxr%SXu8B8X*o_$U7>pQ9Co3{$E1Cn@CJej`Tnxz!X+W6CU<-tXKv6>mGazX& zc|D`;WDX{U$$m^GtOg+Q$?Z&)lhv5Tz&zv0iOlAV=98IOePvU@`g0gk!DbjS7yxZC z2I>IWXFmBZtM23;HaV~^qsi;pG8ru=e`J%NY{jkx)IXnHnbBzSVRm~UxtT+I@;?qK zAWM={7f8BungYq4VD@uP*~vj%E{ukgr*mloMGtdXGMY}-<(3DsCvvM#{={uCIe^DP V4CHnbATD6YXDDVU0lER?M*x6|IdA{~ From 2836be7490b706be31acb650e3d9a00e07695d6f Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:28:12 +0200 Subject: [PATCH 69/83] add updates --- config/config.py | 1 - src/__init__.py | 19 ++--- src/logic/database.py | 34 +++++--- src/logic/documentation_thread.py | 5 +- src/ui/main_ui.py | 127 +++++++++++++++++++++++++++--- src/ui/multiUserInfo.py | 4 +- 6 files changed, 149 insertions(+), 41 deletions(-) diff --git a/config/config.py b/config/config.py index 59133e3..d6b37a7 100644 --- a/config/config.py +++ b/config/config.py @@ -1,7 +1,6 @@ from typing import Optional import omegaconf - class Config: _config: Optional[omegaconf.DictConfig] = None diff --git a/src/__init__.py b/src/__init__.py index 89fbd17..3fed036 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,18 +1,12 @@ -# import argparse - - import sys from config import Config# -import argparse +__version__ = "0.1.0" +__author__ = "Alexander Kirchner" +__email__ = "alexander.kirchner@ph-freiburg.de" +__license__ = "MIT" + config = Config("config/settings.yaml") -__version__ = "0.1.0" -# if programm launched with argument --debug, set debug to True -# if "--debug" in sys.argv: -# config.debug = True -# # if programm launched with argument --log, set log_debug -# if "--log" in sys.argv: -# config.log_debug = True valid_args = ["--debug", "--log", "--no-backup", "--ic-logging", "--version", "-h"] args_description = { @@ -27,7 +21,8 @@ args_description = { args = sys.argv[1:] if any(arg not in valid_args for arg in args): print("Invalid argument present") - sys.exit() + #sys.exit() + def help(): print("Ausleihsystem") print("Ein Ausleihsystem für Handbibliotheken") diff --git a/src/logic/database.py b/src/logic/database.py index 2593398..16beeaa 100644 --- a/src/logic/database.py +++ b/src/logic/database.py @@ -168,7 +168,9 @@ class Database: conn = self.connect() cursor = conn.cursor() cursor.execute(query) - + result = cursor.fetchall() + self.close_connection(conn) + return result def checkUserExists(self, key, value) -> list[User] | bool: query = f"SELECT * FROM users WHERE {key} like '%{value}%'" conn = self.connect() @@ -208,19 +210,28 @@ class Database: return user def getUser(self, user_id) -> User: + conn = self.connect() cursor = conn.cursor() cursor.execute(f"SELECT * FROM users") result = cursor.fetchall() self.close_connection(conn) - - for res in result: - if res[1] == user_id: - user = User(userid=res[1], username=res[2], email=res[3], id=res[0]) - dbg(f"Returning User {user}") - log.info(f"Returning User {user}") - return user - return User(userid="gelöscht", username="gelöscht", email="gelöscht", id="gelöscht") + if len(str(user_id)) == 1: + for res in result: + if res[0] == user_id: + user = User(userid=res[1], username=res[2], email=res[3], id=res[0]) + dbg(f"Returning User {user}") + log.info(f"Returning User {user}") + return user + else: + for res in result: + if res[1] == user_id: + user = User(userid=res[1], username=res[2], email=res[3], id=res[0]) + dbg(f"Returning User {user}") + log.info(f"Returning User {user}") + return user + raise ValueError(f"User {user_id} not found") + #return User(userid="gelöscht", username="gelöscht", email="gelöscht", id="gelöscht") # user = User(userid=result[1], username=result[2], email=result[3],id = result[0]) # return user @@ -287,6 +298,11 @@ class Database: log.info(f"Returning Active Loans {result}") return str(len(result)) + def getMediaList(self): + query = "SELECT signature FROM media" + result = self.query(query) + + return [res[0] for res in result] def getAllLoans(self): loan_data = [] query = "SELECT * FROM loans" diff --git a/src/logic/documentation_thread.py b/src/logic/documentation_thread.py index ed2c7cc..0ebd344 100644 --- a/src/logic/documentation_thread.py +++ b/src/logic/documentation_thread.py @@ -6,7 +6,4 @@ class DocumentationThread(QThread): super().__init__() def run(self): - launch_documentation() - - - + launch_documentation() \ No newline at end of file diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index cbdbffd..e6f533d 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -1,7 +1,9 @@ +import os import sys import atexit import datetime -from src import config +import webbrowser +from src import config, __email__ from src.logic import Database, Catalogue, Backup from src.utils import stringToDate, Icon, Log from src.utils import debugMessage as dbg @@ -16,14 +18,18 @@ from .settings import Settings from .newBook import NewBook from .loans import LoanWindow from .reportUi import ReportUi - -from PyQt6 import QtCore, QtWidgets - +from src.utils import launch_documentation +from PyQt6 import QtCore, QtWidgets, QtGui +from omegaconf import OmegaConf +from src.logic.documentation_thread import DocumentationThread backup = Backup() cat = Catalogue() log = Log("main") dbg(backup=config.database.do_backup, catalogue=config.catalogue) +def getShortcut(shortcuts, name): + shortcut = [cut for cut in shortcuts if cut["name"] == name][0] + return shortcut["current"] class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self): @@ -38,13 +44,21 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.actionRueckgabemodus.triggered.connect(self.changeMode) self.actionNutzer.triggered.connect(self.showUser) self.actionEinstellungen.triggered.connect(self.showSettings) - self.actionAusleihistorie.triggered.connect(self.showLoanHistory) + self.actionAusleihhistorie.triggered.connect(self.showLoanHistory) self.actionBericht_erstellen.triggered.connect(self.generateReport) + self.actionDokumentation_ffnen.triggered.connect(self.openDocumentation) + self.actionBeenden.triggered.connect(self.shutdown) + def __mail(): + webbrowser.open(f"mailto:{__email__}") + self.actionProblem_melden.triggered.connect(__mail) + #if close button is pressed call shutdown + self.closeEvent = self.shutdown # Buttons self.btn_show_lentmedia.clicked.connect(self.showUser) self.btn_createNewUser.clicked.connect(self.createUser) self.btn_createNewUser.setText("") self.btn_createNewUser.setIcon(Icon("add_user").overwriteColor("#1E90FF")) + self.mode.clicked.connect(self.changeMode) # LineEdits self.input_userno.returnPressed.connect( @@ -67,13 +81,15 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): QtWidgets.QHeaderView.ResizeMode.Stretch ) self.input_file_ident.setFocus() - # self.userdata.textChanged.connect(lambda: self.mode.setText("Ausleihe")) + self.assignShortcuts() # variables self.db = Database() self.currentDate = QtCore.QDate.currentDate() loanDate = self.currentDate.addDays(config.loan_duration) self.activeUser = None self.activeState = "Rückgabe" + self.docu = DocumentationThread() + # self.docu.start() self.duedate.setDate(loanDate) # functions @@ -84,6 +100,24 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): else: log.warning("Backup disabled") self.show() + + def shutdown(self, *args): + #kill documentation thread + log.info("Shutting down") + self.docu.terminate() + sys.exit() + + + def assignShortcuts(self): + shortcuts = config.shortcuts + shortcuts = OmegaConf.to_container(shortcuts) + #convert to dictconfig + + self.actionDokumentation_ffnen.setShortcut(getShortcut(shortcuts, "Hilfe")) + self.actionAusleihhistorie.setShortcut(getShortcut(shortcuts, "Ausleihhistorie")) + self.actionBericht_erstellen.setShortcut(getShortcut(shortcuts, "Bericht_erstellen")) + self.actionNutzer.setShortcut(getShortcut(shortcuts, "Nutzer")) + self.actionRueckgabemodus.setShortcut(getShortcut(shortcuts, "Rueckgabemodus")) def generateReport(self): log.info("Generating Report") @@ -106,15 +140,53 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): log.info("Showing Settings") settings = Settings() settings.exec() + result = settings.result() + if result == 1: + #dialog to ask if program should be restarted + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Einstellungen geändert") + dialog.setIcon(QtWidgets.QMessageBox.Icon.Information) + dialog.setText("Einstellungen wurden geändert\nProgramm neu starten?") + dialog.setStandardButtons( + QtWidgets.QMessageBox.StandardButton.Yes + | QtWidgets.QMessageBox.StandardButton.No + ) + dialog.setDefaultButton(QtWidgets.QMessageBox.StandardButton.No) + #translate buttons + yes = dialog.button(QtWidgets.QMessageBox.StandardButton.Yes) + yes.setText("Ja") + no = dialog.button(QtWidgets.QMessageBox.StandardButton.No) + no.setText("Nein") + dialog.exec() + result = dialog.result() + if result == QtWidgets.QMessageBox.StandardButton.Yes: + self.restart() # reload settings #print(config) + def openDocumentation(self): + log.info("Opening Documentation") + webbrowser.open("http://localhost:6543") + + + + + def restart(self): + #log restart + dbg("Restarting Program") + import os + python_executable = sys.executable + args = sys.argv[:] + args.insert(0, sys.executable) + os.execvp(python_executable, args) + + def changeMode(self): log.info("Changing Mode") dbg(f"Current mode: {self.activeState}") self.input_username.clear() stayReturn = False - if self.userdata.toPlainText() != "": + if config.advanced_refresh and self.userdata.toPlainText() != "": stayReturn = True self.userdata.clear() self.input_userno.clear() @@ -138,9 +210,10 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_username.setEnabled(True) self.input_userno.setEnabled(True) self.duedate.setEnabled(True) + self.input_username.setPlaceholderText("") + self.input_userno.setPlaceholderText("") self.input_userno.setFocus() # set mode background color to blue with rounded edges - # self.mode.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.mode.setStyleSheet("background-color: #1E90FF") self.mode.setText("Ausleihe") self.activeState = "Ausleihe" @@ -161,6 +234,8 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.mode.setText("Rückgabe") self.input_file_ident.setEnabled(True) self.input_file_ident.setPlaceholderText("Buchidentifikation eingeben") + self.input_username.setPlaceholderText("Bitte erst in den Ausleihmodus wechseln") + self.input_userno.setPlaceholderText("Bitte erst in den Ausleihmodus wechseln") def showUser(self): log.info(f"Showing User {self.activeUser}") @@ -180,7 +255,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): ) # self.user_ui.setFields("John Doe", "123456789", "test@mail.com") self.user_ui.show() - + def setUserData(self): log.info("Setting User Data") self.input_username.setText(str(self.activeUser.username)) @@ -188,7 +263,6 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.userdata.setText(self.activeUser.__repr__()) today = QtCore.QDate.currentDate().toString("yyyy-MM-dd") self.db.setUserActiveDate(self.activeUser.userid, today) - # self.mode.setText("Ausleihe") def createUser(self): log.info("Creating User") @@ -260,7 +334,20 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): value = self.input_file_ident.text().strip() log.debug(f"Handling Line Input {value}") if self.mode.text() == "Rückgabe": - self.returnMedia(value) + if value in self.db.getMediaList(): + self.returnMedia(value) + else: + # create warning dialog + log.info("Invalid Input") + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Ungültige Eingabe") + dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning) + dialog.setWindowIcon(Icon("warning").overwriteColor("#EA3323")) + dialog.setText("Eingabe ist nicht in der Datenbank\nBitte prüfen und erneut eingeben") + dialog.exec() + self.input_file_ident.setFocus() + self.input_file_ident.clear() + return else: if not " " in value: # create warning dialog @@ -294,7 +381,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.input_file_ident.setEnabled(True) return book_id = self.db.insertMedia(media) - self.loanMedia(user_id, [book_id], media) + self.loanMedia(user_id, [book_id]) else: newbook = NewBook() newbook.exec() @@ -410,6 +497,8 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): dbg("Book not found") #print("Book not found") #self.input_file_ident.setPlaceholderText(f"Buch {identifier} nicht gefunden") + + def setStatusTipMessage(self, message): dialog = QtWidgets.QMessageBox() @@ -460,9 +549,21 @@ def launch(*argv): options = [arg for arg in options if arg.startswith("--")] #print("Launching Main UI") #print(options) - + QtCore.QLocale().setDefault(QtCore.QLocale(QtCore.QLocale.Language.German, QtCore.QLocale.Country.Germany)) + SYSTEM_LANGUAGE = QtCore.QLocale().system().name() + print(SYSTEM_LANGUAGE) + + # Load base QT translations from the normal place app = QtWidgets.QApplication([]) main_ui = MainUI() + #translate ui to system language + if SYSTEM_LANGUAGE: + translator = QtCore.QTranslator() + #do not use ascii encoding + translator.load(f"qt_{SYSTEM_LANGUAGE}", "translations") + translator.load("app.qm", "translations") + app.installTranslator(translator) + atexit.register(exit_handler) sys.exit(app.exec()) # sys.exit(app.exec()) diff --git a/src/ui/multiUserInfo.py b/src/ui/multiUserInfo.py index 6a4ede0..8f8c88c 100644 --- a/src/ui/multiUserInfo.py +++ b/src/ui/multiUserInfo.py @@ -29,7 +29,7 @@ class MultiUserFound(QtWidgets.QDialog, Ui_Dialog): def selectUser(self, row, column): #print(row, column) user = User( - id=self.tableWidget.item(row, 0).text(), + userid=self.tableWidget.item(row, 0).text(), username=self.tableWidget.item(row, 1).text(), email=self.tableWidget.item(row, 2).text(), ) @@ -39,6 +39,6 @@ class MultiUserFound(QtWidgets.QDialog, Ui_Dialog): def displayUsers(self): for user in self.users: self.tableWidget.insertRow(0) - self.tableWidget.setItem(0, 0, QtWidgets.QTableWidgetItem(str(user.id))) + self.tableWidget.setItem(0, 0, QtWidgets.QTableWidgetItem(str(user.userid))) self.tableWidget.setItem(0, 1, QtWidgets.QTableWidgetItem(user.username)) self.tableWidget.setItem(0, 2, QtWidgets.QTableWidgetItem(user.email)) From 42df8a70f63c3af7d6b7950ca5cb5de1bfe35c10 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:52:04 +0200 Subject: [PATCH 70/83] update docs --- docs/Ausleihsystem.md | 1 + docs/index.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/docs/Ausleihsystem.md b/docs/Ausleihsystem.md index d42b07b..8dda56d 100644 --- a/docs/Ausleihsystem.md +++ b/docs/Ausleihsystem.md @@ -7,6 +7,7 @@ ![Nutzeroberfläche ohne Bewerkungen](images/main_marked%20areas.png) Die Oberfläche kann generell in drei Bereiche unterteilt werden: + - Konto und Ausleihe - Nutzerdaten - Historie diff --git a/docs/index.md b/docs/index.md index 0f0c53a..814a841 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,4 +18,7 @@ Unterbereiche umfassen: - [Bericht erstellen](Bericht erstellen.md) - Bericht für einen festgelegten Zeitrahmen erstellen - [Einstellungen](Einstellungen.md) - Einstellungen der Anwendung ändern +## Navigation + +tbd From a1bb56597c27295aab90aee87989925ba1e5ce03 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:56:11 +0200 Subject: [PATCH 71/83] remove navigation --- docs/index.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 814a841..0f0c53a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,4 @@ Unterbereiche umfassen: - [Bericht erstellen](Bericht erstellen.md) - Bericht für einen festgelegten Zeitrahmen erstellen - [Einstellungen](Einstellungen.md) - Einstellungen der Anwendung ändern -## Navigation - -tbd From d67b2e0cddbae804a732f6cb6d030d5a43df0172 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:30:04 +0200 Subject: [PATCH 72/83] enable documentation, add docport, fix newentry bug --- src/ui/main_ui.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index e6f533d..208569e 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -1,9 +1,8 @@ -import os import sys import atexit import datetime import webbrowser -from src import config, __email__ +from src import config, __email__, docport from src.logic import Database, Catalogue, Backup from src.utils import stringToDate, Icon, Log from src.utils import debugMessage as dbg @@ -18,8 +17,7 @@ from .settings import Settings from .newBook import NewBook from .loans import LoanWindow from .reportUi import ReportUi -from src.utils import launch_documentation -from PyQt6 import QtCore, QtWidgets, QtGui +from PyQt6 import QtCore, QtWidgets from omegaconf import OmegaConf from src.logic.documentation_thread import DocumentationThread backup = Backup() @@ -89,7 +87,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.activeUser = None self.activeState = "Rückgabe" self.docu = DocumentationThread() - # self.docu.start() + self.docu.start() self.duedate.setDate(loanDate) # functions @@ -146,6 +144,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): dialog = QtWidgets.QMessageBox() dialog.setWindowTitle("Einstellungen geändert") dialog.setIcon(QtWidgets.QMessageBox.Icon.Information) + dialog.setWindowIcon(Icon("settings").icon) dialog.setText("Einstellungen wurden geändert\nProgramm neu starten?") dialog.setStandardButtons( QtWidgets.QMessageBox.StandardButton.Yes @@ -166,7 +165,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def openDocumentation(self): log.info("Opening Documentation") - webbrowser.open("http://localhost:6543") + webbrowser.open("http://localhost:{}/".format(docport)) @@ -277,6 +276,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): # set user to active user self.setUserData() self.activateLoanMode() + self.input_file_ident.setPlaceholderText("Buchidentifikation eingeben") self.input_file_ident.setFocus() @@ -421,15 +421,16 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): return newentry = NewEntry([book_id[0]]) newentry.exec() - self.setStatusTipMessage("Neues Exemplar hinzugefügt") - #print(created_ids) - self.input_file_ident.setEnabled(True) - newentries = newentry.newIds - if newentries: - for entry in newentries: - book = self.db.getMedia(entry) - self.loanMedia(user_id, [entry], book) - dbg("inserted duplicated book into database") + if newentry.result() == 1: # only create dialog if new entry was created + self.setStatusTipMessage("Neues Exemplar hinzugefügt") + #print(created_ids) + self.input_file_ident.setEnabled(True) + newentries = newentry.newIds + if newentries: + for entry in newentries: + book = self.db.getMedia(entry) + self.loanMedia(user_id, [entry], book) + dbg("inserted duplicated book into database") return else: #print("Book not loaned, loaning now") From 6e9971b89d648567615beec01e077733c2d7b633 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:30:29 +0200 Subject: [PATCH 73/83] disable user deletion if has lent media --- src/ui/user.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ui/user.py b/src/ui/user.py index f74aca7..bc3a7b3 100644 --- a/src/ui/user.py +++ b/src/ui/user.py @@ -32,6 +32,9 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): self.btn_extendSelectedMedia.clicked.connect(self.extendLoan) self.deleteUser.clicked.connect(self.userDelete) self.deleteUser.setIcon(Icon("delete").overwriteColor("red")) + self.deleteUser.setEnabled(False) + self.deleteUser.setToolTip("Nutzer löschen nicht möglich, solange Medien ausgeliehen sind") + self.btn_extendSelectedMedia.setEnabled(False) # radioButtons self.radio_allLoanedMedia.clicked.connect(self.loadMedia) @@ -40,6 +43,10 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): # frames self.frame.hide() + if self.UserMediaTable.rowCount() == 0: + self.btn_extendSelectedMedia.setEnabled(False) + self.deleteUser.setEnabled( True) + else: self.btn_extendSelectedMedia.setEnabled(True) # table self.UserMediaTable.horizontalHeader().setSectionResizeMode( @@ -54,6 +61,7 @@ class UserUI(QtWidgets.QMainWindow, Ui_MainWindow): self.name.textChanged.connect(self.showFrame) self.user_no.textChanged.connect(self.showFrame) self.mail.textChanged.connect(self.showFrame) + self.show() From 5c7284e584315d67a77e4dc69c6f88216fae1266 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:40:26 +0200 Subject: [PATCH 74/83] add function to check if to be deleted user has loans --- src/logic/database.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/logic/database.py b/src/logic/database.py index 16beeaa..7ba1d0d 100644 --- a/src/logic/database.py +++ b/src/logic/database.py @@ -273,7 +273,18 @@ class Database: if len(result) == 0: log.info(f"Deleting {len(result)} inactive users") for user in result: - self.deleteUser(user) + hasLoans = self.hasLoans(user[0]) + if not hasLoans: + self.deleteUser(user) + + def hasLoans(self, userid)->bool: + query = f"SELECT * FROM loans WHERE user_id = '{userid}' AND returned = 0" + conn = self.connect() + cursor = conn.cursor() + cursor.execute(query) + result = cursor.fetchall() + self.close_connection(conn) + return False if len(result) == 0 else True def deleteUser(self, userid): log.debug(f"Deleting User {userid}") From b5687876096d00cb00a2c51103d83f7555965f28 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:43:16 +0200 Subject: [PATCH 75/83] update documentation --- docs/Ausleihe_Sonderfälle.md | 16 ++++++++++++++++ docs/Ausleihsystem.md | 3 +++ docs/images/book_addNew.png | Bin 0 -> 7892 bytes docs/images/book_loaned.png | Bin 0 -> 4179 bytes 4 files changed, 19 insertions(+) create mode 100644 docs/Ausleihe_Sonderfälle.md create mode 100644 docs/images/book_addNew.png create mode 100644 docs/images/book_loaned.png diff --git a/docs/Ausleihe_Sonderfälle.md b/docs/Ausleihe_Sonderfälle.md new file mode 100644 index 0000000..e9a9d02 --- /dev/null +++ b/docs/Ausleihe_Sonderfälle.md @@ -0,0 +1,16 @@ +# Sonderfälle bei der Ausleihe + +In einigen Fällen kann es bei der Ausleihe zu Problemen kommen. Diese werden hier aufgeführt. + +## Medium bereits entliehen + +Diese Meldung erscheint, wenn ein Medium bereits entliehen ist und erneut ausgeliehen werden soll. +Es erscheint folgende Meldung: + +![FehlerAusleihe](images/book_loaned.png) + +Wenn "Yes" geklickt wird, erscheint ein neues Fenster zum anlegen eines neuen Exemplars. + +![AddNewBook](images/book_addNew.png) + +Über das + Symbol können neue Exemplare hinzugefügt werden. Es ist empfohlen, die Signatur jeweils um +1 zu erhöhen, um eine eindeutige Zuordnung zu gewährleisten. diff --git a/docs/Ausleihsystem.md b/docs/Ausleihsystem.md index 8dda56d..4fa3510 100644 --- a/docs/Ausleihsystem.md +++ b/docs/Ausleihsystem.md @@ -68,6 +68,9 @@ Um ein Medium auszuleihen, muss die Signatur in die entsprechende Zeile eingegeb Die Signatur muss mindestens ein Leerzeichen enthalten, ansonsten wird eine Fehlermeldung angezeigt. +Sonderfälle siehe [AusleiheSonderfälle](Ausleihe_Sonderfälle.md) + + ## Rückgaben !!! info diff --git a/docs/images/book_addNew.png b/docs/images/book_addNew.png new file mode 100644 index 0000000000000000000000000000000000000000..020ad68f22aabd07391f43a0dbc8dce9f075283e GIT binary patch literal 7892 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 literal 0 HcmV?d00001 diff --git a/docs/images/book_loaned.png b/docs/images/book_loaned.png new file mode 100644 index 0000000000000000000000000000000000000000..9e699feb5cd96c1ad0959d00f712b2b02a763df3 GIT binary patch literal 4179 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 O06H25Pb<_MBmV<5dIt3X literal 0 HcmV?d00001 From 26ecf20aaddc02beadcaf0f78dfd520929ba9fee Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:43:56 +0200 Subject: [PATCH 76/83] documentation, settings change --- src/ui/settings.py | 30 +++++++++++++++++++----------- src/utils/documentation.py | 3 ++- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/ui/settings.py b/src/ui/settings.py index 8f74940..8acce08 100644 --- a/src/ui/settings.py +++ b/src/ui/settings.py @@ -2,6 +2,7 @@ from .sources.Ui_dialog_settings import Ui_Dialog from PyQt6 import QtWidgets, QtCore from src.utils import Icon from src import config +from src.utils import debugMessage as dbg from omegaconf import OmegaConf import os @@ -15,6 +16,7 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.originalSettings = config.to_Omegaconf() self.changedSettings = config.to_Omegaconf() self.shortcuts = config.shortcuts + self.shortcuts = self.sortShortcuts(self.shortcuts) self.settingschanged = False self.restart_required = False @@ -92,8 +94,6 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): ) self.settingschanged = True - - def selectDatabasePath(self): databasePath = QtWidgets.QFileDialog.getExistingDirectory( self, "Select Database Path", self.originalSettings.database.path @@ -108,7 +108,6 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): self.settingschanged = True self.restart_required = True - def selectDatabaseName(self): # filepicker with filter to select only .db files if a file is selected, set name to the lineedit and set database_path databaseName = QtWidgets.QFileDialog.getOpenFileName( @@ -141,6 +140,13 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): } ) return shortcuts + + def sortShortcuts(self, shortcuts): + short = [] + for shortcut in shortcuts: + short.append(shortcut) + short.sort(key=lambda x: x["name"]) + return short def saveSettings(self): # save settings to config file @@ -162,8 +168,6 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): if database_path != self.originalSettings.database.path : os.makedirs(database_path, exist_ok=True) self.restart_required = True - if shortcuts != self.originalSettings.shortcuts: - self.restart_required = True @@ -182,18 +186,22 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): changed = self.changedSettings original = self.originalSettings + + + if changed == original: - print("No changes") self.settingschanged = False self.restart_required = False + dbg("Settings not changed") else: - print("Changes detected") self.settingschanged = True - if original.database.path != changed.database.path or original.shortcuts != changed.shortcuts: + #compare if database or shortcuts were changed + database = original.database == changed.database + shortcuts = self.shortcuts == self.sortShortcuts(changed.shortcuts) + if not database or not shortcuts: self.restart_required = True + dbg(f"Settings changed, restart required: {self.restart_required}",database=database,shortcuts=shortcuts) - - # save the new settings if self.settingschanged: # save the settings @@ -202,7 +210,7 @@ class Settings(QtWidgets.QDialog, Ui_Dialog): config.updateValue("database.backupLocation", self.changedSettings.database.backupLocation) config.updateValue("database.path", self.changedSettings.database.path) config.updateValue("database.name", self.changedSettings.database.name) - config.updateValue("delete_inactive_user_duration", self.changedSettings.delete_inactive_user_duration) + config.updateValue("inactive_user_deletion", self.changedSettings.inactive_user_deletion) config.updateValue("report.report_day", self.changedSettings.report.report_day) config.updateValue("report.generate_report", self.changedSettings.report.generate_report) config.updateValue("report.path", self.changedSettings.report.path) diff --git a/src/utils/documentation.py b/src/utils/documentation.py index 0b5bf8e..f4a0cc8 100644 --- a/src/utils/documentation.py +++ b/src/utils/documentation.py @@ -1,5 +1,6 @@ from pyramid.config import Configurator from wsgiref.simple_server import make_server +from src import docport import os def website(): config = Configurator() @@ -13,7 +14,7 @@ def website(): def launch_documentation(): app = website() server = make_server('localhost', 6543, app) - print("Serving MkDocs documentation on http://0.0.0.0:6543") + print("Serving MkDocs documentation on http://0.0.0.0:{}".format(docport)) server.serve_forever() if __name__ == '__main__': From 132e4ca9c17c439a1a5d00949c838e7f39948771 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:20:08 +0200 Subject: [PATCH 77/83] update settings --- config/settings.yaml | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/config/settings.yaml b/config/settings.yaml index 67df6fd..dcd0da8 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -1,17 +1,34 @@ -institution_name: +institution_name: Test default_loan_duration: 7 inactive_user_deletion: 365 -catalogue: True database: - path: + path: ./database name: library.db - backupLocation: - do_backup: True - + backupLocation: ./backup + do_backup: true report: - generate_report: false - path: - report_day: 1 + generate_report: true + path: ./report + report_day: 0 +shortcuts: +- name: Rueckgabemodus + default: F5 + current: F5 +- name: Nutzer + default: F6 + current: F6 +- name: Hilfe + default: F1 + current: F1 +- name: Bericht_erstellen + default: F7 + current: F7 +- name: Ausleihhistorie + default: F8 + current: F8 +advanced_refresh: false +catalogue: true debug: false log_debug: false ic_logging: false +documentation: true \ No newline at end of file From ac45c85034fcc67021bfdbf91faadafcf11a4bb8 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 8 Oct 2024 07:51:04 +0200 Subject: [PATCH 78/83] add logo icon --- icons/icon.ico | Bin 0 -> 67646 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 icons/icon.ico diff --git a/icons/icon.ico b/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9c09b65fef5f100cdf3c6d2bc2c3aa85d974eeeb GIT binary patch literal 67646 zcmeHQ34EMYwV$+Q6YZ9*Nt`Rjt5W$722ts9Z;elHbWKn<013`9LR2ErUx-Upc zX+=>K9x4{p$5VK?bxYbbZPJ~BZGlos>5?Wn{{MHG`M#OS*36_c;rseK{kF+H_n!Yf z=iYnHz4!C^GVpK86d(Tg`$~_`@a6b?zETh#@u7OoOJ4mG|C8*IofmizB%oGv=Cw!t zTLuA{lmW5r4FAj#0VygB%DBpqRF{XO#-n~A z17b-`i7hck9f;BY5hDYw9~kDBd#)&x=0~b!+4Qlpa;8W9K?cNL#;DE(#N;h+8(_C^mq{5||V`~Uu}13l6Idof1X_-M7Pz}S!d zMvu~82E>w>vTxs2|8MHI%4FxyYGnO_v9jT()gJW&84yciIt|!%gg<=#t;69TJheI` z7fvXWi61YL&wlbRy0k zrLPPuj>MGM5@TW=4}UysN8MBDGm-ocHz|I45p0o1<8QG%(zU+d)ApB-M z?1oA+5D2PqA$AKN@7Ed9QZe9=7o;W%PWYF zF|d!V5k6m7|3?Lm0sc)+{9k#fO4iOpToAsY-uR@wzfGL{hy^iuWoj(`(EU#J-@`v~ zI2^4y?ctyP81|;_c=&tx_m%yB34ILKRnCiJzr&-WF9TvhOo&Z%|1Tl!zZw5=Y(ZPI zeNnX5BS_5A)Giyb{5OX>jaEOgL99YW&xn2>dYy;Jo3sQ3V0H^(&?F zqi>YSU6+;09bYSzo4#BsHyLVogUNdh;9Mj-txNh>z(srKKPs7@?l;( zDu;beUMzcWc@YDqNlb_hF(OvPjAQpi;g7W1vM_g?afIWCg3S3iT=$k#T1>AK>Malc zeK_Bp@ZoO~!mBeL=3PSgIIti##E4iW5dTJ#)*+k^BaYytHfJm-e^fw5qW*P7&c9(? zA}UR1`p#?qE*y92iU;54AwSN)!F6jlc=0#kzhfHR;pO}rTqmtelyMXXM*(Y(li_b+PXnhQ&mJ|TZL-uXF`-ar>4+~B$5k28P6GPSZyOoM5~D-(G$ z4;%wCPY%C(1JYognXb#nZk}0QEIaXU<+{8V?x|EVqO9VTiL$*LGG@LgbMv>vfP4l; zm+46F;s1y3lVAfh*2LRM#~YqE;;iyQn@e5bJj3NPV#macO|6?hR-Qu4)bvp-CVw(% zG1lbd!E44JC@a2ev~h?rkoEuFk2q8ZehBjC+{JH9ELQD=1OG1|9hQmtkLG2|^m)8f z_|vX44W<>ZOdNMJ4}U>km?wq5#&aFgV4j(GmLZxK-!UI7dl>(x?x;{QqO79*D&$Yu zQpS|E1OFeGwBnU%P-b4xN1_c#)B%1h5A#nO7RH~pi}jXyUp=SVMT`|PX8bhb)KU2V z(WIsH#hFJuxB5>xy6~s3qY?Fq{)lK^eVO-8jEG!T4kCEo5core#>J%9JknubfdXyBnXD z18=UA_jc6Dd#|sMwY6tT)BG}ddU91l^#A3Ds-JHA$y!uCIcKkF`!_V!k3Z#^iDi_c90{)Mvf>2GL0 z;K5^0^BRJjlCYW?@3{ZzBGX6?ss0JSfX_!+-yddX;z9Vd_ic;w0(6 z#Xqe7OJxPHO1S-3_%Bsu3hTeZKe76+@u&XNo~5t;d-xwg|DWx@(ti(sj$bg&=y3d} z`hRTyJ^fFi@n59<_w?V>f87D_+JC1n#`tQiZ^-5U<=EJ>{}E|<_TTIOIrPQl|MlAc zaQw$>|2b0f#(xQm|3xft{43~W2mXJr|7X@+9$EayYyVvp&TId@_TOv&Ma5D)|F1j# z@AdyuyZ_hs@t;G=-Js$>N%Q~P_>bnL<3Gvp|J(Ran<`UU#((--{Fm{c4bLk)>{x^G z--e{cf73Dk!}(u2{=cW`IXU#sHaYNC6Xr@Z%GyO|!MBy@`5#5n{Hsgk&>rvskCwf! z$s4O`5&tbreEbh-zOk}a@gOhqB=3eUDKCzCP%E4oa{ng6H1z2Khzr^}Qs^S|_5Nxddj%S_K{i7^xx%1OZZL4 z{$J<(e>Wx`{u%~{Ka2BQ-0@#E|KGO%Zuxok-|heL?0+iye?0x~Rs6@(e^*-t``F?9 zKTrQX{g15wi)|~t{=Z9=5_;qH|8#xx#(&=UFY)Vtz5c&7R$lubY1h2*pEv&V{J)<6 zH?`M)d*gp^{BQS4iL_mw|KIj#)S5>6edDkH@%%p#LE*~l|3!4j>;HNEzhw3QxHni& zs5Ac$_s?cswevpgxf}66WfPtGNT&Wf`S99dlRMe}%U!PS`hT3fz{km}NAW+(wzK}9 zlTUZ9t+6$EV!pTsTl8La@$T(jd9MBcZpYqq+$V7b_NuEx9dHu&%3Fu;(Eb{G+(qsG zX7~T&UbFFdv0m_%`f(UJRN8n%4;}z`N<7 zzk#?Paof-kz9OcaSUC0Ru_D%YlHDtJJFaJe=7XYn+GphV zHi)*PJrDbHU-Zc$x#TmkJjj>)zl?m;RD@(0@`e0aUQ3b23=q?c=EY}x$KR;G;omY1 zrqxlIm`BjjT1?;tw4P_KhcH~dlQpS`uWp4hK7!V7~ z&&kJmO-vVq&u4L_4h*omhHKggKI0z3%#Yn$me<`P?#;`+vembvv2QBR@#uq@iP`^~ zdu7K<-^t&|sayGvS5B6RX*ko2$CG&>e(}=i$ur6(Uiwb{z1Y*-VtPCBM6AQ>g?VkB zsrDS#2SA5W{=?X3x-)9Sd+tT8h&sUi={qCtr*D66&h0pN#>2kj@4|n}bUNc>_sMtu z4X#_e!Hd5M^T9s5UGnM6GEv6P{Nb7ySQ*8fi6OBxn6i%A{et7+e-qyGH|ksPL!r;= z+wZ8CCwi2AGJsuD_QGw`Zw9^{>A%9C^@+A<(z(U*_zx;%?)S=N{(qOtg6pbW#AhC* zrVQp@TduIX3>XqiVrq1|OZ>UFIQy9Qd{g)5b>A0zs?QBc^W31K4f6_P`l!);R_{Kx zAKzCpA!Zye+zSk&_6zS8{@nYX-@Radm(Qq_`ne~|=I5@HElY2dEzjR18=w8Q)Xk~E z_`51*tQtLb_3mTG^U0Qp^%wA)A;i-0y?}1xKLy_v+oINUmPzAdpOm(ieQNzH|2u$v zu(?P1>bW&$uX?v{%=-Pj^U;wDeN*%o^4oAnj6dp51pf)rdT6Nl({>kAz{neiC+?(9g1AePu{S)WO zs;Of+g;cy|Yi$kM`U>>-#$kVj zMd3Dp-zaEr(|gpn9X=qhzkEOJK#A0YJX>&d>H*`dtQ(9KKRvlpP8l2a7u)e)Xa1jC z%oyOeccGnbhgkQ**3k+t&}SE-U^501f}+m~Vgi!JO1>hC-7TNTC@?A!iNS-nBUP4s^Vs} zA0HhblK+`fDccsEB&!|{VLUiq-dg>HsspMXIBh}8d-B>}@0PmhMXGP$*@EQS0_r@! z{Y{VfJPIBdbY+x}nHJ${y~n#O*CXA8P&vybHc zO6YtOz8S}FEArcbz)ku4h#9ftJ&5@KI4j?OY!<)m2&`@F&tT7Is!rgn;_x`10;&V4 zzejrN8>O@bXPip>jf0dEzjGdgc^XPf^()=10?8l~`MjMvVm@xc~^@wXRv@5{*# zg2HuWATVd0I1RKG6tM-UXS4;MKDAJuzq?Ad!WOKY7Le5oPL;RUJrmy+9M~hT{`oek zWB&oZL$6=Zvwi_}o^#5vjrm(;3s?t0 zC*avbAl1g;zLoWj@nOcle+WNg^P{u{70MQDdG1?Cf0D+9 zXBb;BPWucJXd92D9ZmJWq0Up^*}vy^wmH9nvZwAtU#-p)8_Wgrf571A(s_QPl`w0lon}M%STyv>nkdZGOBUewQ8*n+76S@X#0YVShR zAgDTE`vr&hza`sVxDjIs^dIo8SkD&3w*^+`>C6A~y_Is#M~jp{pE9THfrG_<1Fk=n zquX+}^8>Tch7TJt8u$f$zKV+OLLdHJc5W!f7BDT)iAbOG*I3`$S^roMIS*=NK|sYE zH^3IGn@3v^kgd;uUk>koGp;Sz)4WvHKKfDE0@Rmzo-J_NA?iH)^_&m-lPk-lV6>L8 zL+9;$!+F5rgCOGN()k?N^WGezMvcOl5cdDmLw(t!jV*}uA9d)>3r@W^Rs6W+hJfI81TB{m{< z^UyVA>bue=eq-BttMiYdjUN)PooD|&8Aq;Z4>EK7z9ECrH$Z(uKX3s1hqyi$w9V85 zWeZqenfD9ND3ZTEfcjjE?`UJ3*sySd?A`pLsspG1E?dy@?he`V`|qf}Vg2m)k1bF- z5A3%r)Zdf;Jm!>Bc5!rmJ7j!bb}nQ-LXG7I4lM}yGDiiK&S%12C;MQ(0(r2c#t6D! z5EzQ*wWbb)`voiq*GD}3?Q+@l*mzkxrwn^^hh+Qz-hnvmcb)NCs8Q&?FU@&N7w$VDj1acm3^v%`I!&j4R*j?%8=8jhN4fWGE4E6w+qtEF_ z8>Fr;!2Ro{9)xWH`-flrRFS+gwHj^PIP?w2sBhiByY(fvEim7`KD2kcZ2s-lum$iR z&qfTI{lk9LFQCp7d+t^Cx2aWF_fe$!(8FTe`5lnu=d<9O!CAHQ^y%c#uLE13*@JPN z3&aH6WVy!(%p2<^^O*&D0>pknn;j$27KBj0XMML^Ha~WfG|Vnl`}M!}mmf*nAw9n= zVheP&cx&}9F#q5rRaN1VGs9Byw}9^YAZ`MyN3(eFX#92Sq+F2JHle#|WHTKwEIhXNu(S)2dMq zv@K|S{A}5`{qON@0lv4e>6y!oEvSMIIX!KGQ|JFNtxB!wq92FrjLdl6_U+$*%q~@9 zFVoIz+p61Mw!x`(&}W1^TXNchTs(iu)B$A+SkE~xvA8g()(~u|JxSKk7Cc-mufKFJ z=9=!0>KEv0@n+o;6-%s}9x^_o^sxm_oqy!Iaw#o>&ckoUSc-|YTb+LfvdS~Eq&-zS zPaDAaT`C{hBb1N8)&ZXBi^@P;nb%(dy#=zipq=%e@uaVQwpji(9ku}T4VPn%$+{&I zW#4Pd;@X0PyEnRHEoHwN zb`<@3?RQo7HI=b<<}J!!;IsuJQLbk}s$al*&-zb2C@T)iPp_|l{}{GlR*9^fULyZ~ zIX2koT~i->OA)dpfBgK8_K1kq(hzmJ!Db1Rf9;FX<)XhJBPUQZA1#->caz^U_Gi}B6#idETIMzr%zoqq>1xN>OT7$1E4M(1fO4IRb4 zeWL0d`zma^Odr;13r3^tOHCc%I#y*17+0<=4ap*`J=}`Q0LbeonL%Yg^Vo^saQGs@zL%4A5nHc$gT5?;iPyw z-_yFm*rWClsrV7gI}mdaGO|ajvR{q+cR?;&!1{m)En9X>d<#C!rB6?Ii-Hl7Bv6z zzvRH1>y0h2b4-ja*jX=4KflP>g5<4FvpUZhE!+I>exXG5>j$GB6V=Y|N13kA@|XCs zb95XP^&5R5-7XFvC8NIDu_9zdIXP`X1@5`W zRk&ZkYwFplHC&$-u?2OrvFE|6$6*BYJVR{@61zT)^D?Z?^IN9ekBfO>p5W7@zFVFD zC$7hWSRSMEy6@Z9vASg7m5ekQOVsjGa-#kZ${eY#DZkr5hd{~}ux{diIBd<*h;XI5Tt5cM0^(0S#{Q)SB>`Tn-|-KjTvoM?R@E?ZCoc`i3~ zpw;M`(!Y4?)86cAeHz9GT(7FXwcPPIkviYu`YQJv%$3O7c#ze3)?4NO;<(N`x-S8) zLojmWS=oXt#u*d2K5gsEvVO@qYHmSCb)d)VtDxV=udAI;#aLbUkvO`)RB;5xKXeR) z`lNIU=U%Q))4s#~yBf^;gDRIDP&yC2KZ5mDn0Jks4JfA{_->Cb`kH%GEJ^nZ;46V% z^?ZHWUb8;U#F4f=e*=8RA-z7;_zKz1Cu4n8D#$XO^8lSU>(f-*plku>AmjPzpzf_t zn}4csZXu7|jZdq2rgo14r_Lv9eN{TkKNa$B<`>$&fwBc^-ceq#TkF&GniRimdG32^ zJ!<_-?qR0CS6{0>J}n>X)40A#^$BdhU;^&DSf4fyYgI#t(Nv&cufMl= zG}c$8v;0#b@A22Css10u>SQkalOgZ1K0XAeusc-t6q8_HP%<9GX|-U_jqFj;Lf%o?)o&&J>{J1BFsNE z`{PEhwRjYLjUn{aA`RDgo-sS`NIeOt^=X(plbJonH|#@Lw`%qdOX8`QTle}j zRTt1N!1^>b4=r#$uzuBGPTXGuU6hLJt5Q$0>6N3b+(K&ycxJzlZ8@*GKJ5>n7hnV8 zuCL1V;`8YRb`o;i;!bmt`_|`+exK_1f()KN#dqQ=U&i%5-y~_lE|3#^zT=1be7wQ~ z_j~W&lZUgu^tL{1VJ(}&`j)|~<7)YTr2ZWfoqnxb;g0EV_p99HJ8_BozNp@PKXLmM z=Yx!rmI==L0h3xEcHWSOn+j`yEsC z4>EG~{oV=se!vC#zHO+!_r0j^+l>f@PuBNRtKX~K^3gx^{Y>-P<2S5Wrc# X0-=f=ZGJ)(Ioj`2P(>arv4#H+t|KlU literal 0 HcmV?d00001 From d7b3754d07ca9be9563dbe3166d64fb7d89562f1 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:13:52 +0200 Subject: [PATCH 79/83] set correct color, disable button until new entry created --- src/ui/newentry.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ui/newentry.py b/src/ui/newentry.py index 163e370..00f9445 100644 --- a/src/ui/newentry.py +++ b/src/ui/newentry.py @@ -9,7 +9,7 @@ class NewEntry(QtWidgets.QDialog, Ui_Dialog): super(NewEntry, self).__init__() self.setupUi(self) self.setWindowTitle("Neues Exemplar hinzufügen") - self.setWindowIcon(Icon("newentry").overwriteColor("#ffffff")) + self.setWindowIcon(Icon("newentry").icon) self.tableWidget.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.ResizeMode.Stretch ) @@ -19,8 +19,15 @@ class NewEntry(QtWidgets.QDialog, Ui_Dialog): self.populateTable() self.btn_addNewBook.clicked.connect(self.addEntry) self.buttonBox.accepted.connect(self.insertEntry) + #disable buttonbox accepted + self.buttonBox.button( + QtWidgets.QDialogButtonBox.StandardButton.Ok + ).setEnabled(False) def addEntry(self): + self.buttonBox.button( + QtWidgets.QDialogButtonBox.StandardButton.Ok + ).setEnabled(True) # clone last row and its data row = self.tableWidget.rowCount() self.tableWidget.insertRow(row) From c85a777e5b801f6466aa0953eb497b5e32594d65 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:14:18 +0200 Subject: [PATCH 80/83] update requirements.txt --- requirements.txt | Bin 3222 -> 2014 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3aa802c094007dddf513e2cd78df6bea5e46feab..bc5f67d1b0a49c4f327737cf958dd11b885910b5 100644 GIT binary patch delta 79 zcmV-V0I>g-8Qu>9|NfCCVv)E`lW+kllePhJlV}1Yll}rGlSTu0v)TjX0h0;_s*~0R lD3fjnHj~H)N|V9}Jd=V6Jd^ARy0gj(3IUTC3^FC={x=D6V8CP9}rVq)9Zo2>A`+ zL7~h3fG&!wE=qreTlZab-CgyZ`;w_&h&*N(-aGG{bI-lW+05tp#q)oGG^8uBv?Y+1 z)TNA-mlxv6E4+*HSRTo6B3uaM0Jx^OKpo?4zau%4NbmSW54X0W5TGtrV{~B23Q9?` z%jd52fC{C9J5|}mcQ-Xa!q+O#^J2PBK7aUad9EfkNp#y&a{Bd#r^j!qFtsi3!5L#E zvX3{*q6f0(m9+x3M#^f-&6{`5(x9fL9v~818tJKO@5R_X#Iry0>cgAm^_2JW`CavS zqnMdVjNn@cHIY4OnSnE(V?!cUVU9Kxn<0uc#_2z4039KM{|$QyAKuCI#BZmG}DU~;W9(>QS+g0EK;5tCI6~h>P@y(Zv8#CYzEOL@) z)*?zk*b;4Qa2fBh}m^;TU(TuO%GX3n@5_9Qkz6JD!Np>}<2zSQ=!k!h(kL<5p zUGrt?M6v2 From edb530dc9eb07bf24d8361c5b19d2240a96fb26d Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:14:45 +0200 Subject: [PATCH 81/83] add documentation to config class --- config/config.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config/config.py b/config/config.py index d6b37a7..871778e 100644 --- a/config/config.py +++ b/config/config.py @@ -141,6 +141,17 @@ class Config: else: raise KeyError(f"Option {option} not found in configuration") + @property + def documentation(self)->bool: + if self._config is None: + raise RuntimeError("Configuration not loaded") + return self._config.documentation + @documentation.setter + def documentation(self, value: bool): + if self._config is None: + raise RuntimeError("Configuration not loaded") + self._config.documentation = value + def to_Omegaconf(self): return omegaconf.OmegaConf.create(self._config) From a51253c45c735556b70a125bd0145b0f80bdc6dc Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:15:10 +0200 Subject: [PATCH 82/83] make documentation toggleable by settings.yaml / cli --- src/ui/main_ui.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/ui/main_ui.py b/src/ui/main_ui.py index 208569e..c6d2603 100644 --- a/src/ui/main_ui.py +++ b/src/ui/main_ui.py @@ -87,7 +87,8 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): self.activeUser = None self.activeState = "Rückgabe" self.docu = DocumentationThread() - self.docu.start() + if config.documentation: + self.docu.start() self.duedate.setDate(loanDate) # functions @@ -102,7 +103,8 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def shutdown(self, *args): #kill documentation thread log.info("Shutting down") - self.docu.terminate() + if config.documentation: + self.docu.terminate() sys.exit() @@ -139,6 +141,7 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): settings = Settings() settings.exec() result = settings.result() + print(settings.settingschanged, settings.restart_required) if result == 1: #dialog to ask if program should be restarted dialog = QtWidgets.QMessageBox() @@ -165,7 +168,15 @@ class MainUI(QtWidgets.QMainWindow, Ui_MainWindow): def openDocumentation(self): log.info("Opening Documentation") - webbrowser.open("http://localhost:{}/".format(docport)) + if config.documentation: + webbrowser.open("http://localhost:{}/".format(docport)) + else: + dialog = QtWidgets.QMessageBox() + dialog.setWindowTitle("Dokumentation nicht verfügbar") + dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning) + dialog.setWindowIcon(Icon("warning").icon) + dialog.setText("Dokumentation nicht verfügbar") + dialog.exec() From c7c8c1d4842f4bf3e8185b3e0fc1df25a777a73f Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:15:33 +0200 Subject: [PATCH 83/83] add docport, update args --- src/__init__.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 3fed036..0a6f585 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,13 +1,14 @@ import sys -from config import Config# +from config import Config __version__ = "0.1.0" __author__ = "Alexander Kirchner" __email__ = "alexander.kirchner@ph-freiburg.de" __license__ = "MIT" +docport = 6543 config = Config("config/settings.yaml") -valid_args = ["--debug", "--log", "--no-backup", "--ic-logging", "--version", "-h"] +valid_args = ["--debug", "--log", "--no-backup", "--ic-logging", "--version", "-h","--no-documentation"] args_description = { "--debug": "Enable debug mode", @@ -15,13 +16,15 @@ args_description = { "--no-backup": "Disable database backup", "--ic-logging": "Enable icecream logging (not available in production)", "--version": "Show version", - "-h": "Show help message and exit" + "-h": "Show help message and exit", + "--no-documentation": "Disable documentation server and shortcut" } args = sys.argv[1:] if any(arg not in valid_args for arg in args): print("Invalid argument present") - #sys.exit() + print([arg for arg in args if arg not in valid_args]) + sys.exit() def help(): print("Ausleihsystem") @@ -30,7 +33,7 @@ def help(): print("Valide Argumente:") print("args") print("--------") - print("usage: main.py [-h] [--debug] [--log] [--no-backup] [--ic-logging] [--version]") + print("usage: main.py [-h] [--debug] [--log] [--no-backup] [--ic-logging] [--version] [--no-documentation]") print("options:") for arg in valid_args: print(f"{arg} : {args_description[arg]}") @@ -47,6 +50,12 @@ if "--no-backup" in args: config.no_backup = True if "--ic-logging" in args: config.ic_logging = True +if "--no-documentation" in args: + config.documentation = False if "--version" in args: print(__version__) sys.exit() + + + +